这一次,彻底弄懂 Promise 原理

Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。

基本过程:

  1. 初始化 Promise 状态(pending)
  2. 立即执行 Promise 中传入的 fn 函数,将Promise 内部 resolve、reject 函数作为参数传递给 fn ,按事件机制时机处理
  3. 执行 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
  4. Promise里的关键是要保证,then方法传入的参数 onFulfilled 和 onRejected,必须在then方法被调用的那一轮事件循环之后的新执行栈中执行。

真正的链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise.

链式调用

先从 Promise 执行结果看一下,有如下一段代码:

    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
            resolve({ test: 2 })
            reject({ test: 2 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
    },(data1)=>{
        console.log('result2',data1)
    }).then((data) => {
        console.log('result3', data)
    })
    //result1 { test: 1 }
    //result3 undefined

显然这里输出了不同的 data。由此可以看出几点:

  1. 可进行链式调用,且每次 then 返回了新的 Promise(2次打印结果不一致,如果是同一个实例,打印结果应该一致。
  2. 只输出第一次 resolve 的内容,reject 的内容没有输出,即 Promise 是有状态且状态只可以由pending -> fulfilled或 pending-> rejected,是不可逆的。
  3. then 中返回了新的 Promise,但是then中注册的回调仍然是属于上一个 Promise 的。

基于以上几点,我们先写个基于 PromiseA+ 规范的只含 resolve 方法的 Promise 模型:

function Promise(fn){ 
        let state = 'pending';
        let value = null;
        const callbacks = [];

        this.then = function (onFulfilled){
            return new Promise((resolve, reject)=>{
                handle({ //桥梁,将新 Promise 的 resolve 方法,放到前一个 promise 的回调对象中
                    onFulfilled, 
                    resolve
                })
            })
        }

        function handle(callback){
            if(state === 'pending'){
                callbacks.push(callback)
                return;
            }
            
            if(state === 'fulfilled'){
                if(!callback.onFulfilled){
                    callback.resolve(value)
                    return;
                }
                const ret = callback.onFulfilled(value) //处理回调
                callback.resolve(ret) //处理下一个 promise 的resolve
            }
        }
        function resolve(newValue){
            const fn = ()=>{
                if(state !== 'pending')return

                state = 'fulfilled';
                value = newValue
                handelCb()
            }
            
            setTimeout(fn,0) //基于 PromiseA+ 规范
        }
        
        function handelCb(){
            while(callbacks.length) {
                const fulfiledFn = callbacks.shift();
                handle(fulfiledFn);
            };
        }
        
        fn(resolve)
    }

这个模型简单易懂,这里最关键的点就是在 then 中新创建的 Promise,它的状态变为 fulfilled 的节点是在上一个 Promise的回调执行完毕的时候。也就是说当一个 Promise 的状态被 fulfilled 之后,会执行其回调函数,而回调函数返回的结果会被当作 value,返回给下一个 Promise(也就是then 中产生的 Promise),同时下一个 Promise的状态也会被改变(执行 resolve 或 reject),然后再去执行其回调,以此类推下去…链式调用的效应就出来了。

但是如果仅仅是例子中的情况,我们可以这样写:

    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
        //dosomething
        console.log('result3')
    })
    //result1 { test: 1 }
    //result3

实际上,我们常用的链式调用,是用在异步回调中,以解决”回调地狱”的问题。如下例子:

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve({ test: 1 })
  }, 1000)
}).then((data) => {
  console.log('result1', data)
  //dosomething
  return test()
}).then((data) => {
  console.log('result2', data)
})

function test(id) {
  return new Promise(((resolve) => {
    setTimeout(() => {
      resolve({ test: 2 })
    }, 5000)
  }))
}
//基于第一个 Promise 模型,执行后的输出
//result1 { test: 1 }
//result2 Promise {then: ƒ}

用上面的 Promise 模型,得到的结果显然不是我们想要的。认真看上面的模型,执行 callback.resolve 时,传入的参数是 callback.onFulfilled 执行完成的返回,显然这个测试例子返回的就是一个 Promise,而我们的 Promise 模型中的 resolve 方法并没有特殊处理。那么我们将 resolve 改一下:

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve({ test: 1 })
  }, 1000)
}).then((data) => {
  console.log('result1', data)
  //dosomething
  return test()
}).then((data) => {
  console.log('result2', data)
})

function test(id) {
  return new Promise(((resolve) => {
    setTimeout(() => {
      resolve({ test: 2 })
    }, 5000)
  }))
}
//基于第一个 Promise 模型,执行后的输出
//result1 { test: 1 }
//result2 Promise {then: ƒ}

用上面的 Promise 模型,得到的结果显然不是我们想要的。认真看上面的模型,执行 callback.resolve 时,传入的参数是 callback.onFulfilled 执行完成的返回,显然这个测试例子返回的就是一个 Promise,而我们的 Promise 模型中的 resolve 方法并没有特殊处理。那么我们将 resolve 改一下:

    function Promise(fn){ 
        ...
        function resolve(newValue){
            const fn = ()=>{
                if(state !== 'pending')return

                if(newValue && (typeof newValue === 'object' || typeof newValue === 'function')){
                    const {then} = newValue
                    if(typeof then === 'function'){
                        // newValue 为新产生的 Promise,此时resolve为上个 promise 的resolve
                        //相当于调用了新产生 Promise 的then方法,注入了上个 promise 的resolve 为其回调
                        then.call(newValue,resolve)
                        return
                    }
                }
                state = 'fulfilled';
                value = newValue
                handelCb()
            }
            
            setTimeout(fn,0)
        }
        ...
    }

用这个模型,再测试我们的例子,就得到了正确的结果:

   new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
        //dosomething
        return test()
    }).then((data) => {
        console.log('result2', data)
    })

    function test(id) {
        return new Promise(((resolve, reject) => {
            setTimeout(() => {
            resolve({ test: 2 })
            }, 5000)
        }))
    }
    //result1 { test: 1 }
    //result2 { test: 2 }

显然,新增的逻辑就是针对 resolve 入参为 Promise 的时候的处理。我们观察一下 test 里面创建的 Promise,它是没有调用 then方法的。从上面的分析我们已经知道 Promise 的回调函数就是通过调用其 then 方法注册的,因此 test 里面创建的 Promise 其回调函数为空。

显然如果没有回调函数,执行 resolve 的时候,是没办法链式下去的。因此,我们需要主动为其注入回调函数。

我们只要把第一个 then 中产生的 Promise 的 resolve 函数的执行,延迟到 test 里面的 Promise 的状态为 onFulfilled 的时候再执行,那么链式就可以继续了。所以,当 resolve 入参为 Promise 的时候,调用其 then 方法为其注入回调函数,而注入的是前一个 Promise 的 resolve 方法,所以要用 call 来绑定 this 的指向。

基于新的 Promise 模型,上面的执行过程产生的 Promise 实例及其回调函数,可以用看下表:

阶段Promise回调函数操作备注
P1初始 Promisenew Promise() 创建resolve({ test: 1 }) 会在 1 秒后被触发
c1P1c1执行第一个 then()打印 result1 { test: 1 } ,调用 test()
P2创建 P2创建新的 Promise (P2)resolve 回调触发时会调用 c1,并返回 test()
P3创建 P3返回新的 Promise (test())test() 返回的 Promise 在 5 秒后 resolve({ test: 2 })
p2resolveP2p2resolve触发 P3resolve 处理触发 P3 的 Promise 完成
c2P3c2执行第二个 then()打印 result2 { test: 2 }
p3resolveP3p3resolve触发第二个 then() 完成resolve({ test: 2 }) 完成
p5resolveP4 (隐式)p5resolve链式回调结束结束回调链

解释:

  • P1: 初始的 Promise,创建时会执行传入的 fn,并在 1 秒后 resolve({ test: 1 })
  • c1: 第一个 .then() 的回调,在 P1 的 resolve 被触发后执行,调用了 test()
  • P2: c1 执行时,返回一个新的 Promise (P2),该 Promise 会在 test() 完成后继续执行回调。
  • P3: test() 返回的 Promise,在 5 秒后完成并触发 resolve({ test: 2 })
  • p2resolve: P2resolve 触发时,将 test() 的 Promise (P3) 完成后的结果传递给 P3
  • c2: 第二个 .then() 的回调,在 P3 完成时触发,打印 result2 { test: 2 }
  • p3resolve: 触发 P3 的回调(c2),完成第二个 .then()
  • p5resolve: 作为链式回调,p5resolve 会在链条结束时完成,但在这个代码中没有显式地定义。

以上就是链式调用的原理了。

reject

下面我们再来补全 reject 的逻辑。只需要在注册回调、状态改变时加上 reject 的逻辑即可。

完整代码如下:

    function Promise(fn){ 
        let state = 'pending';
        let value = null;
        const callbacks = [];

        this.then = function (onFulfilled,onRejected){
            return new Promise((resolve, reject)=>{
                handle({
                    onFulfilled, 
                    onRejected,
                    resolve, 
                    reject
                })
            })
        }

        function handle(callback){
            if(state === 'pending'){
                callbacks.push(callback)
                return;
            }
            
            const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected;
            const next = state === 'fulfilled'? callback.resolve:callback.reject;

            if(!cb){
                next(value)
                return;
            }
            const ret = cb(value)
            next(ret)
        }
        function resolve(newValue){
            const fn = ()=>{
                if(state !== 'pending')return

                if(newValue && (typeof newValue === 'object' || typeof newValue === 'function')){
                    const {then} = newValue
                    if(typeof then === 'function'){
                        // newValue 为新产生的 Promise,此时resolve为上个 promise 的resolve
                        //相当于调用了新产生 Promise 的then方法,注入了上个 promise 的resolve 为其回调
                        then.call(newValue,resolve, reject)
                        return
                    }
                }
                state = 'fulfilled';
                value = newValue
                handelCb()
            }
            
            setTimeout(fn,0)
        }
        function reject(error){

            const fn = ()=>{
                if(state !== 'pending')return

                if(error && (typeof error === 'object' || typeof error === 'function')){
                    const {then} = error
                    if(typeof then === 'function'){
                        then.call(error,resolve, reject)
                        return
                    }
                }
                state = 'rejected';
                value = error
                handelCb()
            }
            setTimeout(fn,0)
        }
        function handelCb(){
            while(callbacks.length) {
                const fn = callbacks.shift();
                handle(fn);
            };
        }
        fn(resolve, reject)
    }

异常处理

异常通常是指在执行成功/失败回调时代码出错产生的错误,对于这类异常,我们使用 try-catch 来捕获错误,并将 Promise 设为 rejected 状态即可。

handle代码改造如下:

    function handle(callback){
        if(state === 'pending'){
            callbacks.push(callback)
            return;
        }
        
        const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected;
        const next = state === 'fulfilled'? callback.resolve:callback.reject;

        if(!cb){
            next(value)
            return;
        }
        try {
            const ret = cb(value)
            next(ret)
        } catch (e) {
            callback.reject(e);
        }  
    }

我们实际使用时,常习惯注册 catch 方法来处理错误,例:

    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
        //dosomething
        return test()
    }).catch((ex) => {
        console.log('error', ex)
    })

实际上,错误也好,异常也罢,最终都是通过reject实现的。也就是说可以通过 then 中的错误回调来处理。所以我们可以增加这样的一个 catch 方法:

    function Promise(fn){ 
        ...
        this.then = function (onFulfilled,onRejected){
            return new Promise((resolve, reject)=>{
                handle({
                    onFulfilled, 
                    onRejected,
                    resolve, 
                    reject
                })
            })
        }
        this.catch = function (onError){
            this.then(null,onError)
        }
        ...
    }

简易的的Promise实现(错误传播):

function Promise(fn) {
    this.state = 'pending';  // 状态:'pending', 'fulfilled', 'rejected'
    this.value = null;       // 存储最终的值或错误
    this.callbacks = [];     // 存储回调

    // 成功回调
    const resolve = (value) => {
        if (this.state === 'pending') {
            this.state = 'fulfilled';
            this.value = value;
            this.callbacks.forEach(callback => callback());  // 执行所有回调
        }
    };

    // 失败回调
    const reject = (reason) => {
        if (this.state === 'pending') {
            this.state = 'rejected';
            this.value = reason;
            this.callbacks.forEach(callback => callback());  // 执行所有回调
        }
    };

    // then 方法:处理成功和失败的回调
    this.then = function(onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            const callback = () => {
                if (this.state === 'fulfilled') {
                    if (onFulfilled) {
                        try {
                            resolve(onFulfilled(this.value)); // 传递值
                        } catch (e) {
                            reject(e); // 发生错误时传递给 reject
                        }
                    } else {
                        resolve(this.value); // 没有 onFulfilled 直接传递
                    }
                } else if (this.state === 'rejected') {
                    if (onRejected) {
                        try {
                            resolve(onRejected(this.value)); // 传递错误
                        } catch (e) {
                            reject(e); // 发生错误时传递给 reject
                        }
                    } else {
                        reject(this.value); // 没有 onRejected 直接传递
                    }
                }
            };

            if (this.state === 'pending') {
                this.callbacks.push(callback); // 状态为 pending 时,保存回调
            } else {
                callback(); // 状态已变为 fulfilled 或 rejected,直接执行
            }
        });
    };

    // catch 方法:处理错误
    this.catch = function(onRejected) {
        return this.then(null, onRejected); // 将 catch 转换为 then 的第二个回调
    };

    // 执行构造器传入的 fn
    fn(resolve, reject);
}

new Promise((resolve, reject) => {
  resolve('Start');
})
  .then((data) => {
    console.log('First then:', data);  // 输出 "Start"
    throw new Error('Error in first then');  // 抛出错误
  })
  .then((data) => {
    // 这个 .then() 不会执行
    console.log('Second then:', data);
  })
  .catch((error) => {
    // 捕获第一个 then 中抛出的错误
    console.log('Caught error:', error.message);  // 输出 "Caught error: Error in first then"
  });

Finally方法

在实际应用的时候,我们很容易会碰到这样的场景,不管Promise最后的状态如何,都要执行一些最后的操作。我们把这些操作放到 finally 中,也就是说 finally 注册的函数是与 Promise 的状态无关的,不依赖 Promise 的执行结果。所以我们可以这样写 finally 的逻辑:

    function Promise(fn){ 
        ...
        this.catch = function (onError){
            this.then(null,onError)
        }
        this.finally = function (onDone){
            this.then(onDone,onDone)
        }
        ...
    }

resolve 方法和 reject 方法

实际应用中,我们可以使用 Promise.resolve 和 Promise.reject 方法,用于将于将非 Promise 实例包装为 Promise 实例。如下例子:

Promise.resolve({name:'winty'})
Promise.reject({name:'winty'})
// 等价于
new Promise(resolve => resolve({name:'winty'}))
new Promise((resolve,reject) => reject({name:'winty'}))

这些情况下,Promise.resolve 的入参可能有以下几种情况:

  • 无参数 [直接返回一个resolved状态的 Promise 对象]
  • 普通数据对象 [直接返回一个resolved状态的 Promise 对象]
  • 一个Promise实例 [直接返回当前实例]
  • 一个thenable对象(thenable对象指的是具有then方法的对象) [转为 Promise 对象,并立即执行thenable对象的then方法。]

基于以上几点,我们可以实现一个 Promise.resolve 方法如下:

// 静态方法:Promise.resolve()
Promise.resolve = function(value) {
    return new Promise((resolve, reject) => {
        // 如果 value 是一个 Promise,直接返回它
        if (value instanceof Promise) {
            value.then(resolve, reject);
        } else if (value && typeof value === 'object' && typeof value.then === 'function') {
            // 如果 value 是一个 thenable,等待它执行
            value.then(resolve, reject);
        } else {
            // 如果是其他值,创建一个已 resolved 状态的 Promise
            resolve(value);
        }
    });
};

Promise.reject与Promise.resolve类似,区别在于Promise.reject始终返回一个状态的rejected的Promise实例,而Promise.resolve的参数如果是一个Promise实例的话,返回的是参数对应的Promise实例,所以状态不一 定。 因此,reject 的实现就简单多了,如下:

// 静态方法:Promise.reject()
Promise.reject = function(reason) {
    return new Promise((resolve, reject) => {
        reject(reason);  // 创建一个 rejected 状态的 Promise
    });
};

Promise.all

Promise.all():接收一个包含多个 Promise 的数组,返回一个新的 Promise,当所有 Promise 都完成时,新的 Promise 才会完成,并返回所有 Promise 的结果。如果其中任何一个 Promise 被拒绝,Promise.all() 立即返回 rejected 状态,并携带该错误。

Promise.all()

  • 等待所有的 Promise 都完成,如果有一个 Promise 被拒绝,则返回的 Promise 会立即变为 rejected,并返回该错误。
// 静态方法:Promise.all()
Promise.all = function(promises) {
    return new Promise((resolve, reject) => {
        let results = [];
        let completed = 0;
        const total = promises.length;

        // 处理每个 Promise
        promises.forEach((promise, index) => {
            promise.then(value => {
                results[index] = value;
                completed++;
                if (completed === total) {
                    resolve(results); // 当所有 Promise 都解决时,返回结果数组
                }
            }).catch(reason => {
                reject(reason); // 如果有一个 Promise 被拒绝,直接拒绝
            });
        });
    });
};

我们遍历传入的 promises 数组,并为每个 Promise 调用 .then(),当所有 Promise 都完成时,返回一个新的 Promise,并携带所有 Promise 的结果。

如果其中任何一个 Promise 被拒绝,返回的 Promise 会立即进入 rejected 状态,并携带该错误。

Promise.race

Promise.race():接收一个包含多个 Promise 的数组,返回一个新的 Promise,这个 Promise 会在第一个 Promise 完成时(无论是 fulfilled 还是 rejected)返回,并且会携带该 Promise 的结果或错误。

Promise.race()

  • 返回的 Promise 会在第一个 Promise 完成时(无论是 fulfilled 还是 rejected)变为相同的状态,并返回第一个完成的 Promise 的结果。
// 静态方法:Promise.race()
Promise.race = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach(promise => {
            promise.then(resolve).catch(reject); // 返回第一个完成的 Promise
        });
    });
};

我们遍历传入的 promises 数组,并为每个 Promise 调用 .then().catch()

只要第一个 Promise 完成(无论是 fulfilled 还是 rejected),返回的 Promise 就会进入相同的状态,并携带该 Promise 的结果或错误。

总结

Promise 源码不过几百行,我们可以从执行结果出发,分析每一步的执行过程,然后思考其作用即可。其中最关键的点就是要理解 then 函数是负责注册回调的,真正的执行是在 Promise 的状态被改变之后。而当 resolve 的入参是一个 Promise 时,要想链式调用起来,就必须调用其 then 方法(then.call),将上一个 Promise 的 resolve 方法注入其回调数组中。

补充说明

虽然 then 普遍认为是微任务。但是浏览器没办法模拟微任务,目前要么用 setImmediate ,这个也是宏任务,且不兼容的情况下还是用 setTimeout 打底的。还有,promise 的 polyfill (es6-promise) 里用的也是 setTimeout。因此这里就直接用 setTimeout,以宏任务来代替微任务了。

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注