promise-polyfill 梳理分析【一:构造和resolve决策】

3,994 阅读10分钟

原地址链接

现在主流的浏览器都已经支持大部分的ES6的语法了,但是我们还是基本上会使用Babel将代码转成ES5的代码(可以结合webpack对polyfill进行动态引入),为了更好的兼容,我们还是会引入一些polyfill,今天主要是想梳理一下经常使用的Promise,就从这个promise-polyfill库进行分析吧。

请结合源码阅读

第二节

相对于 Promise/A+ 规范,这个库还实现了一些常用的Promise方法,比如: allracefinally等,这些考虑放在另外一章进行梳理,这些相对比较容易。

篇幅很长,主要是担心自己漏掉细节,可能有些地方比较啰嗦重复。这章只是梳理了正常执行的流程,不包括异常的处理流程。那么下一节就会梳理 reject 的执行流程和一些原型链上的方法,例如allracefinally的实现等。

构造函数

阅读分析源码可以将代码下载下来,然后进行断点调试,多调几遍,核心的流程就可以分析完成了。接下来,就从断点开始吧。

下载源码

  1. 该库本来就是用来做兼容的,所有代码全都是ES5的,所有直接在它的github上拷下来就行,当然也是可以通过npm安装的,这里就不多说了。源码地址

  2. 得到源码后,还需要修改一些构造函数挂载的全局变量名。因为主流的浏览器已经实现了Promise了,直接在html页面中用script标签引入的话,它会判断浏览器中已经有了相应的Promise就不会把自己挂载上去了。

if (!('Promise' in globalNS)) {  // 这里的 globalNS 在浏览器中就是 window
  globalNS['Promise'] = Promise;
} else if (!globalNS.Promise.prototype['finally']) {
  globalNS.Promise.prototype['finally'] = finallyConstructor;
}

我将globalNS['Promise'] 修改成 globalNS['MyPromise'],挂载到其他地方,最后如图:

全局挂载变量修改

  1. html 使用 script 标签引入代码,然后简单的使用MyPromise
...
<script src="./promise-ployfill.js"></script>
...
<script>
    window.onload = function () {
      var p = new MyPromise(function (resolve, reject) {
        setTimeout(() => {
          resolve('ok');
        }, 3000)
      });

      p.then(function (value) {
        console.log(value);
      })
    }
</script>

运行看看,会不会报错。

运行测试

源码结构

通过 vscode 看看代码结构

代码结构

这个 Promise 就是构造函数了,入口就在这里了。当我们使用 new MyPromise 就是调用这个构造函数来生成Promise实例了。

我们先分析一下代码然后在打断点进行验证。

Promise 构造函数

首先了解一下一个Promise有什么样的要求:

  1. promise 是有状态的,一个 promise 有且只有一个状态(pending,fulfilled,rejected 其中之一)。并且只能从 pending 状态到 fulfilled 状态或者 pending 状态到 rejected 状态。所有可以预知,必须有一个属性来表面或者控制状态。
  2. 一个 promise 必须提供一个 then 方法,用来获取当前或最终的 value 或 reason。一个 promise 的 then 方法接受两个参数:promise.then(onFulfilled, onRejected),并且这两个参数都是可选的。

接下来看一下构造函数:

/**
 * @constructor
 * @param {Function} fn
 */
function Promise(fn) {
  if (!(this instanceof Promise))
    throw new TypeError('Promises must be constructed via new');
  if (typeof fn !== 'function') throw new TypeError('not a function');
  /** @type {!number} */
  this._state = 0;
  /** @type {!boolean} */
  this._handled = false;
  /** @type {Promise|undefined} */
  this._value = undefined;
  /** @type {!Array<!Function>} */
  this._deferreds = [];

  doResolve(fn, this);
}
  1. 首先判断调用这个构造函数是否使用 new 关键字进行使用,否则报错。
  2. 再判断构造函数传入的 fn 必须是一个 function 否则进行报错。
  3. 初始化 this._state = 0,这就是要求的状态的表示属性了。这个库的状态有四个值:0、1、2、4,前三个分别对应 pending,fulfilled,rejected,最后一个状态用来表示 thenable(也就是调用 then)函数的 resolve 的值也是一个 Promise 实例,这个主要是用来转移状态。
  4. 初始化 this._handled = false,用来表面当前的 promise 示例是否已经被处理过。
  5. 初始化 this._value = undefined ,用来记录resolvereject传递的值。
  6. 初始化this._deferreds = [];,用来存储 then 传入的方法,之后在 promise 从 pending 状态到 fulfilled状态(resolve后),会循环处理这些传入函数。
  7. 最后将传入的fn函数,传入到 doResolve 方法中去执行。

doResolve 函数

/**
 * Take a potentially misbehaving resolver function and make sure
 * onFulfilled and onRejected are only called once.
 *
 * Makes no guarantees about asynchrony.
 */
function doResolve(fn, self) {
  var done = false;
  try {
    fn(
      function(value) {
        if (done) return;
        done = true;
        resolve(self, value);
      },
      function(reason) {
        if (done) return;
        done = true;
        reject(self, reason);
      }
    );
  } catch (ex) {
    if (done) return;
    done = true;
    reject(self, ex);
  }
}
  1. 如官方的注释说的,这个函数主要是用来保证 resolvereject 只会被执行一次。之前说过 promise 的要求,状态只能变更一次,所有这里主要是防止执行多次。通过一个标志变量done来避免,代码还是比较好理解。

  2. 可以看到fn函数被执行了,并且传入了两个函数,这里结合我们使用new MyPromise的代码看一下:

var p = new MyPromise(function (resolve, reject) {
   setTimeout(() => {
      resolve('ok');
    }, 3000)
});

这里的 resolve 就等于:

 function(value) {
    if (done) return;
    done = true;
    resolve(self, value);
 }

reject 就等于:

function(reason) {
    if (done) return;
   done = true;
   reject(self, reason);
}
  1. 函数的执行被try-catch 包裹,就是说如果fn执行错误的话,就会直接导致 promise 调用 reject ,从 pending 状态变更到 rejected 状态了。

打断点先看看 promise 的构造函数的执行(gif)

我们先在如下几个地方打入断点:

  1. new MyPromise
  2. new MyPromise 传入的函数内部
  3. Promise 构造函数
  4. doResolve 内部

开始执行吧~~~

promise 执行动图

then 函数

doResolve执行完成后,new MyPromise就会返回了一个 promise 对象了,然后我们立马又去调用了这个 promise 对象的 then 函数。看看这个函数干了写什么。

Promise.prototype.then = function(onFulfilled, onRejected) {
  // @ts-ignore
  var prom = new this.constructor(noop);

  handle(this, new Handler(onFulfilled, onRejected, prom));
  return prom;
};
  1. 首先调用了this对象的constructor函数又创建了一个MyPromise对象,这里的this指的是当前的promise对象,constructor 就指向的是Promise这个构造函数。注意这里的noop,它只是一个空函数,没有任何操作。

  2. 接着调用 handle 函数,传入了两个参数,第一个是当前 promise 对象, 第二个是一个Handler实例。

  3. 最后将新创建的 promise 对象返回,这样就可以链式调用了。

Handler 对象

function Handler(onFulfilled, onRejected, promise) {
  this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}

这个对象也是相对比较简单,只是将then 传入的resolvereject处理函数绑定在了当前对象上,这里注意一点,上一步then函数创建的新promise对象(noop创建的)也是绑定在了这个对象上。

handle 函数

看的到,then 函数关键的一步就是 handle 函数,把当前的 Promise 对象和 Handler 对象传入进行处理。

function handle(self, deferred) {
  ...
  if (self._state === 0) {
    self._deferreds.push(deferred);
    return;
  }
  ...
}

这里我们先只看到这一步,因为,在构造 Promise 对象时候,我们传入的函数,是使用了setTimeout回调调用的时候才去执行resolve,也就是成功回调后才去修改 promise 状态。所有我们执行then函数的时候,promise 对象的状态还是初始状态,还没有修改,所以then函数在还没有resolve的情况下,只是将传入的 Handler 对象存储到了 _deferreds 属性上。

那么到这里,我们可以发现,其实我们使用then函数,传入的处理函数都只是先存放在了一个任务队列中(在还没有调用resolve的情况下)。

resolve 函数

上面的几个步骤后,Promise 同步执行的操作就已经完了。之后的步骤就是等待resolve或者reject的执行了,所以接下来看看当我们setTimeout回调调用了resolve之后是个什么样子执行流程。

function resolve(self, newValue) {
  try {
    // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
    if (newValue === self)
      throw new TypeError('A promise cannot be resolved with itself.');
    if (
      newValue &&
      (typeof newValue === 'object' || typeof newValue === 'function')
    ) {
      var then = newValue.then;
      if (newValue instanceof Promise) {
        self._state = 3;
        self._value = newValue;
        finale(self);
        return;
      } else if (typeof then === 'function') {
        doResolve(bind(then, newValue), self);
        return;
      }
    }
    self._state = 1;
    self._value = newValue;
    finale(self);
  } catch (e) {
    reject(self, e);
  }
}

还记得上面说的 fn这个传入函数么,那里传入了上面这个resolve,回顾一下:

 function(value) {
    if (done) return;
    done = true;
    resolve(self, value);
 }

所以,newValues 就是setTimeout那里使用resolve调用传入的ok字符串。

  1. 判断 newValues 是否是自身,如果是则报错。
  2. 如果newValues是一个对象或者方法(构造函数)。
    1. 是一个 Promise 实例,则将状态修改为3,然后把值赋值给_value,再去执行finale(self)去完成最后的操作,这里先记住什么情况下 state 是被赋值为 3 的,之后在来分析。
    2. 如果这个对象不是一个 Promise 实例,但是有then方法,则调用doResolve方法去直接执行这个then函数。
  3. 上面两种情况都不成立,则将状态修改为1,然后把值赋值给_value。最后调用finale(self)

这里可以看到,状态是一定会进行变更了,然后再将实例丢给finale函数去执行。跟着流程我们接下来看看finale

finale 函数

function finale(self) {
  if (self._state === 2 && self._deferreds.length === 0) {
    Promise._immediateFn(function() {
      if (!self._handled) {
        Promise._unhandledRejectionFn(self._value);
      }
    });
  }

  for (var i = 0, len = self._deferreds.length; i < len; i++) {
    handle(self, self._deferreds[i]);
  }
  self._deferreds = null;
}
  1. 如果状态为 2 或者没有处理函数,就把事情交给_unhandledRejectionFn 去处理了。
  2. 循环的执行使用then注册的handler对象。之前说过,then传入的方法都会被放入到_deferreds中。
const p = new MyPromise(...)
p.then(() => { ... })
p.then(() => { ... })
p.then(() => { ... })

这里看到我们使用p执行了三个then,所以_deferreds就会有三个,finale会一次性全部循环处理完,注意这里与这样的链式调用是有区别的:

const p = new MyPromise(...)
p.then(() => { ... }).then(() => { ... }).then(() => { ... })

之前看过then方法会常见一份新的promise对象,然后返回,所以链式调用都是基于新的promise上。

好,接下来看一下handle函数:

function handle(self, deferred) {
  while (self._state === 3) {
    self = self._value;
  }
  if (self._state === 0) {
    self._deferreds.push(deferred);
    return;
  }
  self._handled = true;
  Promise._immediateFn(function() {
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
    if (cb === null) {
      (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
      return;
    }
    var ret;
    try {
      ret = cb(self._value);
    } catch (e) {
      reject(deferred.promise, e);
      return;
    }
    resolve(deferred.promise, ret);
  });
}

之前我把其他分支省略了,那么现在就是看其他分支的时候了。

  1. resolve 传入的是一个promise也就是该库定义的状态 3 时候,这里直接把 self 指向了 传入的promise
  2. 状态是 0 就先过了,之前说过。
  3. 接下来就是标记_handle,表示已经处理过。
  4. 接着将cb,也就是我们传入的回调函数onFulfilled、onRejected,放到了_immediateFn方法里面去执行。
Promise._immediateFn =
  // @ts-ignore
  (typeof setImmediate === 'function' &&
    function(fn) {
      // @ts-ignore
      setImmediate(fn);
    }) ||
  function(fn) {
    setTimeoutFunc(fn, 0);
  };

看到这个函数,只是兼容了不同环境的创建异步任务的方法,ES6里面Promise是 Microtasks (微任务),在ES5上,这里只能使用setTimeout创建一个 Macrotasks(宏任务),两者的区别有兴趣的同学去搜索一下吧。

根据_immediateFn函数我们就可以知道,finale函数将最后调用cb函数,并且是在下一个 Macrotasks 上,然后得到cb执行的结果,再去调用resolve(deferred.promise, ret);,这样就能继续处理后面新的promise,也就是这样循环往复的去调用了。

小结

梳理完了promise-polyfill源码的从 pending 状态到 fulfilled状态的执行流程。

  1. new Promise的时候,传入的函数里面代码是同步执行的,resolvereject方法是经过了防止多次调用的处理的,所以就算在方法里面调用多次也没有用。
  2. then 函数执行的时候,状态还没有变化时,只是将 传入函数 保存(_deferreds)起来,在之后状态变更后再遍历调用。
  3. resolve函数被执行,会修改当前promise状态,遍历执行_deferreds。并且会生成一个新的promise对象,后再去 resolve 这个新的对象,链式操作就是这样链接起来的。