深入理解Promise
一位高端的前端攻城狮曾经说过,一位不愿意透露姓名的顶级摸鱼工程师曾经说过,学习 Promise 最好的方式就是先阅读它的规范定义 。那么哪里可以找到 Promise 的标准定义呢?
答案是 Promises/A+ 规范 。
假设你已经打开了上述的规范定义的页面并尝试开始阅读(不要因为是英文的就偷偷关掉,相信自己,你可以的),规范在开篇描述了 Promise 的定义,与之交互的方法,然后强调了规范的稳定性。关于稳定性,换言之就是:我们可能会修订这份规范,但是保证改动微小且向下兼容,所以放心地学吧,这就是权威标准,五十年之后你再去谷歌 Promise,出来的规范还是这篇 😂。
好的,让我们回到规范。从开篇的介绍看,到底什么是 Promise ?
A promise represents the eventual result of an asynchronous operation.
Promise就是表示一个异步操作的最终结果
划重点!!这里其实引出了 JavaScript 引入 Promise 的动机:异步 。
学习一门新技术,最好的方式是先了解它是如何诞生的,以及它所解决的问题是什么。Promise 跟我们说的异步编程有什么联系呢?Promise 到底解决了什么问题?
要回答这些问题,我们需要先回顾下没有 Promise 之前,异步编程存在什么问题?
异步编程
由于web是单线程的架构,决定了javascript的异步编程模型是基于消息队列 和事件循环
看图说话
我们的异步任务的回调函数会被放入消息队列,然后等待主线程上的同步任务执行完成,执行栈为空时,由事件循环机制调度进执行栈继续执行。
这导致了 JavaScript 异步编程的一大特点:异步回调 ,比如网络请求,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function resolve (response ) { console .log (response); }function reject (error ) { console .log (error); }var xhr = new XMLHttpRequest (); xhr.onreadystatechange = () => resolve (xhr.response ); xhr.ontimeout = (e ) => reject (e); xhr.onerror = (e ) => reject (e); xhr.open ("Get" , "http://xxx" ); xhr.send ();
虽然可以通过简单的封装使得异步回调的方式变得优雅一些,比如,
1 2 3 4 5 6 $.ajax ({ url : "https://xxx" , method : "GET" , fail : () => {}, success : () => {}, });
但是仍然没有办法解决业务复杂后的“回调地狱”的问题,比如多个依赖请求,
1 2 3 4 5 6 7 8 9 10 11 12 13 $.ajax ({ success : function (res1 ) { $.ajax ({ success : function (res2 ) { $.ajax ({ success : function (res3 ) { }, }); }, }); }, });
这种线性的嵌套回调使得异步代码变得难以理解和维护,也给人很大的心智负担。
所以我们需要一种技术,来解决异步编程风格的问题 ,这就是 Promise 的动机。
了解 Promise 背景和动机有利于我们理解规范,现在让我们重新回到规范的定义
规范
Promise A+ 规范首先定义了 Promise 的一些相关术语和状态。
Terminology,术语
“promise” ,一个拥有 then
方法的对象或函数,其行为符合本规范
“thenable”,一个定义了 then
方法的对象或函数
“value”,任何 JavaScript 合法值(包括 undefined
, thenable
和 promise
)
“exception”,使用 throw
语句抛出的一个值
“reason”,表示一个 promise
的拒绝原因
State,状态
promise 的当前状态必须为以下三种状态之一:Pending
, Fulfilled
, Rejected
处于 Pending 时,promise 可以迁移至 Fullfilled 或 Rejected
处于 Fulfilled 时,promise 必须拥有一个不可变的终值且不能迁移至其他状态
处于 Rejected 时,promise 必须拥有一个不可变的拒绝原因且不能迁移至其他状态
所以 Promise 内部其实维护了一个类似下图所示的状态机
Promise 在创建时处于 Pending(等待态),之后可以变为 Fulfilled(执行态)或者 Rejected(拒绝态),一个承诺要么被兑现,要么被拒绝,这一过程是不可逆的。
定义了相关的术语和状态后,是对 then
方法执行过程的详细描述。
Then
一个 promise 必须提供一个 then
方法以访问其当前值、终值和拒绝原因。
then
方法接受两个参数
1 promise.then (onFulfilled, onRejected);
onFulfilled,在 promise 执行结束后调用,第一个参数为 promise 的终值
onRejected,在 promise 被拒绝执行后调用,第一个参数为 promise 的拒绝原因
对于这两个回调参数和 then
的调用及返回值,有如下的一些规则,
onFulfilled 和 onRejected 都是可选参数。
onFulfilled 和 onRejected 必须作为函数被调用,调用的 this
应用默认绑定规则,也就是在严格环境下,this
等于 undefined
,非严格模式下是全局对象(浏览器中就是 window
)。关于 this
的绑定规则如果不了解的可以参考我之前的一篇文章 《可能是最好的 this 解析了…》 ,里面有非常详细地介绍。
onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用。由于 promise 的实施代码本身就是平台代码(JavaScript),这个规则可以这么理解:就是要确保这两个回调在 then 方法被调用的那一轮事件循环之后异步执行。这不就是微任务的执行顺序吗?所以 promise 的实现原理是基于微任务队列的。
then
方法可以被同一个 promise 调用多次,而且所有的成功或拒绝的回调需按照其注册顺序依次回调。所以 promise 的实现需要支持链式调用,可以先想一下怎么支持链式调用,稍后我们会有对应的实现。
then
方法必须返回一个 promise 对象。
针对第 5 点,还有如下几条扩展定义,我们将返回值与 promise 的解决过程结合起来,
1 promise2 = promise1.then (onFulfilled, onRejected);
then
的两个回调参数可能会抛出异常或返回一个值,
5.1 如果 onFulfilled 或者 onRejected 抛出一个异常 e
,那么返回的 promise2 必须拒绝执行,并返回拒绝的原因 e
。
5.2 如果 onFulfilled 或者 onRejected 返回了一个值 x
,会执行 promise 的解决过程
如果 x
和返回的 promise2 相等,也就是 promise2 和 x
指向同一对象时,以 TypeError
作为拒绝的原因拒绝执行 promise2
如果 x
是 promise,会判断 x
的状态。如果是等待态,保持;如果是执行态,用相同的值执行 promise2;如果是拒绝态,用相同的拒绝原因拒绝 promise2
如果 x
是对象或者函数,将 x.then
赋值给 then
;如果取 x.then
的值时抛出错误 e
,则以 e
为拒绝原因拒绝 promise2。如果 then
是函数,将 x
作为函数的 this
,并传递两个回调函数 resolvePromise, rejectPromise 作为参数调用函数
读到这里,相信你跟我一样已经迫不及待想要实现一个 Promise 了,既然了解了原理和定义,我们就来手写一个 Promise 吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 const PENDING = "PENDING" ;const FULFILLED = "FULFILLED" ;const REJECTED = "REJECTED" ;function resolve (value ) { return value; }function reject (err ) { throw err; }function resolvePromise (promise2, x, resolve, reject ) { if (promise2 === x) { return reject ( new TypeError ("Chaining cycle detected for promise #<Promise>" ) ); } let called; if ((typeof x === "object" && x != null ) || typeof x === "function" ) { try { let then = x.then ; if (typeof then === "function" ) { then.call ( x, (y ) => { if (called) return ; called = true ; resolvePromise (promise2, y, resolve, reject); }, (r ) => { if (called) return ; called = true ; reject (r); } ); } else { resolve (x); } } catch (e) { if (called) return ; called = true ; reject (e); } } else { resolve (x); } }class Promise { constructor (executor ) { this .status = PENDING ; this .value = undefined ; this .reason = undefined ; this .resolveCallbacks = []; this .rejectCallbacks = []; let resolve = (value ) => { if (this .status === PENDING ) { this .status = FULFILLED ; this .value = value; this .resolveCallbacks .forEach ((fn ) => fn ()); } }; let reject = (reason ) => { if (this .status === PENDING ) { this .status = REJECTED ; this .reason = reason; this .rejectCallbacks .forEach ((fn ) => fn ()); } }; try { executor (resolve, reject); } catch (error) { reject (error); } } then (onFulfilled, onRejected ) { onFulfilled = typeof onFulfilled === "function" ? onFulfilled : resolve; onRejected = typeof onRejected === "function" ? onRejected : reject; let promise2 = new Promise ((resolve, reject ) => { if (this .status === FULFILLED ) { setTimeout (() => { try { let x = onFulfilled (this .value ); resolvePromise (promise2, x, resolve, reject); } catch (e) { reject (e); } }, 0 ); } if (this .status === REJECTED ) { setTimeout (() => { try { let x = onRejected (this .reason ); resolvePromise (promise2, x, resolve, reject); } catch (e) { reject (e); } }, 0 ); } if (this .status === PENDING ) { this .resolveCallbacks .push (() => { setTimeout (() => { try { let x = onFulfilled (this .value ); resolvePromise (promise2, x, resolve, reject); } catch (e) { reject (e); } }, 0 ); }); this .rejectCallbacks .push (() => { setTimeout (() => { try { let x = onRejected (this .reason ); resolvePromise (promise2, x, resolve, reject); } catch (e) { reject (e); } }, 0 ); }); } }); return promise2; } }