文章通过实现一个简易的类来理解Promise原理,并将Promise和传统回调对比,展示了Promise在异步编程中的优势,最后是介绍了Promise在实际开发中的应用。
一. 概念
Promise将异步操作和回调进行解耦,并通过执行状态将两者关联。异步操作结束后把状态通知Promise,由Promise负责触发回调函数。
二. Promise原理
1. 状态变更
输入:
let p0 = new Promise((resolve, reject) => {})
console.log('p0', p0)
let p1 = new Promise((resolve, reject) => {
resolve('成功')
})
console.log('p1', p1)
let p2 = new Promise((resolve, reject) => {
reject('失败')
})
console.log('p2', p2)
let p3 = new Promise((resolve, reject) => {
throw('报错')
})
console.log('p3', p3)
输出:
结论:
- 刚创建的Promise对象处于pending状态。
- 当异步操作完成后,调用resolve/reject变更状态为fulfilled/rejected。
- 执行函数发生异常(同步异常)时,变更状态为rejected。
实现:
class MyPromise {
constructor(executor) {
this.initValue();
// 由于resolve/reject是外部函数executor调用的,所以必须将this硬绑定为当前MyPromise对象
this.initBind();
try {
// 执行传进来的函数
executor(this.resolve, this.reject);
} catch (e) {
// 捕捉到错误直接执行reject
this.reject(e);
}
}
initBind() {
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
}
initValue() {
this.PromiseResult = null;
this.PromiseState = 'pending';
}
resolve(value) {
// 状态只能由pending转换为fulfilled/rejected
if (this.PromiseState == 'pending'){
this.PromiseState = 'fulfilled';
this.PromiseResult = value;
}
}
reject(reason) {
// 状态只能由pending转换为fulfilled/rejected
if (this.PromiseState !== 'pending'){
this.PromiseState = 'rejected';
this.PromiseResult = reason;
}
}
}
2. 执行回调
输入/输出:
// 立刻输出 ”resolve=成功“
const p1 = new Promise((resolve, reject) => {
resolve('成功');
}).then(res => console.log('resolve=',res), err => console.log('reject=',err))
// 1秒后输出 ”reject=失败“
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败');
}, 1000)
}).then(res => console.log('resolve=',res), err => console.log('reject=',err))
结论:
- then接收两个参数:
成功回调和失败回调 - 当Promise状态为
fulfilled执行成功回调,为rejected执行失败回调 - 通过多次调用then可以
注册多个回调函数,注意区分链式调用
实现:
通过then注册回调时,如果Promise状态为fulfilled/rejected,则执行回调函数;
如果Promise状态为pending,则先保存回调函数,等异步操作结束后再执行回调。
initValue() {
...
// 保存回调函数。
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
}
resolve(value) {
...
// 状态变更为fulfilled,执行保存的成功回调
while (this.onFulfilledCallbacks.length) {
this.onFulfilledCallbacks.shift()(this.PromiseResult);
}
}
reject(reason) {
...
// 状态变更为rejected,执行保存的失败回调
while (this.onRejectedCallbacks.length) {
this.onRejectedCallbacks.shift()(this.PromiseResult);
}
}
then(onFulfilled, onRejected) {
// 参数校验,确保一定是函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
if (this.PromiseState === 'fulfilled') {
// 执行fulfilled回调
onFulfilled(this.PromiseResult);
} else if (this.PromiseState === 'rejected') {
// 执行rejected回调
onRejected(this.PromiseResult);
}else if (this.PromiseState === 'pending') {
// Promise为pending状态,暂时保存两个回调
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
}
3. 链式调用
输入/输出
// 链式调用 输出 200
const p3 = new Promise((resolve, reject) => {
resolve(100);
}).then(res => 2 * res)
.then(res => console.log(res))
// 链式调用 输出300
const p4 = new Promise((resolve, reject) => {
resolve(100);
}).then(res => new Promise((resolve, reject) => resolve(3 * res)))
.then(res => console.log(res))
结论
-
then方法本身会返回一个新Promise对象。
-
如果回调函数返回值是Promise对象,则新Promise对象状态由该Promise对象决定。
-
如果回调函数返回值不是Promise对象,则新Promise对象状态为成功。
实现
then(onFulfilled, onRejected) {
...
var thenPromise = new MyPromise((resolve, reject) => {
const resolvePromise = cb => {
try {
const x = cb(this.PromiseResult)
if(x === thenPromise){
// 自己等待自己完成,循环等待:在then回调中返回了then的返回值。
reject(new TypeError('Chaining cycle detected for promise'));
}
if (x instanceof MyPromise) {
// 如果返回值是Promise对象,则新Promise状态由该Promise决定。
x.then(resolve, reject);
} else {
// 非Promise就直接成功
resolve(x);
}
} catch (err) {
// 处理报错
reject(err);
}
}
if (this.PromiseState === 'fulfilled') {
// 如果当前为成功状态,执行第一个回调
resolvePromise(onFulfilled);
} else if (this.PromiseState === 'rejected') {
// 如果当前为失败状态,执行第二个回调
resolvePromise(onRejected);
} else if (this.PromiseState === 'pending') {
// 如果状态为待定状态,暂时保存两个回调
this.onFulfilledCallback = resolvePromise(onFulfilled);
this.onRejectedCallback = resolvePromise(onRejected);
}
})
// 返回这个包装的Promise
return thenPromise;
}
4. 调用时序
输入输出
setTimeout(()=>{console.log(0)},0);
const p = new Promise((resolve, reject) => {
console.log(1);
resolve()
}).then(() => console.log(2))
console.log(3)
// 输出顺序是 1 3 2 0
结论
- 即使Promise的状态被立刻更新为fulfilled,回调函数也不会被立刻执行。
- 回调函数是在事件循环的微任务中执行的。
- 因为回调本来就是异步的,放在微任务执行可以让后面的同步代码尽快被执行。
实现
const resolvePromise = cb => {
setTimeout(() => {
// 执行回调...
})
}
三. Promise VS 传统回调
1. 回调地狱
面对执行多重异步操作的场景,传统回调需要将回调函数作为参数传入,存在“回调地狱”问题。而Promise将异步操作和回调函数解耦了,不需要在一开始就传入回调函数。
// 传统回调 实现多重异步操作
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
// Promise 实现多重异步操作
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
2. 异常捕获
注:这里的异常是指 回调函数异常,而不是异步操作异常。
// 下面是一个典型的超时回调,由于timeoutCallback是在异步操作中被调用的,所以try catch无法捕获异步异常。
try{
setTimeout(()=>timeoutCallback("3 seconds passed"), 3000);
}catch(err){
// 这里是无法捕获到 timeoutCallback异常的。
console.log(err);
}
// Promise负责调用回调函数。当异步操作结束Promise状态变更后,Promise会在调用回调函数时加上try catch。
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(3000).then(() => timeoutCallback("3 seconds passed")).catch(err=>{console.log(err)});
四. Promise应用
1. 创建已完成的Promise
Promise.resolve() 和 Promise.reject()
2. 封装传统回调
由于传统回调存在 回调地狱和缺乏异步异常捕获等问题,所以使用Promise对已存在的传统回调进行封装。
// 下面是一个典型的超时回调,由于timeoutCallback是异步执行的,所以无法捕获异步回调中的异常。
try{
setTimeout(()=>timeoutCallback("3 seconds passed"), 3000);
}catch(err){
// 这里是无法捕获到 timeoutCallback异常的。
console.log(err);
}
// Promise可以将异步操作和回调分开,当异步操作完成后调用回调时,会自动在回调函数加上try catch。
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(3000).then(() => timeoutCallback("3 seconds passed")).catch(err=>{console.log(err)});
3. 执行多个异步操作
// 并行多个异步操作,然后等所有操作完成后进入下一步操作
Promise.all([promise1, promise2, promise3])
.then(([result1, result2, result3]) => {
// next step
});
.catch(err){
// 当任意一个操作reject时,进入catch,并返回对应reject信息。
}
// 按顺序执行多个异步操作(链式调用)
[promise1, promise2, promise3].reduce((pre, cur) => pre.then(()=>{return cur}), Promise.resolve())
.then(result3 => {
/* use result3 */
}).catch(err=>{
// 当任意一个操作reject时,进入catch,并返回对应reject信息。
});
// 在es2017中,也可以使用async/await对上面代码进行优化
let result;
try{
for (const pro of [promise1, promise1, promise1]) {
result = await pro(result);
}
}catch(err){
result = err;
}