JavaScript 核心原理精讲 - 前美团前端技术专家 - 拉勾教育

我在上一讲带你实现了一个浏览器端的 EventEmitter,那么这一讲还是继续进阶,我们来实现一个符合 Promise/A+ 规范的 Promise。

其实在 “14 | 异步编程(中):如何深入理解异步编程的核心 Promise” 中,我已经专门介绍了 Promise 的基本原理和使用情况。由于 Promise 在异步编程中的重要性不言而喻,因此在很多互联网大厂面试中,现场实现 Promise 相关方法的题目经常会出现,比如 all、race 或者 any 等。

因此这一讲,我将要带你一步步实现一个符合标准的 Promise,希望你在遇到相关题目时能够游刃有余。

在课程开始前请你先思考一下:

  1. Promise/A+ 约定了哪些规范?
  2. 在手动实现 Promise 的过程中都遇到过哪些问题?

在开始手动实现 Promise 之前,你有必要先了解 Promise/A+ 的规范。

Promise/A+ 规范

只有对规范进行解读并且形成明确的认知,才能更好地实现 Promise。官方的地址为:https://promisesaplus.com/,这是一个英文的版本,我把其中比较关键的部分挑了出来。

术语

先来看看 Promise/A+ 规范的基本术语,如下所示。

“promise” is an object or function with a then method whose behavior conforms to this specification.
“thenable” is an object or function that defines a then method.
“value” is any legal JavaScript value (including undefined, a thenable, or a promise).
“exception” is a value that is thrown using the throw statement.
“reason” is a value that indicates why a promise was rejected.

翻译过来,它所描述的就是以下五点。

  1. “promise”:是一个具有 then 方法的对象或者函数,它的行为符合该规范。
  2. “thenable”:是一个定义了 then 方法的对象或者函数。
  3. “value”:可以是任何一个合法的 JavaScript 的值(包括 undefined、thenable 或 promise)。
  4. “exception”:是一个异常,是在 Promise 里面可以用 throw 语句抛出来的值。
  5. “reason”:是一个 Promise 里 reject 之后返回的拒绝原因。

状态描述

看完了术语部分,我们再看下 Promise/A+ 规范中,对 Promise 的内部状态的描述,如下所示。

A promise must be in one of three states: pending, fulfilled, or rejected.
When pending, a promise:
may transition to either the fulfilled or rejected state.
When fulfilled, a promise:
must not transition to any other state.
must have a value, which must not change.
When rejected, a promise:
must not transition to any other state.
must have a reason, which must not change.
Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.

将上述描述总结起来,大致有以下几点。

  1. 一个 Promise 有三种状态:pending、fulfilled 和 rejected。
  2. 当状态为 pending 状态时,即可以转换为 fulfilled 或者 rejected 其中之一。
  3. 当状态为 fulfilled 状态时,就不能转换为其他状态了,必须返回一个不能再改变的值。
  4. 当状态为 rejected 状态时,同样也不能转换为其他状态,必须有一个原因的值也不能改变。

then 方法

关于 then 方法的英文解读和翻译,我直接总结了出来:一个 Promise 必须拥有一个 then 方法来访问它的值或者拒绝原因。

then 方法有两个参数:

promise.then(onFulfilled, onRejected)

onFulfilled 和 onRejected 都是可选参数。

onFulfilled 和 onRejected 特性

如果 onFulfilled 是函数,则当 Promise 执行结束之后必须被调用,最终返回值为 value,其调用次数不可超过一次。而 onRejected 除了最后返回的是 reason 外,其他方面和 onFulfilled 在规范上的表述基本一样。

多次调用

then 方法其实可以被一个 Promise 调用多次,且必须返回一个 Promise 对象。then 的写法如下所示,其中 Promise1 执行了 then 的方法之后,返回的依旧是个 Promise2,然后我们拿着 Promise2 又可以执行 then 方法,而 Promise2 是一个新的 Promise 对象,又可以继续进行 then 方法调用。

  1. promise2 = promise1.then(onFulfilled, onRejected);

规范里面还有很大一部分讲解的是 Promise 的解决过程。其实只看规范的话,整体感觉很空洞,方才我已经将规范的主要部分为你讲解了,这些内容基本可以指导我们自己实现一个 Promise 了。

那么下面我们就理论结合实践,动手实现一个 Promise 吧。

一步步实现 Promise

按照 Promise/A+ 的规范,第一步就是构造函数。

构造函数

这一步的思路是:Promise 构造函数接受一个 executor 函数,executor 函数执行完同步或者异步操作后,调用它的两个参数 resolve 和 reject。请看下面的代码,大致的构造函数框架就是这样的。

  1. function Promise(executor) {
  2. var self = this
  3. self.status = 'pending'
  4. self.data = undefined
  5. self.onResolvedCallback = []
  6. self.onRejectedCallback = []
  7. executor(resolve, reject)
  8. }

从上面的代码中可以看出,我们先定义了一个 Promise 的初始状态 pending,以及参数执行函数 executor,并且按照规范设计了一个 resolve 回调函数集合数组 onResolvedCallback 以及 一个 reject 回调函数集合数组,那么构造函数的初始化就基本完成了。

接下来我们看看还需要添加什么东西呢?那就是需要在构造函数中完善 resolve 和 reject 两个函数,完善之后的代码如下。

  1. function Promise(executor) {
  2. var self = this
  3. self.status = 'pending'
  4. self.data = undefined
  5. self.onResolvedCallback = []
  6. self.onRejectedCallback = []
  7. function resolve(value) {
  8. }
  9. function reject(reason) {
  10. }
  11. try {
  12. executor(resolve, reject)
  13. } catch(e) {
  14. reject(e)
  15. }
  16. }

resolve 和 reject 内部应该怎么实现呢?我们根据规范知道这两个方法主要做的事情就是返回对应状态的值 value 或者 reason,并把 Promise 内部的 status 从 pending 变成对应的状态,并且这个状态在改变了之后是不可以逆转的。

那么这两个函数应该怎么写呢?可以看下面的这段代码。

  1. function Promise(executor) {
  2. function resolve(value) {
  3. if (self.status === 'pending') {
  4. self.status = 'resolved'
  5. self.data = value
  6. for(var i = 0; i < self.onResolvedCallback.length; i++) {
  7. self.onResolvedCallback[i](value)
  8. }
  9. }
  10. }
  11. function reject(reason) {
  12. if (self.status === 'pending') {
  13. self.status = 'rejected'
  14. self.data = reason
  15. for(var i = 0; i < self.onRejectedCallback.length; i++) {
  16. self.onRejectedCallback[i](reason)
  17. }
  18. }
  19. }
  20. }

上述代码所展示的,基本就是在判断状态为 pending 之后,把状态改为相应的值,并把对应的 value 和 reason 存在内部的 data 属性上面,之后执行相应的回调函数。逻辑比较简单,无非是由于 onResolveCallback 和 onRejectedCallback 这两个是数组,需要通过循环来执行,这里就不多解释了,你应该会知道。

好了,构造函数基本就实现了,那么我们再看看如何实现 then 方法,从而保证可以实现链式调用。

实现 then 方法

根据标准,我们要考虑几个问题。

then 方法是 Promise 执行完之后可以拿到 value 或者 reason 的方法,并且还要保持 then 执行之后,返回的依旧是一个 Promise 方法,还要支持多次调用(上面标准中提到过)。

因此 then 方法实现的思路也有了,请看下面的一段代码。

  1. Promise.prototype.then = function(onResolved, onRejected) {
  2. var self = this
  3. var promise2
  4. onResolved = typeof onResolved === 'function' ? onResolved : function(v) {}
  5. onRejected = typeof onRejected === 'function' ? onRejected : function(r) {}
  6. if (self.status === 'resolved') {
  7. return promise2 = new Promise(function(resolve, reject) {
  8. })
  9. }
  10. if (self.status === 'rejected') {
  11. return promise2 = new Promise(function(resolve, reject) {
  12. })
  13. }
  14. if (self.status === 'pending') {
  15. return promise2 = new Promise(function(resolve, reject) {
  16. })
  17. }
  18. }

从上面的代码中可以看到,我们在 then 方法内部先初始化了 Promise2 的对象,用来存放执行之后返回的 Promise,并且还需要判断 then 方法传参进来的两个参数必须为函数,这样才可以继续执行。

上面我只是搭建了 then 方法框架的整体思路,但是不同状态的返回细节处理也需要完善,通过仔细阅读标准,完善之后的 then 的代码如下。

  1. Promise.prototype.then = function(onResolved, onRejected) {
  2. var self = this
  3. var promise2
  4. onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
  5. onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}
  6. if (self.status === 'resolved') {
  7. return promise2 = new Promise(function(resolve, reject) {
  8. try {
  9. var x = onResolved(self.data)
  10. if (x instanceof Promise) {
  11. x.then(resolve, reject)
  12. }
  13. resolve(x)
  14. } catch (e) {
  15. reject(e)
  16. }
  17. })
  18. }
  19. if (self.status === 'rejected') {
  20. return promise2 = new Promise(function(resolve, reject) {
  21. try {
  22. var x = onRejected(self.data)
  23. if (x instanceof Promise) {
  24. x.then(resolve, reject)
  25. }
  26. } catch (e) {
  27. reject(e)
  28. }
  29. })
  30. }
  31. if (self.status === 'pending') {
  32. return promise2 = new Promise(function(resolve, reject) {
  33. self.onResolvedCallback.push(function(value) {
  34. try {
  35. var x = onResolved(self.data)
  36. if (x instanceof Promise) {
  37. x.then(resolve, reject)
  38. }
  39. } catch (e) {
  40. reject(e)
  41. }
  42. })
  43. self.onRejectedCallback.push(function(reason) {
  44. try {
  45. var x = onRejected(self.data)
  46. if (x instanceof Promise) {
  47. x.then(resolve, reject)
  48. }
  49. } catch (e) {
  50. reject(e)
  51. }
  52. })
  53. })
  54. }
  55. }

根据上面的代码可以看出,我们基本实现了一个符合标准的 then 方法。但是标准里提到了,还要支持不同的 Promise 进行交互,关于不同的 Promise 交互其实Promise 标准说明中有提到。其中详细指定了如何通过 then 的实参返回的值来决定 Promise2 的状态。

关于为何需要不同的 Promise 实现交互,原因应该是 Promise 并不是 JS 一开始存在的标准,如果你使用的某一个库中封装了一个 Promise 的实现,想象一下如果它不能跟你自己使用的 Promise 实现交互的情况,其实还是会有问题的,因此我们还需要调整一下 then 方法中执行 Promise 的方法。

另外还有一个需要注意的是,在 Promise/A+ 规范中,onResolved 和 onRejected 这两项函数需要异步调用,关于这一点,标准里面是这么说的:

In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.

所以我们需要对代码做一点变动,即在处理 Promise 进行 resolve 或者 reject 的时候,加上 setTimeout(fn, 0)。

下面我就结合上面两点调整,给出完整版的代码,你可以根据注释关注一下我所做的调整。

  1. try {
  2. module.exports = Promise
  3. } catch (e) {}
  4. function Promise(executor) {
  5. var self = this
  6. self.status = 'pending'
  7. self.onResolvedCallback = []
  8. self.onRejectedCallback = []
  9. function resolve(value) {
  10. if (value instanceof Promise) {
  11. return value.then(resolve, reject)
  12. }
  13. setTimeout(function() {
  14. if (self.status === 'pending') {
  15. self.status = 'resolved'
  16. self.data = value
  17. for (var i = 0; i < self.onResolvedCallback.length; i++) {
  18. self.onResolvedCallback[i](value)
  19. }
  20. }
  21. })
  22. }
  23. function reject(reason) {
  24. setTimeout(function() {
  25. if (self.status === 'pending') {
  26. self.status = 'rejected'
  27. self.data = reason
  28. for (var i = 0; i < self.onRejectedCallback.length; i++) {
  29. self.onRejectedCallback[i](reason)
  30. }
  31. }
  32. })
  33. }
  34. try {
  35. executor(resolve, reject)
  36. } catch (reason) {
  37. reject(reason)
  38. }
  39. }
  40. function resolvePromise(promise2, x, resolve, reject) {
  41. var then
  42. var thenCalledOrThrow = false
  43. if (promise2 === x) {
  44. return reject(new TypeError('Chaining cycle detected for promise!'))
  45. }
  46. if (x instanceof Promise) {
  47. if (x.status === 'pending') {
  48. x.then(function(v) {
  49. resolvePromise(promise2, v, resolve, reject)
  50. }, reject)
  51. } else {
  52. x.then(resolve, reject)
  53. }
  54. return
  55. }
  56. if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
  57. try {
  58. then = x.then
  59. if (typeof then === 'function') {
  60. then.call(x, function rs(y) {
  61. if (thenCalledOrThrow) return
  62. thenCalledOrThrow = true
  63. return resolvePromise(promise2, y, resolve, reject)
  64. }, function rj(r) {
  65. if (thenCalledOrThrow) return
  66. thenCalledOrThrow = true
  67. return reject(r)
  68. })
  69. } else {
  70. resolve(x)
  71. }
  72. } catch (e) {
  73. if (thenCalledOrThrow) return
  74. thenCalledOrThrow = true
  75. return reject(e)
  76. }
  77. } else {
  78. resolve(x)
  79. }
  80. }
  81. Promise.prototype.then = function(onResolved, onRejected) {
  82. var self = this
  83. var promise2
  84. onResolved = typeof onResolved === 'function' ? onResolved : function(v) {
  85. return v
  86. }
  87. onRejected = typeof onRejected === 'function' ? onRejected : function(r) {
  88. throw r
  89. }
  90. if (self.status === 'resolved') {
  91. return promise2 = new Promise(function(resolve, reject) {
  92. setTimeout(function() {
  93. try {
  94. var x = onResolved(self.data)
  95. resolvePromise(promise2, x, resolve, reject)
  96. } catch (reason) {
  97. reject(reason)
  98. }
  99. })
  100. })
  101. }
  102. if (self.status === 'rejected') {
  103. return promise2 = new Promise(function(resolve, reject) {
  104. setTimeout(function() {
  105. try {
  106. var x = onRejected(self.data)
  107. resolvePromise(promise2, x, resolve, reject)
  108. } catch (reason) {
  109. reject(reason)
  110. }
  111. })
  112. })
  113. }
  114. if (self.status === 'pending') {
  115. return promise2 = new Promise(function(resolve, reject) {
  116. self.onResolvedCallback.push(function(value) {
  117. try {
  118. var x = onResolved(value)
  119. resolvePromise(promise2, x, resolve, reject)
  120. } catch (r) {
  121. reject(r)
  122. }
  123. })
  124. self.onRejectedCallback.push(function(reason) {
  125. try {
  126. var x = onRejected(reason)
  127. resolvePromise(promise2, x, resolve, reject)
  128. } catch (r) {
  129. reject(r)
  130. }
  131. })
  132. })
  133. }
  134. }
  135. Promise.prototype.catch = function(onRejected) {
  136. return this.then(null, onRejected)
  137. }
  138. Promise.deferred = Promise.defer = function() {
  139. var dfd = {}
  140. dfd.promise = new Promise(function(resolve, reject) {
  141. dfd.resolve = resolve
  142. dfd.reject = reject
  143. })
  144. return dfd
  145. }

上面这段代码就是通过一步步优化调整出来的最终版,其中细节也是比较多的,介于篇幅问题,暂时能讲的点就先说这么多。如果你还有哪里不清楚的,最好还是动手实践去理解。

最终版的 Promise 的实现还是需要经过规范的测试(Promise /A+ 规范测试的工具地址为:https://github.com/promises-aplus/promises-tests),需要暴露一个 deferred 方法(即 exports.deferred 方法),上面提供的代码中我已经将其加了进去。

最后,执行如下代码 npm 安装之后,即可执行测试。

  1. npm i -g promises-aplus-tests
  2. promises-aplus-tests Promise.js

总结

讲到这里,你可以再思考一下 Promise /A+ 规范中的一些细节,以及实现过程中需要注意的问题,如果你能够在面试中实现一个这样的 Promise,就基本可以满足岗位的需求了。

通过这一讲的学习,你应该能理解 Promise 底层的实现逻辑了,虽然并不一定有场景适合落地业务,但是整个手动实现的过程对于你 JS 编码能力的提升会很有帮助。

关于 Promise 还有很多 API 的方法,在已经实现的 Promise 的版本之上,你可以再尝试实现 all、race 以及 any 等方法,学会举一反三,才能在工作中游刃有余。

下一讲开始,我们就要进入一个新的模块——JS 引擎篇,我要带你了解 JS 引擎中的垃圾回收机制、事件轮询等,这对于写出高性能的 JavaScript 代码有着很好的帮助。

我们到时见。