Promise/A+规范以及实现

Js Front End

Promise 实现原理

源码

Promise基本用法

new Promise(function(resolve, reject) {
    resolve()
}).then(function(val) {
    return val
}, function(error) {
    catch(error)
}).catch(function(error) {
    catch(error)
})

Promise对象基本方法是then, 而catchthen的一个变形, 相当于then(undefined, onReject)

实现过程

根据Promise用法, 我们初步想到需要实现的方法是

  • 构造函数
  • resolve函数
  • reject函数
  • then函数

此时Promise原型应为

const PENDING = 'PENDING'
const RESOLVED = 'RESOLVED'
const REJECT = 'REJECT'

class Promise {
  constructor(func) {}
  resolve() {}
  reject() {}
  then(onReslove, onReject) {}
}

根据Promise/A+规范(以下简称规范)中所说的

  • Promise有三个状态 PENDING, RESOLVED, REJECTED
  • 状态只会从PENDING转换到RESOLVED或者REJECTED其中一个, 并且之后不会再改变
  • 当Promise处于执行态时, 会有一个终值, 并且该值不会再改变
  • 当Promise处于拒绝态时, 会有一个据因, 并且该据因不会再改变
  • 当Promise由PENDING转换为RESOLVED时, 会触发onResolve回调, 并且只执行一次
  • 当Promise由PENDING转换为REJECTED时, 会触发onReject回调, 并且只执行一次
  • Promise状态的转换时机在于开发者何时调用promise的resolve或者reject函数
class Promise {
  constructor(func) {
    this.value = null // 终值或者据因
    this.status = PENDING // 状态
    this.onResolveCallBack = [] // resolved 回调
    this.onRejectCallBack = [] // rejected 回调
    try {
      func(this.resolve.bind(this), this.reject.bind(this))
    } catch (e) {
      this.reject(e)
    }
  }
  resolve(val) {
    if (this.status === PENDING) {
      this.value = val // 设置终值
      this.status = RESOLVED // 设置状态
      this.onResolveCallBack.forEach(each => {
        each(val) // 执行回调
      })
    }
  }
  reject(reason) {
    if (this.status === PENDING) {
      this.value = reason // 设置据因
      this.status = REJECT // 设置状态
      this.onRejectCallBack.forEach(each => {
        each(reason) // 执行回调
      })
    }
  }
  then(onReslove, onReject) {}
}

这里可能有人会说Promise应该是一个异步的过程, 在上面代码中并没有看到任何的异步. 比如说: setTimeout。

解答:

其实当创建一个Promise实例的时候,整个过程是同步的。

也就是说

const ins = new Promise(function(res, rej) {
  res(10)
})
console.log(ins)
console.log('after ins')

// 输出
// Promise {<resolved>: 10}
// after ins

当你执行完这一句, ins的状态会马上变成RESOLVED. 说明在构造方法中并没有执行异步操作。如果真的需要异步的话,则需要主动在调用res前,加上setTimeout来触发异步。

const ins = new Promise(function(res, rej) {
  setTimeout(() => {
    res(10)
  })
})
console.log(ins)
console.log('after ins')

// 输出
// Promise {<pending>}
// after ins

还有一个then方法没有完成. 先看下规范怎么说

  • 一个promise必须提供一个then方法以访问当前值, 终止和据因
  • then接受两个参数then(onResolve, onReject)
  • onResolve和onReject都是可选, 如果不是函数则被忽略
  • onResolve方法在promise执行结束后被调用, 其第一个参数为promise的终值, 被调用次数不超过一次
  • onReject方法在promise被拒绝后被调用, 其第一个参数为promise的据因, 同样被调用次数不超过一次
  • onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用
  • 如果onResolve和onReject返回一个值x, 则执行 Promise解决过程
  • then方法必须返回一个promise对象

简单说就是

  • 如果promise处于pending, 则将then回调放入promise的回调列表中
  • 如果promise处于resolved, 则实行then方法中的onResolve
  • 如果promise处于rejected, 则执行then方法中的onReject
  • then方法要确保onResolve和onReject异步执行
  • onResolve和onReject返回的值都将用来解决下一个promise(后面再讲解)
  • 返回新的promise(注意: 一定是新的promise)
class Promise() {
    // ...
    then(onResolve, onReject){
        const self = this
        return new Promise(function(nextResolve, nextReject) {
            if(self.status === PENDING) {
                // 加入到任务队列
                self.onResolveCallback.push(onResolve)
                self.onRejectCallback.push(onReject)
            } else if(self.status === RESOLVED) {
                // 异步执行
                setTimeout(onResolve, 0, self.value)
            } else {
                // 异步执行
                setTimeout(onReject, 0, self.value)
            }
        })
    }
}

此时Promise已经可以完成异步操作. 但是Promise还有一个关键特点是可以链式调用. 目前是还没有实现链式调用这一步. 具体代码看promise2.js

接下来继续看下规范怎么说

Promise 解决过程

  • blablabla 这里比较长

简单说就是

xthen方法中onResolve或者onReject中返回的值, promise2then方法返回的新promise.

promise的解决过程是一个抽象步骤. 需要输入一个promise和一个. 表示为[[Resolve]](promise, x)

  • 如果xpromise2相等, 则以TypeError为据因拒绝执行promise2
  • 如果xPromise实例, 则让promise2接受x的状态
  • 如果xthenable对象, 则调用其then方法
  • 如果都不满足, 则用x为参数执行promise2

继续修改then方法, 以及添加resolvePromise来执行Promise解决过程

function _isFunction(val) {
  return typeof val === 'function'
}
function _isThenable(x) {
  return _isFunction(x) || (typeof x === 'object' && x !== null)
}

/**
 * Promise 解决过程
 * 如果是thenable对象, 则触发该对象的then方法
 * 如果是一个值, 则直接调用resolve解析这个值
 * @param {Promise}} promise
 * @param {Object} x
 * @param {Function} resolve
 * @param {Function} reject
 */
function resolvePromise(promise, x, resolve, reject) {
  // 要求每次返回新的promise
  // 如果返回是当前的promise, 则抛出typeError
  if (x === promise) {
    reject(new TypeError('Chaining cycle detected for promise'))
  }
  let called = false
  // 判断是否thenable对象
  if (_isThenable(x)) {
    try {
      const { then } = x
      if (_isFunction(then)) {
        then.call(
          x,
          val => {
            if (!called) {
              called = true
              // 如果不断的返回thenable
              // 则需要不断地递归
              // 但是实际上不应该不断的返回thenable
              resolvePromise(promise, val, resolve, reject)
            }
          },
          reason => {
            if (!called) {
              called = true
              reject(reason)
            }
          }
        )
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) {
        return
      }
      called = true
      reject(e)
    }
  } else {
    //  非thenable, 则以该值来执行resolve
    resolve(x)
  }
}
class Promise() {
    // ...
    /**
   * then方法
   * @param {Function} [onFulfilled] 前then的resolve函数, 当promise为RESOLVE时,处理当前结果
   * @param {Function} onRejected 当前then的reject函数, 当promise被REJECT时调用
   * @returns {Promise}
   * @memberof Promise
   */
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : err => {
            throw err
          }
    const self = this
    // 如果有then方法调用, 则将hasThenHandle设为true
    // console.log(this);
    this.hasThenHandle = true
    /**
     * 返回一个新的promise, 用于链式调用
     */
    const ret = new Promise(function(resolve, reject) {
      // 用try..catch包裹执行方法
      const tryCatchWrapper = function(fnc) {
        return function() {
          try {
            fnc()
          } catch (e) {
            reject(e)
          }
        }
      }
      // 封装resolve方法回调
      const doResolve = tryCatchWrapper(function() {
        resolvePromise(ret, onFulfilled(self.value), resolve, reject)
      })
      // 封装reject方法回调
      // 如果当前then没有相应的reject回调
      const doReject = tryCatchWrapper(function() {
        resolvePromise(ret, onRejected(self.value), resolve, reject)
      })
      if (self.status === PENDING) {
        // 如果当前promise还未执行完毕, 则设置回调
        self.onResolveCallback.push(doResolve)
        self.onRejectCallback.push(doReject)
      } else if (self.status === RESOLVED) {
        // 如果为RESOLVE, 则异步执行resolve
        setTimeout(doResolve, 0)
      } else {
        // 如果为REJECT, 则异步执行reject
        setTimeout(doReject, 0)
      }
    })
    return ret
  }
}

至此一个Promise可以说基本完成了.(完整代码请看index.js)

规范外的一些东西

其实规范中定义的是Promise的构建和执行过程.

而我们日常用到的却不至于规范中所提到的.

比如

  • catch
  • finally
  • Promise.resolve
  • Promise.reject
  • all (未实现)
  • race (未实现)

那接下来就说下关于这部分的实现

catch

上面有提到. catch其实是then(undefined, reject) 的简写. 所以这里比较简单

class Promise() {
    // ...
    catch(reject) {
        // 相当于新加入一个then方法
        return this.then(undefined, reject)
    }
}

finally (ES2018引入标准)

finally函数作用我想大家都应该知道, 就是无论当前promise状态是如何. 都一定会执行回调.

finally方法中, 不接收任何参数, 所以并不能知道前面的Promise的状态.

同时, 他不会对promise产生影响.总是返回原来的值 所以在finally中的操作,应该是与状态无关, 不依赖于promise的执行结果

class Promise() {
    // ...
    finally(fnc = () => {}) {
        return this.then(val => {
            fnc()
            return val
        }, err => {
            fnc()
            throw err
        })
    }
}

Promise.resolve和Promise.reject (这里是从ES6入门中看到的定义)

// 调用形式
Promise.resolve(arg)
Promise.reject(arg)
  • Promise.resolve

    根据arg的不同, 会执行不同的操作 - arg为Promise实例, 则原封不动的返回这个实例 - arg为thenable对象, 则会将arg转成promise, 并且立即执行arg.then方法(并不代表同步, 而是本轮事件循环结束时执行) - arg不满足上述情况, 则返回一个新的Promise实例, 状态为resolved, 终值为arg 因此Promise.resolve是一个更方便的创建Promise实例的方法.

  • Promise.reject

    这里就不会区分arg, 而是原封不动的把arg作为据因, 执行后续方法的调用.

实现代码

class Promise() {
    // ...
    /**
     * Promise.resolve
     * 将参数转成Promise对象
     * @static
     * @param {any} val
     * @returns {MPromise}
     * @memberof MPromise
     */
    static resolve(x) {
        // 如果为MPromise实例
        // 则返回该实例
        if(x instanceof Promise) {
            return val
        } else if(_isThenable(x)) {
            // 如果为具有then方法的对象
            // 则转为MPromise对象, 并且执行thenable
            /**
             * @example
             * MPromise.resolve({
             *      then(res) {
             *          console.log('do promise')
             *          res(10)
             *      }
             *  })
             */
            return new Promise(function(res, rej) {
                // 执行异步
                setTimeout(function() {
                    val.then(res, rej)
                }, 0)
            })
        }
        // 如果val为一个原始值,或者不具有then方法的对象
        // 则返回一个新的MPromise对象,状态为resolved
        /**
         * @example
         * MPromise.resolve()
         */
        return new Promise(function(res) {res(x)})
    }
    /**
     * reject方法参数会原封不动的作为据因而变成后续方法的参数
     * 且初始状态为REJECT
     * 不存在判别thenable
     * @static
     * @param {any} reason 
     * @returns 
     * @memberof MPromise
     */
    static reject(reason) {
        /**
         * @example
         * MPromise.reject('some error')
         */
        return new Promise(function(res, rej) {rej(reason)})
    }
}

开发过程中遇到其他问题

node中的unhandledRejection和浏览器中的Uncaught (in promise) 提示

在Promise中产生的所有错误都会被Promise吞掉. 当没有相应的错误处理函数时候, node和浏览器分别有不同的表现.

但是这并不是一个新的错误, 因为不能用try{} catch(){} 捕获.

所以在浏览器端, 是一个console.error的错误提示, 在node中, 这个算是一个事件. 具体可以通过process.on来监听

process.on('unhandledRejection', function(err, p) {
  throw err
})

在编写代码中, 一开始卡在这一步挺久.

由于无法知道promise实例后续是否有相应的错误处理函数.

简单的判断onReject === undefined 是不行的.

形如:

Promise.reject(10)
// 或者
new Promise(function(res, rej) {
  rej(10)
})

这类是同步执行的, onReject === undefined 恒为true.

我的做法是给promise实例添加一个hasThenHandle的属性, 在then方法中将其设为true

reject方法中使用setTimeout异步判断该值是否为true, 如果不是则通过console.error抛出提示.

其实在原生Promise中, 抛出的unhandledRejection 也是属于异步的.

Promise.reject(10)
console.log('after Promise.reject')
new Promise(function(res, rej) {
  rej(10)
})
console.log('after new Promise')

// 输出
// after Promise.reject
// after new Promise
// Uncaught (in promise) 10
// Uncaught (in promise) 10

于是这个问题也能得到很好地解决.

至此完整代码已经结束, 具体看index.js.

存在的问题

  • 由于用的是setTimeout模拟, 所以优先级不能保证高于setTimeout
    • 浏览器中可以用MessageChannel(macrotask)
    • node中可以用setImmediate(优先级在某些情况下比setTimeout高一些)
    • setTimeout和setImmediate在无IO操作下,两者执行顺序不确定,但是在IO操作下,setImmediate比setTimeout优先级高. 且setImmediate只在IE下有效

参考

【翻译】Promises/A+规范

ECMAScript 6入门