Promise
简介:Promise是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一。套规范最早诞生于前端社区,规范名称为Promise A+
导读:针对Promise本文注重使用上细节,以及最后通过分析手写实现Promise。
Promise基础知识
前端中所有的异步场景,都可以看作是一个异步任务,JS中的表现为一个对象,该对象称之为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函数
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.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推出了两个关键字async
和await
,用于更加优雅的表达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的任务执行结果。(注意:仔细阅读上文代码何其注释去理解其实现原理)