解读Promise内部实现原理

36,695 阅读14分钟

前言

早前有针对 Promise 的语法写过博文,不过仅限入门级别,浅尝辄止食而无味。后面一直想写 Promise 实现,碍于理解程度有限,多次下笔未能满意。一拖再拖,时至今日。

随着 Promise/A+规范ECMAscript规范Promise API 制定执行落地,Javascript 异步操作的基本单位也逐渐从 callback 转换到 promise。绝大多数JavaScript/DOM平台新增的异步API(FetchService worker)也都是基于Promise构建的。这其中对 Promise 理解不是仅看过 API,读过几篇实践就能完全掌握的。笔者以此行文,剖析细节,伴随读者一起成长,砥砺前行。

本文为前端异步编程解决方案实践系列第二篇,主要分析 Promise 内部机制及实现原理。后续异步系列还会包括GeneratorAsync/Await相关,挖坑占位。

注:本文 Promise 遵守 Promises/A+ 规范,实现参照 then/promise

Promise 是什么

既然要讲实现原理,不免要承前启后交代清楚 Promise 是什么。查阅文档,如下:

A promise represents the eventual result of an asynchronous operation. --Promises/A+

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation. --ECMAscript

Promises/A+ 规范中表示为一个异步操作的最终结果,ECMAscript 规范定义为延时或异步计算最终结果的占位符。言简意赅,但稍微聱牙诘屈,如何表述更浅显易懂呢?

说个故事,Promise 是一个美好的承诺,承诺本身会做出正确延时或异步操作。承诺会解决callback处理异步回调可能产生的调用过早,调用过晚、调用次数过多过少、吞掉可能出现的错误或异常问题等。另外承诺只接受首次 resolve(..)reject(..) 决议,承诺本身状态转变后不会再变,承诺所有通过 then(..) 注册的回调总是依次异步调用,承诺所有异常总会被捕获抛出。她,是一个可信任的承诺。

严谨来讲,Promise 是一种封装和组合未来值得易于复用机制,实现关注点分离、异步流程控制、异常冒泡、串行/并行控制等。

注:文中提及 callback 问题详情见<<你不知道的JavaScript(中卷)>> 2.3 、3.3章节

标准解读

Promise A+ 规范字数不多简明扼要,但仔细翻读,其中仍有有几点需要引人注意。

thenable 对象

thenable 是一个定义 then(..) 方法的对象或函数。thenable 对象的存在目的是使 Promise 的实现更具有通用性,只要其暴露出一个遵循 Promise/A+ 规范的 then(..) 方法。同时也会使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。

识别 thenable 或行为类似 Promise 对象可以根据其是否具有 then(..) 方法来判断,这其实叫类型检查也可叫鸭式辩型(duck typing)。对于 thenable 值鸭式类型检测大致类似于:

if ( p !== null && 
     (
       typeof p === 'object' || 
       typeof p === 'function'
     ) &&
     typeof p.then === 'function'
) {
    // thenable
} else {
    // 非 thenable 
}
then 回调异步执行

众所周知,Promise 实例化时传入的函数会立即执行,then(...) 中的回调需要异步延迟调用。至于为什么要延迟调用,后文会慢慢解读。这里有个重要知识点,回调函数异步调用时机。

onFulfilled or onRejected must not be called until the execution context stack contains only platform code --Promise/A+

简译为onFulfilledonRejected 只在执行环境堆栈仅包含平台代码时才可被调用。稍有疑惑,Promise/A+ 规范又对此句加以解释:“实践中要确保 onFulfilledonRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用宏任务 macro-task机制或微任务 micro-task机制来实现。”

虽然Promise A+未明确指出是以 microtask 还是 macrotask 形式放入队列,但 ECMAScript 规范明确指出 Promise 必须以 Promise Job 形式加入 job queues(也就是 microtask)。Job Queue 是 ES6 中新提出的概念,建立在事件循环队列之上。job queue存在也是为了满足一些低延迟的异步操作。

敲黑板划重点,注意这里 macrotask microtask 分别表示异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到两个队列中,首先在 macrotask 的队列(也叫 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。

对于microtask执行时机,whatwg HTML规范中也有阐述,详情可点击查阅。更多相关文章可参考附录 event loop

再看一个示例,加深理解:

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
  console.log('promise1');
}).then(function () {
  console.log('promise2');
});

打印的顺序?正确答案是:promise1, promise2, setTimeout

在进一步实现 Promise 对象之前,简单模拟异步执行函数供后文Promise回调使用(也可采用 asap库等)。

var asyncFn = function () {
  if (typeof process === 'object' && process !== null && 
      typeof(process.nextTick) === 'function'
  ) {
    return process.nextTick;
  } else if (typeof(setImmediate) === 'function') {
    return setImmediate;
  }
  return setTimeout;
}();
Promise 状态

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

为保持代码清晰,暂无异常处理。同时为表述方便,约定如下:

  • fulfilled 使用 resolved 代替
  • onFulfilled 使用 onResolved 代替

Promise 构造函数

从构造函数开始,我们一步步实现符合 Promsie A+ 规范的 Promise。大概描述下,Promise构造函数需要做什么事情。

  1. 初始化 Promise 状态(pending
  2. 初始化 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
  3. 立即执行传入的 fn 函数,传入Promise 内部 resolvereject 函数
  4. ...
function Promise (fn) {
  // 省略非 new 实例化方式处理
  // 省略 fn 非函数异常处理

  // promise 状态变量
  // 0 - pending
  // 1 - resolved
  // 2 - rejected
  this._state = 0;
  // promise 执行结果
  this._value = null;
 
  // then(..) 注册回调处理数组
  this._deferreds = [];

  // 立即执行 fn 函数
  try {
    fn(value => {
      resolve(this, value);
    },reason => {
      reject(this, reason);
    })
  } catch (err) {
    // 处理执行 fn 异常
    reject(this, err);
  }
}

_state_value 变量很容易理解,_deferreds变量做什么?规范描述:then 方法可以被同一个 promise 调用多次。为满足多次调用 then 注册回调处理,内部选择使用 _deferreds 数组存储处理对象。具体处理对象结构,见 then 函数章节。

最后执行 fn 函数,并调用 promise 内部的私有方法 resolverejectresolvereject 内部细节随后介绍。

then 函数

Promise A+提到规范专注于提供通用的 then 方法。then 方法可以被同一个 promise 调用多次,每次返回新 promise 对象 。then 方法接受两个参数onResolvedonRejected(可选)。在 promiseresolvereject 后,所有 onResolvedonRejected 函数须按照其注册顺序依次回调,且调用次数不超过一次。

根据上述,then 函数执行流程大致为:

  1. 实例化空 promise 对象用来返回(保持then链式调用)
  2. 构造 then(..) 注册回调处理函数结构体
  3. 判断当前 promise 状态,pending 状态存储延迟处理对象 deferred ,非pending状态执行 onResolvedonRejected 回调
  4. ...
Promise.prototype.then = function (onResolved, onRejected) {

  var res = new Promise(function () {});
  // 使用 onResolved,onRejected 实例化处理对象 Handler
  var deferred = new Handler(onResolved, onRejected, res);

  // 当前状态为 pendding,存储延迟处理对象
  if (this._state === 0) {
    this._deferreds.push(deferred);
    return res;
  }

  // 当前 promise 状态不为 pending
  // 调用 handleResolved 执行onResolved或onRejected回调
  handleResolved(this, deferred);
  
  // 返回新 promise 对象,维持链式调用
  return res;
};

Handler 函数封装存储 onResolvedonRejected 函数和新生成 promise 对象。

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

链式调用为什么要返回新的 promise

如我们理解,为保证 then 函数链式调用,then 需要返回 promise 实例。但为什么返回新的 promise,而不直接返回 this 当前对象呢?看下面示例代码:

var promise2 = promise1.then(function (value) {
  return Promise.reject(3)
})

假如 then 函数执行返回 this 调用对象本身,那么 promise2 === promise1promise2 状态也应该等于 promise1 同为 resolved。而 onResolved 回调中返回状态为 rejected 对象。考虑到 Promise 状态一旦 resolvedrejected就不能再迁移,所以这里 promise2 也没办法转为回调函数返回的 rejected 状态,产生矛盾。

handleResolved 函数功能为根据当前 promise 状态,异步执行 onResolvedonRejected 回调函数。因在 resolvereject 函数内部同样需要相关功能,提取为单独模块。往下翻阅查看。

resolve 函数

Promise 实例化时立即执行传入的 fn 函数,同时传递内部 resolve 函数作为参数用来改变 promise 状态。resolve 函数简易版逻辑大概为:判断并改变当前 promise 状态,存储 resolve(..)value 值。判断当前是否存在 then(..) 注册回调执行函数,若存在则依次异步执行 onResolved 回调。

但如文初所 thenable 章节描述,为使 Promise 的实现更具有通用性,当 value 为存在 then(..) 方法的 thenable 对象,需要做 Promise Resolution Procedure 处理,规范描述为 [[Resolve]](promise, x)。(x 即 为后面 value 参数)。

具体处理逻辑流程如下:

  • 如果 promisex 指向同一对象,以 TypeError 为据因拒绝执行 promise

  • 如果 xPromise ,则使 promise 接受 x 的状态

  • 如果 x 为对象或函数

    1. x.then 赋值给 then
    2. 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
    3. 如果 then 是函数,将 x 作为函数的作用域 this 调用之。
    4. 如果 x 不为对象或者函数,以 x 为参数执行 promise

原文参考Promise A+规范 Promise Resolution Procedure

function resolve (promise, value) {
  // 非 pending 状态不可变
  if (promise._state !== 0) return;
  
  // promise 和 value 指向同一对象
  // 对应 Promise A+ 规范 2.3.1
  if (value === promise) {
    return reject( promise, new TypeError('A promise cannot be resolved with itself.') );
  }
  
  // 如果 value 为 Promise,则使 promise 接受 value 的状态
  // 对应 Promise A+ 规范 2.3.2
  if (value && value instanceof Promise && value.then === promise.then) {
    var deferreds = promise._deferreds
    
    if (value._state === 0) {
      // value 为 pending 状态
      // 将 promise._deferreds 传递 value._deferreds
      // 偷个懒,使用 ES6 展开运算符
      // 对应 Promise A+ 规范 2.3.2.1
      value._deferreds.push(...deferreds)
    } else if (deferreds.length !== 0) {
      // value 为 非pending 状态
      // 使用 value 作为当前 promise,执行 then 注册回调处理
      // 对应 Promise A+ 规范 2.3.2.2、2.3.2.3
      for (var i = 0; i < deferreds.length; i++) {
        handleResolved(value, deferreds[i]);
      }
      // 清空 then 注册回调处理数组
      value._deferreds = [];
    }
    return;
  }

  // value 是对象或函数
  // 对应 Promise A+ 规范 2.3.3
  if (value && (typeof value === 'object' || typeof value === 'function')) {
    try {
      // 对应 Promise A+ 规范 2.3.3.1
      var then = obj.then;
    } catch (err) {
      // 对应 Promise A+ 规范 2.3.3.2
      return reject(promise, err);
    }

    // 如果 then 是函数,将 value 作为函数的作用域 this 调用之
    // 对应 Promise A+ 规范 2.3.3.3
    if (typeof then === 'function') {
      try {
        // 执行 then 函数
        then.call(value, function (value) {
          resolve(promise, value);
        }, function (reason) {
          reject(promise, reason);
        })
      } catch (err) {
        reject(promise, err);
      }
      return;
    }
  }
  
  // 改变 promise 内部状态为 `resolved`
  // 对应 Promise A+ 规范 2.3.3.4、2.3.4
  promise._state = 1;
  promise._value = value;

  // promise 存在 then 注册回调函数
  if (promise._deferreds.length !== 0) {
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    // 清空 then 注册回调处理数组
    promise._deferreds = [];
  }
}

resolve 函数逻辑较为复杂,主要集中在处理 valuex)值多种可能性。如果 valuePromise 且状态为pending时,须使 promise 接受 value 的状态。在 value 状态为 pending 时,简单将 promisedeferreds 回调处理数组赋予 value deferreds变量。非 pending 状态,使用 value 内部值回调 promise 注册的 deferreds

如果 valuethenable 对象,以 value 作为函数的作用域 this 调用之,同时回调调用内部 resolve(..)reject(..)函数。

其他情形则以 value 为参数执行 promise,调用 onResolvedonRejected 处理函数。

事实上,Promise A+规范 定义的 Promise Resolution Procedure 处理流程是用来处理 then(..) 注册的 onResolvedonRejected 调用返回值 与 then 新生成 promise 之间关系。不过考虑到 fn 函数内部调用 resolve(..)产生值 与当前 promise 值仍然存在相同关系,逻辑一致,写进相同模块。

reject 函数

Promise 内部私有方法 reject 相较于 resolve 逻辑简单很多。如下所示:

function reject (promise, reason) {
  // 非 pending 状态不可变
  if (promise._state !== 0) return;

  // 改变 promise 内部状态为 `rejected`
  promise._state = 2;
  promise._value = reason;

  // 判断是否存在 then(..) 注册回调处理
  if (promise._deferreds.length !== 0) {
    // 异步执行回调函数
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    promise._deferreds = [];
  }
}

handleResolved 函数

了解完 Promise 构造函数、then 函数、以及内部 resolvereject 函数实现,你会发现其中所有的回调执行我们都统一调用 handleResolved函数,那 handleResolved 到底做了哪些事情,实现又有什么注意点?

handleResolved 函数具体会根据 promise 当前状态判断调用 onResolvedonRejected,处理 then(..) 注册回调为空情形,以及维护链式 then(..) 函数后续调用。具体实现如下:

function handleResolved (promise, deferred) {
  // 异步执行注册回调
  asyncFn(function () {
    var cb = promise._state === 1 ? 
            deferred.onResolved : deferred.onRejected;

    // 传递注册回调函数为空情况
    if (cb === null) {
      if (promise._state === 1) {
        resolve(deferred.promise, promise._value);
      } else {
        reject(deferred.promise, promise._value);
      }
      return;
    }

    // 执行注册回调操作
    try {
      var res = cb(promise._value);
    } catch (err) {
      reject(deferred.promise, err);
    }

    // 处理链式 then(..) 注册处理函数调用
    resolve(deferred.promise, res);
  });
}

具体处理注册回调函数 cb 为空情形,如下面示例。判断当前回调 cb 为空时,使用 deferred.promise 作为当前 promise 结合 value 调用后续处理函数继续往后执行,实现值穿透空处理函数往后传递。

Promise.resolve(233)
  .then()
  .then(function (value) {
    console.log(value)
  })

关于 then 链式调用,简单再说下。实现 then 函数的链式调用,只需要在 Promise.prototype.then(..) 处理函数中返回新的 promise 实例即可。但除此之外,还需要依次调用 then 注册的回调处理函数。如 handleResolved 函数最后一句 resolve(deferred.promise, res) 所示。

then 注册回调函数为什么异步执行

这里回答开篇所提到的一个问题,then 注册的 onResolvedonRejected 函数为什么要采用异步执行?再来看一段实例代码。

var a = 1;

promise1.then(function (value) {
  a = 2;
})

console.log(a)

promise1 内部执行同步或异步操作未知。假如未规定 then 注册回调为异步执行,则这里打印 a 可能存在两种值。promise1 内部同步操时 a === 2,相反执行异步操作时 a === 1。为屏蔽依赖外部的不确定性,规范指定 onFulfilledonRejected 方法异步执行。

promise 内部错误或异常

如果 promiserejected,则会调用拒绝回调并传入拒由。比如在 Promise 的创建过程中(fn执行时)出现异常,那这个异常会被捕捉并调用 onRejected

但还存在一处细节,如果 Promise 完成后调用 onResolved 查看结果时出现异常错误会怎么样呢?注意此时 onRejected 不会被触发执行,因为 onResolved 内部异常并不会改变当前 promise 状态(仍为resolved),而是改变 then 中返回新的 promise 状态为 rejected。异常未丢失但也未调用错误处理函数。

如何处理?Ecmascript规范有定义Promise.prototype.catch方法,假如你对 onResolved 处理过程没有信心或存在异常 case 情况,最好还是在 then 函数后调用 catch 方法做异常捕获兜底处理。

Promise 相关方法实现

查阅 Promise 相关文档或书籍,你还会发现 Promise 相关有用的API:Promise.racePromise.allPromise.resolvePromise.reject。这里对 Promise.race 方法实现做个展示,剩余可自行参考实现。

Promise.race = function (values) {
  return new Promise(function (resolve, reject) {
    values.forEach(function(value) {
      Promise.resolve(value).then(resolve, reject);
    });
  });
};

结语

写到这里,核心的 Promise 实现也逐渐完成,Promise 内部细节也在文中或代码中一一描述。限于笔者本身能力有限,对于 promise 内部实现暂未达到庖丁解牛程度,有些地方一笔带过,可能读者心生疑惑。针对不解的地方,建议多读两遍或参考书籍理解。

如果读完拙文能多少有点收获,也算达到笔者初衷,大家一起成长。最后笔者也非完人,文中不免语句不顺或词不达意,望理解。如果对于本文有任何疑问或错误,欢迎斧正,在此先行谢过。

附录

参考文档

  1. ECMA262 Promise

  2. Promises/A+ Specification

  3. [译] Promises/A+ 规范

  4. then/promise

  5. 写一个符合 Promises/A+ 规范并可配合 ES7 async/await 使用的 Promise

  6. 剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

  7. 剖析 Promise之基础篇

参考书籍

  1. 《你不知道的JavaScript(中卷)》

  2. 《深入理解ES6》

  3. 《JavaScript框架设计(第2版)》

  4. 《ES6标准入门(第3版)》

event loop

  1. Tasks, microtasks, queues and schedules

  2. [译]Tasks, microtasks, queues and schedules

  3. Difference between microtask and macrotask within an event loop context

  4. 从event loop规范探究javaScript异步及浏览器更新渲染时机

  5. 深入探究 eventloop 与浏览器渲染的时序问题

打个广告,欢迎关注笔者公众号