likes
comments
collection
share

前端Promise的使用和实现

作者站长头像
站长
· 阅读数 25

Promise

简介:Promise是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一。套规范最早诞生于前端社区,规范名称为Promise A+

导读:针对Promise本文注重使用上细节,以及最后通过分析手写实现Promise。

Promise基础知识

前端中所有的异步场景,都可以看作是一个异步任务,JS中的表现为一个对象,该对象称之为Promise对象,也叫做任务对象。

每个任务对象,有:两个阶段、三个状态

前端Promise的使用和实现

它们之间存在以下逻辑:

  • 任务总是从未决阶段变到已决阶段,无法逆行
  • 任务总是从挂起状态变到完成或失败状态,无法逆行
  • 任务一旦完成或失败,状态就固定,永远无法改变

前端Promise的使用和实现

挂起->完成,称之为resolve挂起->失败称之为reject。任务完成时,可能有一个相关数据;任务失败时data,可能有一个失败原因reason。成功或者失败后可以针对任务进行后续处理,针对完成状态的后续处理称之为onFulfilled,针对失败的后续处理称之为onRejected。

Promise链式调用

针对上述两个阶段、三个状态的描述,如下代码可做“模型”:

// 创建一个任务对象,该任务立即进入 pending 状态
const pro = new Promise((resolve, reject) => {
  // 任务的具体执行流程,该函数会立即被执行
  // 调用 resolve(data),可将任务变为 fulfilled 状态, data 为需要传递的相关数据
  // 调用 reject(reason),可将任务变为 rejected 状态,reason 为需要传递的失败原因
});

// 接收两个方法参数,并放入微队列
pro.then(
  (data) => {
    // onFulfilled 函数,当任务完成resolve(data)后,会自动运行该函数,data为任务完成的相关数据
  },
  (reason) => {
    // onRejected 函数,当任务失败reject(reason)后,会自动运行该函数,reason为任务失败的相关原因
  }
);

实际开发中的使用如下代码:

new Promise((resolve, reject) => {
    resolve('success');
    reject('fail'); // 上面已经resolve进入成功状态,状态不可被改变,该代码无效
}).then((data) => {
    console.log('onFulfilled:', data);
}, (reason) => {
    console.log('onRejected:', reason);
});
// 输出:onFulfilled: success

new Promise((resolve, reject) => {
    reject('fail');
    resolve('success'); // 上面已经reject进入失败状态,状态不可被改变,该代码无效
}).then((data) => {
    console.log('onFulfilled:', data);
}, (reason) => {
    console.log('onRejected:', reason);
});
// 输出:onRejected: fail

当然还有一种更富有语义化的写法(原理在后文):

// 直接处理任务成功的后续处理
new Promise((resolve, reject) => {
    resolve('success');
}).then((data) => {
    console.log('onFulfilled:', data);
});

// 直接处理任务失败的后续处理
new Promise((resolve, reject) => {
    reject('fail');
}).catch((reason) => {
    console.log('onRejected:', reason);
});

// 更多时候,我们不确定任务的结果
new Promise((resolve, reject) => {
    // resolve('success');
    // or
    // reject('fail');
}).then((data) => {
    console.log('onFulfilled:', data);
}).catch((reason) => {
    console.log('onRejected:', reason);
});

上述代码块中的第三种写法,最为常见,在then方法后继续使用catch进行后续错误的捕获和处理就是Promise的链式调用。

链式调用的终其原因,就是因为then方法必定会返回一个新的Promise(新任务)。

then函数

前端Promise的使用和实现

then返回的新任务的状态取决于前任务的后续处理:

若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据

const pro1 = new Promise((resolve, reject) => {
    resolve('success'); // 任务成功
});
const pro2 = pro1.catch((reason) => {
    // 只有任务失败的后续处理
    console.log(reason); // 不会执行
});

const pro3 = new Promise((resolve, reject) => {
    reject('fail'); // 任务失败
});
const pro4 = pro3.then((data) => {
    // 只有任务成功的后续处理
    console.log(data); // 不会执行
});
setTimeout(() => {
    // 因为Promise是微任务,所以为了保证拿到最终的状态
    // 用setTimeout宏任务去输出
    console.log('pro1:', pro1); // Promise {<fulfilled>: 'success'}
    console.log('pro2:', pro2); // Promise {<fulfilled>: 'success'}
    console.log('pro3:', pro3); // Promise {<rejected>: 'fail'}
    console.log('pro4:', pro4); // Promise {<rejected>: 'fail'}
});

这就解释了new Promise().then(() =>{}).catch(() =>{});写法的原理,因为如果new Promise()中任务失败,紧接着的then方法没有对失败进行后续处理,那么then返回的Promise保存有上一次任务的状态和数据,供接下的catch进行失败的后续处理。

  • 若有后续处理但还未执行,新任务挂起。
const pro1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        // 1s后才会执行任务成功,在任务执行之前pro1为pending
        resolve('success');
    }, 1000);
})
const pro2 = pro1.then((data) => {
    // then对pro1任务成功的后续处理
    // pro1为pending时,pro2必为pending
    console.log(data);
});
setTimeout(() => {
    console.log('pro1:', pro1); // Promise {<pending>}
    console.log('pro2:', pro2); // Promise {<pending>}
});
  • 若后续处理执行了,则根据后续处理的情况确定新任务的状态
    • 后续处理执行无错,新任务的状态为完成fulfilled,数据为后续处理的返回值
    • 后续处理执行有错,新任务的状态为失败rejected,数据为异常对象
    • 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致
// 后续处理执行无错,新任务的状态为完成fulfilled,数据为后续处理的返回值
const pro1 = new Promise((resolve, reject) => {
    resolve('success');
})
const pro2 = pro1.then((data) => {
    // 对任务成功的后续处理,没有任何异常
    console.log(data);
    return 123;
});
setTimeout(() => {
    console.log('pro1:', pro1); // Promise {<fulfilled>: 'success'}
    console.log('pro2:', pro2); // Promise {<fulfilled>: 123}
});

-----------------------------分割线-----------------------------

// 后续处理执行有错,新任务的状态为失败rejected,数据为异常对象
const pro1 = new Promise((resolve, reject) => {
    resolve('success');
})
const pro2 = pro1.then((data) => {
    // 对任务成功的后续处理,抛错
    throw new Error('fail');
});
setTimeout(() => {
    console.log('pro1:', pro1); // Promise {<fulfilled>: 'success'}
    console.log('pro2:', pro2); // Promise {<rejected>: Error: fail
});

-----------------------------分割线-----------------------------

// 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致
const pro1 = new Promise((resolve, reject) => {
    resolve('success');
})
const pro2 = pro1.then((data) => {
    // 对任务成功的后续处理,返回了一个没有执行任务的新任务对象
    return new Promise(() => {});
});
setTimeout(() => {
    console.log('pro1:', pro1); // Promise {<fulfilled>: 'success'}
    console.log('pro2:', pro2); // Promise {<pending>}
});

Promise静态方法

方法名含义
Promise.resolve(data)直接返回一个完成状态的任务
Promise.reject(reason)直接返回一个拒绝状态的任务
Promise.all(任务数组)返回一个任务任务数组全部成功则成功任何一个失败则失败
Promise.any(任务数组)返回一个任务任务数组任一成功则成功任务全部失败则失败
Promise.allSettled(任务数组)返回一个任务任务数组全部已决则成功该任务不会失败
Promise.race(任务数组)返回一个任务任务数组任一已决则已决,状态和其一致

前两个方法最为简单,这里对后面四个方法做介绍

Promise.all(任务数组)

任务数组中所有任务全部成功才可

function getPageList(page) {
    return new Promise((resolve, reject) => {
        // 当前方法是同步的,会立即执行
        let flag = Math.random() < 0.5;
        console.log('get page:', page); 
        setTimeout(() => {
            // 通过随机数模拟成功还是失败
            if (flag) {
                resolve(page);
            } else {
                reject(page);
            }
        });
    });
}
const proms = new Array(6).fill(0).map((it, i) => getPageList(i + 1)); // 组合成一个数组
Promise.all(proms).then((data) => {
    console.log('then:', data);
}).catch((reason) => {
    console.log('catch:', reason);
});
// catch: 2  在page为2时任务失败了,则返回失败的处理

Promise.any(任务数组)

任务数组中任一任务成功即可

Promise.any(proms).then((data) => {
    console.log('then:', data);
}).catch((reason) => {
    console.log('catch:', reason);
});
// then: 1  page为1时就成功,直接返回成功后的处理

Promise.allSettled(任务数组)

无论任务数组中的任务成功还是失败,只要任务执行,进入已决settled状态

Promise.allSettled(proms).then((data) => {
    console.log('then:', data);
}).catch((reason) => {
    console.log('catch:', reason);
});

前端Promise的使用和实现

Promise.race(任务数组)

任务数组中任一任务进入已决settled状态

function getPageList(page) {
    return new Promise((resolve, reject) => {
        // 修改一下任务延迟执行的时间
        let time = Math.random() * 1000;
        let flag = Math.random() < 0.5;
        console.log('get page:', page, time);
        setTimeout(() => {
            if (flag) {
                resolve({ page, time });
            } else {
                reject({ page, time });
            }
        }, time);
    });
}
Promise.race(proms).then((data) => {
    console.log('then:', data);
}).catch((reason) => {
    console.log('catch:', reason);
});
// catch: {page: 5, time: 179.99961985689606}
// page为5的任务最先执行,且任务失败了

async和await使用

有了Promise,异步任务就有了一种统一的处理方式;有了统一的处理方式,ES官方就可以对其进一步优化。

ES7推出了两个关键字asyncawait,用于更加优雅的表达Promise。

async

async关键字用于修饰函数,被它修饰的函数,一定返回Promise。

注意: 返回的是Promise,没有返回,就不存在上述说法了。

如下代码:

async function test1() {
  return 1;
};
async function test2() {
  throw 1;
};
const pro1 = test1();
const pro2 = test2();
setTimeout(() => {
  console.log(pro1); // Promise {<fulfilled>: 1}
  console.log(pro2); // Promise {<rejected>: 1}
});

根据前面了解的知识,上述代码等效于:

function test1() {
  // new Promise((resolve) => {
  //   resolve(1);
  // });
  return Promise.resolve(1);
};
function test2() {
  // return new Promise((resolve) => {
  //   resolve(1);
  // });
  return Promise.reject(1);
};

若返回的就是Promise,则test得到的Promise状态和其一致

async function test() {
  return Promise.resolve(1);
}
const pro1 = test();
setTimeout(() => {
  console.log(pro1); // Promise {<fulfilled>: 1}
});

await

await关键字表示等待某个Promise完成,它必须用于async函数中

在普通的Promise方法中,then或者catch是放在微队列中等待任务执行结果后调用进行后续处理。

new Promise((resolve, reject) => {
  console.log('1');
  resolve(1);
  console.log('2');
}).then((data) => {
  console.log('3');
});
console.log('4');
// 输出顺序:1 2 4 3

使用await关键字

async function test() {
    console.log('1');
    const res = await Promise.resolve('2'); // await之后的代码,会等到await得到结果后才能执行
    console.log(res)
}
test();
console.log('3');
//  输出顺序:1 3 2

在使用await关键字后,函数中await之后的所有的代码都会被放入事件队列中,需要等待await修饰的任务成功后才能执行。

async function test() {
    console.log('1');
    try {
        const res = await Promise.reject('2');
        console.log('try:', res);
    } catch(error) {
        console.log('catch:', error);
    }
}
test();
console.log('3');
//  输出顺序:1 3   catch: 2

如果任务错误,await之后的代码不会执行,需要使用try catch进行捕获。

所以async和await的出现,使得Promise代码可以不再以链式调用的形式书写,继而更优雅使用“同步”的写法。

Promise的实现

有了前面的Promise知识,结合我们所学的js基础,我们也可以尝试实现一下Promise。

Promise的状态变化

示例:

new Promise((resolve, reject) => {
    resolve(1);
});
new Promise((resolve, reject) => {
    throw 1;
});

Promise接收一个回调函数(unsettled),透传两个回调方法(resolve,reject),两方法决定了任务成功失败(settled);如果回调方法发生错误,任务也会失败,任务一旦得到结果,任务状态不可逆。

实现:

// Promise状态常量
const PROMISE_STATE = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected',
};
class MyPromise {
    /**
     * 接收一个任务执行器,初始化时立即执行
     * new MyPromise((resolve, reject) => {})
     */
    constructor(executer) {
        // 任务未执行,状态为pending,返回值为undefined
        this._state = PROMISE_STATE.PENDING;
        this._value = undefined;
        try {
            // 注意resolve()直接调用this指向window
            executer(this._resolve.bind(this), this._reject.bind(this));
        } catch (error) {
            this._reject(error);
        }
    }
    /**
     * 更改任务状态
     * @param {String} newState 新状态
     * @param {any} newValue 相关数据
     */
    _changeState(newState, newValue) {
        // 状态不为pending,不可修改
        if (this._state !== PROMISE_STATE.PENDING) return;
        this._state = newState;
        this._value = newValue;
    }
    /**
     * 标记当前任务完成
     * @param {any} data 任务完成关键数据
     */
    _resolve(data) {
        this._changeState(PROMISE_STATE.FULFILLED, data);
    }
    /**
     * 标记当前任务失败
     * @param {any} reason 任务失败的相关数据
     */
    _reject(reason) {
        this._changeState(PROMISE_STATE.REJECTED, reason);
    }
}

// 使用
new MyPromise((resolve, reject) => {
    resolve(1); // resolve是作函数直接调用的
});

这里是最简单最容易理解的基础部分,MyPromise注册的方法会立即执行,并尝试捕获方法执行过程是否发生错误,未发生错误,则等待方法调用回调_resolve_reject改变当前状态,若发生错误则自动调用_reject进入失败状态。(注意:_resolve 和 _reject在executor中是作为函数直接调用,需要绑定this

Promise的then函数

示例:

const pro1 = new Promise((resolve, reject) => {
    resolve('success');
});
pro1.then((data) => {
    console.log('data1:', data); // data1: success
}, (reason) => {
    console.log('reason1:', reason);
});
pro1.then((data) => {
    console.log('data2:', data); // data2: success
}, () => {
    console.log('reason2:', reason);
});

then函数接收两个函数,两函数会根据Promise进入settled时的状态成功/失败时执行;且对同一个任务,支持多次then函数去进行后续处理。

// Promise状态常量
const PROMISE_STATE = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected',
};
class MyPromise {
    /**
     * 接收一个任务执行器,初始化时立即执行
     * new MyPromise((resolve, reject) => {})
     */
    constructor(executor) {
        // 任务未执行,状态为pending,返回值为undefined
        this._state = PROMISE_STATE.PENDING;
        this._value = undefined;
+       this._handlers = []; // 处理函数形成的队列
        try {
            executor(this._resolve.bind(this), this._reject.bind(this));
        } catch (error) {
            this._reject(error);
        }
    }
+   /**
+    * 向处理队列中添加一个函数
+    * @param {Function} executor 添加的函数
+    * @param {String} state 什么状态去执行函数
+    * @param {Function} resolve 让then返回的Promise成功
+    * @param {Function} reject 让then返回的Promise失败
+    */
+   _pushHandler(executor, state, resolve, reject) {
+       this._handlers.push({ executor, state, resolve, reject });
+   }
+   /**
+    * 根据实际情况,执行队列
+    */
+   _runHanlers() {
+       if (this._state === PROMISE_STATE.PENDING) return;
+       // 执行一个,队列删除一个
+       while(this._handlers[0]) {
+           this._runOneHandler(this._handlers[0]);
+           this._handlers.shift();
+       }
+   }
+   /**
+    * 处理一个handler
+    */
+   _runOneHandler() {
+   }
+   /**
+    * 对任务执行后的后续处理
+    * @param {Function} onFulfilled 任务成功的后续处理
+    * @param {Function} onFulfilled 任务失败的后续处理
+    */
+   then(onFulfilled, onRejected) {
+       // 返回一个新的Promise
+       return new MyPromise((resolve, reject) => {
+           // 将then接收的函数添加到队列
+           this._pushHandler(onFulfilled, PROMISE_STATE.FULFILLED, resolve, reject);
+           this._pushHandler(onRejected, PROMISE_STATE.REJECTED, resolve, reject);
+           // 执行then接收的后续处理任务队列
+           this._runHanlers();
+       });
+   }
    /**
     * 更改任务状态
     * @param {String} newState 新状态
     * @param {any} newValue 相关数据
     */
    _changeState(newState, newValue) {
        if (this._state !== PROMISE_STATE.PENDING) return;
        this._state = newState;
        this._value = newValue;
        // 状态改变,就调用then注册的后续处理
        // 注意,正常情况是先执行任务(resolve、reject),后then()处理,则任务队列没有任务
+       this._runHanlers();
    }
    /**
     * 标记当前任务完成
     * @param {any} data 任务完成关键数据
     */
    _resolve(data) {
        this._changeState(PROMISE_STATE.FULFILLED, data);
    }
    /**
     * 标记当前任务失败
     * @param {any} reason 任务失败的相关数据
     */
    _reject(reason) {
        this._changeState(PROMISE_STATE.REJECTED, reason);
    }
}

// 使用
const pro1 = new MyPromise((resolve, reject) => {
    resolve(1);
});
pro1.then(function A1() {}, function A2() {});
pro1.then(function B1() {}, function B2() {});

创建一个then函数,用于接收我们注册的两个函数,将后续处理函数存入任务队列_hanlers中,同时存入的还有函数执行状态,和改变新Promise状态的回调函数。

如果new MyPromise((resolve) => { setTimeou(() => { resolve() }) }),说明调用then函数存放处理任务队列后pro1任务状态还是unsettled,则需要在_changeState中任务状态改变时_runHanlers()执行任务队列;如果new MyPromise((resolve) => { resolv() }),then函数存放处理任务队列后pro1的任务状态已经settled,则直接执行任务队列。(注意:pro1状态为pending时不能执行任务队列

Promise的核心代码

then在状态settled后,会执行我们注册的后续处理任务队列_hanlers,最终返回一个新的Promise,新Promise的状态受then函数中后续处理的结果的影响。

/**
 * 运行一个为队列任务
 * 
 */
+function runMicroTask(callBack) {
+   if (globalThis.process && globalThis.process.nextTick) {
+       process.nextTick(callBack);
+   } else if (MutationObserver) {
+       const p = document.createElement('p');
+       const observer = new MutationObserver(callBack);
+       observer.observe(p, {
+           childList: true,
+       });
+       p.innerHTML = '1';
+   } else {
+       setTimeout(callBack);
+   }
+}
/**
 * 判断是否是promise
 */
+function isPromise(obj) {
+   return !!(obj && typeof obj === 'object' && typeof obj.then === 'function');
+}
class MyPromise {
    ......
    /**
     * 处理一个handler
     */
+   _runOneHandler({ executor, state, resolve, reject }) {
+       // 利用结构,避免通过对象的方式调用函数,改变了this指向
+       runMicroTask(() => {
+           // 当前promise状态和函数需要执行的状态不一致,不处理
+           if (this._state !== state) return;
+           if (typeof executor !== 'function') {
+               // 状态一致,后续处理不是函数(没有后续处理)
+               // 透传上一个promise状态和任务数据
+               this._state === PROMISE_STATE.FULFILLED ? resolve(this._value) : reject(this._value);
+           }
+           // 尝试捕获错误
+           try {
+               const result = executor(this._value);
+               // 如果返回的本身就是Promisse,以返回的Promise状态为准
+               if (isPromise(result)) {
+                   result.then(resolve, reject);
+               } else {
+                   resolve(result);
+               }
+           } catch (error) {
+               reject(error);
+           }
+       });
+   }
    ......
}

创建一个runMicroTask方法,尝试模拟一个为微任务队列,将then函数注册的方法在微任务中执行,并根据上一个Promise的状态、then函数处理的结果、是否返回的新Promise和其状态规则,去判断then函数执行后返回的新promise的任务执行结果。(注意:仔细阅读上文代码何其注释去理解其实现原理

转载自:https://juejin.cn/post/7248606372953833528
评论
请登录