欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

这一次,彻底弄懂 Promise 原理

程序员文章站 2022-04-14 23:17:54
作者声明 本人将迁移至个人公众号「前端Q」及「掘金」平台写文章。博客园的文章将不再及时更新发布。欢迎大家关注公众号「前端Q」及我的掘金主页:https://juejin.im/user/5874526761ff4b006d4fd9a4/posts Promise 必须为以下三种状态之一:等待态(Pe ......

作者声明

本人将迁移至个人公众号「前端q」及「掘金」平台写文章。博客园的文章将不再及时更新发布。欢迎大家关注公众号「前端q」及:

 

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

基本过程:

  1. 初始化 promise 状态(pending)
  2. 执行 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
  3. 立即执行 promise 中传入的 fn 函数,将promise 内部 resolve、reject 函数作为参数传递给 fn ,按事件机制时机处理
  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 改一下:

    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 callback
p1 [{onfulfilled:c1(第一个then中的fn),resolve:p2resolve}]
p2 (p1 调用 then 时产生) [{onfulfilled:c2(第二个then中的fn),resolve:p3resolve}]
p3 (p2 调用 then 时产生) []
p4 (执行c1中产生[调用 test ]) [{onfulfilled:p2resolve,resolve:p5resolve}]
p5 (调用p2resolve 时,进入 then.call 逻辑中产生) []

有了这个表格,我们就可以清晰知道各个实例中 callback 执行的顺序是:

c1 -> p2resolve -> c2 -> p3resolve -> [] -> 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)
        }
        ...
    }

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,onerror)
        }
        ...
    }

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 方法如下:

 function promise(fn){ 
        ...
        this.resolve = function (value){
            if (value && value instanceof promise) {
                return value;
            } else if (value && typeof value === 'object' && typeof value.then === 'function'){
                let then = value.then;
                return new promise(resolve => {
                    then(resolve);
                });
            } else if (value) {
                return new promise(resolve => resolve(value));
            } else {
                return new promise(resolve => resolve());
            }
        }
        ...
    }

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

function promise(fn){ 
        ...
        this.reject = function (value){
            return new promise(function(resolve, reject) {
                reject(value);
            });
        }
        ...
    }

promise.all

入参是一个 promise 的实例数组,然后注册一个 then 方法,然后是数组中的 promise 实例的状态都转为 fulfilled 之后则执行 then 方法。这里主要就是一个计数逻辑,每当一个 promise 的状态变为 fulfilled 之后就保存该实例返回的数据,然后将计数减一,当计数器变为 0 时,代表数组中所有 promise 实例都执行完毕。

    function promise(fn){ 
        ...
        this.all = function (arr){
            var args = array.prototype.slice.call(arr);
            return new promise(function(resolve, reject) {
                if(args.length === 0) return resolve([]);
                var remaining = args.length;

                function res(i, val) {
                    try {
                        if(val && (typeof val === 'object' || typeof val === 'function')) {
                            var then = val.then;
                            if(typeof then === 'function') {
                                then.call(val, function(val) {
                                    res(i, val);
                                }, reject);
                                return;
                            }
                        }
                        args[i] = val;
                        if(--remaining === 0) {
                            resolve(args);
                        }
                    } catch(ex) {
                        reject(ex);
                    }
                }
                for(var i = 0; i < args.length; i++) {
                    res(i, args[i]);
                }
            });
        }
        ...
    }

promise.race

有了 promise.all 的理解,promise.race 理解起来就更容易了。它的入参也是一个 promise 实例数组,然后其 then 注册的回调方法是数组中的某一个 promise 的状态变为 fulfilled 的时候就执行。因为 promise 的状态只能改变一次,那么我们只需要把 promise.race 中产生的 promise 对象的 resolve 方法,注入到数组中的每一个 promise 实例中的回调函数中即可。

function promise(fn){ 
    ...
    this.race = function(values) {
        return new promise(function(resolve, reject) {
            for(var i = 0, len = values.length; i < len; i++) {
                values[i].then(resolve, reject);
            }
        });
    }
    ...
    }  

总结

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

参考资料

完整 promise 模型

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,
      })
    })
  }

  this.catch = function (onerror) {
    this.then(null, onerror)
  }

  this.finally = function (ondone) {
    this.then(ondone, onerror)
  }

  this.resolve = function (value) {
    if (value && value instanceof promise) {
      return value
    } if (value && typeof value === 'object' && typeof value.then === 'function') {
      const { then } = value
      return new promise((resolve) => {
        then(resolve)
      })
    } if (value) {
      return new promise(resolve => resolve(value))
    }
    return new promise(resolve => resolve())
  }

  this.reject = function (value) {
    return new promise(((resolve, reject) => {
      reject(value)
    }))
  }

  this.all = function (arr) {
    const args = array.prototype.slice.call(arr)
    return new promise(((resolve, reject) => {
      if (args.length === 0) return resolve([])
      let remaining = args.length

      function res(i, val) {
        try {
          if (val && (typeof val === 'object' || typeof val === 'function')) {
            const { then } = val
            if (typeof then === 'function') {
              then.call(val, (val) => {
                res(i, val)
              }, reject)
              return
            }
          }
          args[i] = val
          if (--remaining === 0) {
            resolve(args)
          }
        } catch (ex) {
          reject(ex)
        }
      }
      for (let i = 0; i < args.length; i++) {
        res(i, args[i])
      }
    }))
  }

  this.race = function (values) {
    return new promise(((resolve, reject) => {
      for (let i = 0, len = values.length; i < len; i++) {
        values[i].then(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
    }
    try {
      const ret = cb(value)
      next(ret)
    } catch (e) {
      callback.reject(e)
    }
  }
  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)
}

最后

觉得内容有帮助可以关注下:

觉得内容有帮助可以关注下我的公众号 「前端q」,一起学习成长~~

这一次,彻底弄懂 Promise 原理