promise ployfill 代码分析

promise polyfill 代码分析

一、整体框架介绍

分析 Promise,我们可以从三个阶段入手:

  1. new Promise(fn) 创建一个 promise 实例时
  2. p.then(...) 指定 promise 的回调函数时
  3. 异步函数执行到 resolve(vl)reject(vl)

正常来说,异步函数 fn 的 Promise 会按上述 1→2→3 顺序执行。但是如果 fn 是同步函数的话则会 1→3→2 的顺序。

我们先按正常的异步函数 Promise 来分析一下这三个步骤:

1.1 new Promise(fn)

用 new 操作符创建一个 Promise 实例时,做的操作其实不多,主要是初始化 Promise 实例的一些状态。new 出来的 Promise 实例大概长这个样子:

{
	_state: 0, 
	_value: null,
	_deferredState: 0,
	_deferreds: null,
}

这四个属性作用都很大,我们来一一介绍一下。

_state

p._state 里存放的是 Promise 实例的状态,它有 4 种取值:

  • 0 → 异步 fn 还在执行(pending)
  • 1 → 异步 fn 已经成功执行(fulfilled)
  • 2 → 异步 fn 执行失败 (rejected)
  • 3 → 异步 fn 已经成功执行,但是 resolve 出来的不是一个常量,而是另一个 promise 实例 p2 ,这个时候该 promise 的通过 then 方法指定的回调会被 p2 继承

_value

p._value 里面存储着 promise 当前状态的返回值,具体如下:
-p._value == 0 → null

  • p._value == 1resolve(vl) 里传递出来的 vl
  • p._value == 2reject(res)里传递出来的 res
  • p._value == 3resolve(p2) 时传递出来的另一个 Promise 实例 p2

_deferreds

这个属性其实和 Promise.prototype.then 有很大的关系,但是现在我们还没有介绍到 Promise.prototype.then,所以我们先这样理解:

p._deferreds 是用来存储调用 p.then(fun1, fun2) 时指定的异步回调的,存储的具体单元是 Handler 实例(这个 Handler 类也是后面再说)。

_deferredState

p._deferredState 这个属性是 p._deferreds 的一个辅助变量,它有三种取值:

  • 0 → p 没有调用过 p.then来指定回调,p._deferreds 为 null
  • 1 → p 调用过一次 p.then来指定回调,p._deferreds 为 Handler 实例
  • 2 → p 调用过两次 p.then 来指定回调,p._deferreds 为 Handler 实例组成的数组

1.2 Promise.prototype.then

好的,现在我们说说 Promise.prototype.then。废话不多说,先上源码:

Promise.prototype.then = function(onFulfilled, onRejected) {
  /**
  * this: promise 实例
  * onFulfilled: 开发者指定的成功回调
  * onRejected: 开发者指定的失败后回调
  */ 
  ...
  // noop 是一个空函数,这里是创建了空函数的 Promise 实例 res
  var res = new Promise(noop); 
  // 将两个回调和res封装到一个 Handler 实例,并调用 handle 将调用 then 方法的 promise 实例与 Handler 实例关联起来
  handle(this, new Handler(onFulfilled, onRejected, res));
  // 将 res 返回,此时 res._state 为 0 
  return res;
};

可以看到,它的代码很简单,因为大部分的操作都隐藏在 handle 函数里了。

handle 函数是用来干嘛的呢?如上面注释所言:将两个回调和res封装到一个 Handler 实例,并调用 handle 将调用 then 方法的 promise 实例与 Handler 实例关联起来。(这只是handle 函数的一部分功能,handle 还有着其他更重要的作用,后面再说)。

这个 Handler 实例长这个样子:

let deferred = {
  onFulfilled: onFulfilled, // 异步成功回调
  onRejected: onRejected, // 异步失败回调
  promise: res // p.then() 返回的 Promise 实例
}

那么怎么产生关联的,其实也简单:

p._deferredState = 1; // 标记 p 是有回调的
p._deferreds = deferred; // 通过 p._deferreds 与 Handler 实例 deferred 关联起来

此时,Promise 实例是长这个样子了:

{
	_state: 0, 
	_value: null,
	_deferredState: 1,
	_deferreds: deferred,
}

1.3 resolve(vl) 与 reject(res)

一个 Promise 创建好,又调用 then 指定好回调了,接下来就是等待异步函数执行到 resolve(vl) 或 reject(res) 了。

reject(res)

先说说 reject(res) 吧,异步函数 fn 执行到 reject(res) 证明 fn 执行失败了。reject(res) 会做这几件事:

  1. p._state = 2, 更新 p 的状态码为 2,标记着 fn 执行失败
  2. p._value = res,将通过 reject(res) 传递出来的 res 赋值到 p._value,作为这个 Promise 实例最后的值
  3. 执行 self._deferreds 里面的存储的失败回调

resolve

异步函数 fn 执行到 resolve(vl) ,证明 fn 执行成功了。resolve(vl) 会做这几件事:

  1. vl 为 promise 实例:
    1.1 p._state = 3
    1.2 p._value = vl
    1.3 将 self._deferreds 里面的回调扔给 vl,作为 vl 的回调
  2. vl 为 拥有 then 方法的对象:递归,将 vl.then 看作 p 新的异步函数去执行
  3. 其他:
    3.1 p._state = 1
    3.2 p._value = vl
    3.3 执行 self._deferreds 里面的存储的成功回调

handle 函数

其实无论是 reject(res) 还是 resolve(vl),它们执行到最后,都会调用同一个函数 handle(self, self._defferreds) 来处理自己的回调。

是的,你没看错,还是 handle 这个函数,前面我们有说过,调用 then 方法的时候也会执行 handle 函数来将回调函数存储到 p._defferreds里面 。

所以,handle 函数的作用可以分为两大块:

  1. 存储 Promise 实例的回调函数
  2. 处理 Promise 实例的回调函数
function handle(self, deferred) {
	// self: promise 实例
	// deferred: Handler 实例
	...
	if (self._state === 0) {
    	// 异步函数还没有完成,将 deferred 存储到 self._defferreds 里
    	...
    	return;
  	}
 	// 异步函数已经执行完毕,执行 handleResolved 去处理 deferred 里的回调函数
    handleResolved(self, deferred);
}

关于存储的过程我们在介绍 _deferreds_deferredState 两个内部属性的时候已经大概说过了,这里不再赘述。

我们现在主要看看 handle 是怎么调用 handleResolved 处理回调函数的。

function handleResolved(self, deferred) {
	// self: promise 实例
    // deferred: Handler 实例
	asap(function() {
    	var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; 
        // 根据状态码获取对应回调
        if (cb === null) { 
        	// 没有指定回调
              ...
              return;
        }
        // 有指定回调,执行回调函数,并将 promise 结果传入作为回调的参数
        var ret = tryCallOne(cb, self._value);
        ...
	});
}

这个 asap 是 Promise 执行异步回调的一个工具库,用来兼容 Node 和浏览器环境,将回调函数以高优先级任务来执行(下一个事件循环之前),即把任务放在微任务队列中执行。

看到这里我们应该明白了,resolve / reject 传递出来的值会存储在 p._value 里,然后等着 self._deferreds 里回调函数将其作为参数来调用。

以上,就是 Promise 的大致实现原理。

二、Promise 的状态传递

分析上面的整体流程的时候,有一个点我们没有说得特别清楚,就是执行 resolve(vl) 的时候 vl 是一个 Promise 实例这种情况。

举个例子:

new Promise((resolve, reject) => {
	setTimeout(()=>{
		let a = new Promise(resolve => resolve(1))
		resolve(a);
	}, 1000)
})
.then(res=>console.log(res)) // 1

关于这种情况,我们一步步分析:

  1. 首先,新建了一个 Promise 对象 p
  2. 执行了 p.then,初始化了 p._deferreds
  3. 1 秒后,执行 resolve(a),因为 a 是一个 promise 实例,所以 p._state = 3,p._value = a
  4. 执行 handle(p, p._deferreds ) 处理回调

在这之前,思路都是清晰的,关键还是在于 handle 函数。其实前面介绍 handle 的时候为了精简,写少了处理这种情况的关键一步:

function handle(self, deferred) {
	while (self._state === 3) {
		self = self._value;
	}
	...
}

如代码所示,对于 p._state 为 3 的情况,handle 函数会通过 while 循环拿到 a。

然后 handle 下面全部的处理,都是针对 a 和 p 的回调函数 deferred。也就是说,虽然 p 的异步函数已经成功执行了,但是不会立即执行 p 的成功回调,而是得看 a 的执行结果,如果 a 执行成功才会执行 p 的成功回调,否则会执行 p 的失败回调。这就是所谓的 a 决定了 p 的状态

另外,then 回调返回一个 Promise 实例时,也是一样:

Promise.resolve(1)
.then(res=>Promise.resolve(2))
.then(res=>console.log(res)); // 2

三、Promise 的链式调用

我们都知道,Promise 之所以成功,有一个关键因素就是它可以通过 then 进行链式调用,而这个链式调用的原理是怎么样的呢,下面我们就来分析一下。

链式调用的关键,在于 handleResolved 函数,所以我们来看看它的具体实现。

function handleResolved(self, deferred) {
  // self: promise 实例
  // deferred: Handler 实例

  asap(function() {
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
    if (cb === null) { // 没有指定回调
      if (self._state === 1) { // 将 promise 结果传递给 then 产生的 promise
        resolve(deferred.promise, self._value);
      } else {
        reject(deferred.promise, self._value);
      }
      return;
    }
    // 指定 promise 结果 self._value 作为回调 cb 的参数,并执行
    var ret = tryCallOne(cb, self._value);
    if (ret === IS_ERROR) { // 将回调返回值传递给 then 产生的新 promise
      reject(deferred.promise, LAST_ERROR);
    } else {
      resolve(deferred.promise, ret);
    }
  });
}

还是看一个例子:

let a = Promise.resolve(1); // a._value = 1  
let b = a.then(); // b._value = 1  
let c = b.then(res=>res+1); // res = 1, c._value = 2   
let d = c.then(res=>console.log(res)); // res = 2, d._value = undefined   

调用 p.then() 指定回调时,回调函数的参数一定是 p 的值。

返回的新 promise 实例 res 一定会被处理,只不过处理的方式和参数有区别:

  1. p.then 指定的成功回调为 null,p._state 为 1,res 被 resolve(res, p._value)
  2. p.then 指定的失败回调为 null,p._state 为 2,res 被 reject(res, p._value)
  3. p.then 指定的成功/失败回调正常执行,res 会被 resolve(res, 回调返回值)
  4. p.then 指定的成功/失败回调执行出错,res 会被 reject(res, 错误)

四、ES6 Promise 特性

4.1 Promise.prototype.catch

ES6 里 Promise 的 catch 是 then 指定错误回调的一种语法糖,实现代码也比较简单:

Promise.prototype['catch'] = function (onRejected) {
  return this.then(null, onRejected);
};

说白了,还是 then,不过没有指定成功回调。

这阵子做有什么好处?它最大的作用在于能够通过特定的调用形式来将promise链式调用里的错误全部捕获

前面我们说过,在 handleResolved 函数里,如果异步函数执行完毕后,如果 then 没有指定对应的回调函数,那么异步函数的返回值会被 “冒泡” 到下一个 then 指定的回调里。

所以,链式调用多个 promise 的时候,我们可以用 then 来只指定成功回调,然后在链式调用的末尾放一个 catch 捕获捕获。一旦链式调用遇到了错误,因为没有指定错误回调,这个错误会不断地“冒泡”到下一个 then,最后被末尾的 catch 捕获。

Promise.resolve(1)
.then()
.then(data=>{throw Error('1')})
.then()
.catch(err=>console.log(err));

这种写法也是阮一峰的《ES6标准入门》里推荐的写法。

那么,这个错误是怎么被捕获到并且通过 reject 传递处理的呢?

其实,promise 在执行异步函数、回调函数的时候,都采用了 try 和 ctach(比如 handleResolved 函数里通过 tryCallOne 来执行回调函数时),一旦捕获到错误,就会将错误通过 reject 传递出来。

4.2 Promise.resolve

这个方法用来生成一个 fullfilled 的 Promise 实例,实例的值是该方法的参数。关键函数是 valuePromise

function valuePromise(value) {
  var p = new Promise(Promise._noop);
  p._state = 1;
  p._value = value;
  return p;
}

Promise.resolve 也存在特殊情况:

  1. 参数是 Promise 实例的,直接返回参数
  2. 参数是具有 then 方法的对象时,返回new Promise(then)
  3. 对于第2种情况,如果 then 方法执行错误,会返回 Promise.reject(错误)

4.3 Promise.reject

生成一个 rejected 的 Promise 实例,实例的值是该方法的参数。源码如下:

Promise.reject = function (value) {
  return new Promise(function (resolve, reject) {
    reject(value);
  });
};

4.2 Promise.all

假设执行如下代码:

let p = Promise.all([p1, p2, p3]);

p 的状态由 p1、p2、p3 决定,分两种情况:

  1. 只有 p1、p2、p3 都变为 fulfilled 时,p 的状态才会变为 fulfilled。此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
  2. 只要 p1、p2、p3 有一个被 rejected,p 的状态就会变成 rejected。此时,第一个被 rejected 的实例返回值会传递给 p 的回调函数。

看看代码实现:

Promise.all = function (arr) {
  var args = iterableToArray(arr); // 将 arr 分解为普通数组

  return new Promise(function (resolve, reject) {
    if (args.length === 0) return resolve([]);
    var remaining = args.length;
    function res(i, val) {
      if (val && (typeof val === 'object' || typeof val === 'function')) {
        if (val instanceof Promise && val.then === Promise.prototype.then) {
          // val 是 Promise 实例
          while (val._state === 3) {
            val = val._value;
          }
          if (val._state === 1) return res(i, val._value);
          if (val._state === 2) reject(val._value);
          val.then(function (val) {
            res(i, val); 
          }, reject); // 一旦有错,立刻 reject
          return;
        } else {
          // val 是拥有 then 方法的对象
          var then = val.then;
          if (typeof then === 'function') {
            var p = new Promise(then.bind(val));
            p.then(function (val) {
              res(i, val);
            }, reject); 
            return;
          }
        }
      }
      // val 是普通值,也就是 promise 返回值时
      args[i] = val;
      if (--remaining === 0) {
        resolve(args);
      }
    }
    for (var i = 0; i < args.length; i++) {
      res(i, args[i]); // 用 i 记住 promise 原本下标,方便 promise 返回值插回原位
    }
  });
};

4.3 Promise.race

Promise.race 同样是将多个 Promise 实例包装为一个新的 Promise 实例。

let p = Promise.all([p1, p2, p3]);

不同的是,只要 p1、p2、p3 其中有一个状态改变,p 的状态也跟着改变。率先改变的 Promise 实例的返回值就会传递给 p 的回调函数。

代码实现:

Promise.race = function (values) {
  return new Promise(function (resolve, reject) {
    iterableToArray(values).forEach(function(value){
      Promise.resolve(value).then(resolve, reject);
    });
  });
};
posted @ 2021-02-14 21:15  树干  阅读(173)  评论(0编辑  收藏  举报