本文旨在通过实现一个 Promise 类来深入理解 Promise 的运行机制,虽然会提到一些 Promise 的基本概念,但最好先有一定了解, 可通过 MDN 进行查询
术语
由于对 Reolve、Fulfill、Reject 之类的术语并没有十分统一的中文译名,本文将保留其英文名
Promise 的本质是一个状态机
Promise 总共具有三个状态:Pending、Fulfilled、Rejected。在创建 Promise 对象时,会传入一个由开发者定义的函数 fn。开发者在其中自由的编写业务逻辑,可以是文件读取、网络请求,并通过调用 fn 的两个参数来控制 Promise 改变状态的实际。 Promise 本质就是一个这样的辅助工具,让开发者专注于业务逻辑。
我们可以在 Chrome Devtool 中 print promise 对象来查看它当前的状态和终值(Eventual Value)
_10Promise.resolve(1)_10> Promise {<fulfilled>: 1}
所以实现 Promise 第一步就是实现一个状态机,定义这些状态,与转换状态的方法
_21class MyPromise {_21 #state_21 #val // promise_21_21 constructor(fn) {_21 this.#state = 'pending'_21 this.val = null_21_21 fn(this.#fulfill, this.#reject)_21 }_21_21 #fulfill = (value) => {_21 this.#state = 'fulfilled'_21 this.val = value_21 }_21_21 #reject = (value) => {_21 this.#state = 'rejected'_21 this.val = value_21 }_21}
.then
函数
Promise 允许开发者在任意时间添加对 Promise 状态改变的监听,通过调用.then(onFullfilled, OnRejected)
并传入两个回调函数实现。
注意开发者可多次调用.then()
函数,如:
_10const p = Promise.resolve('resolved')_10p.then((v) => console.log(v))_10p.then((v) => console.log('Yeah!'))_10_10> resolved_10> Yeah!
因此我们需要就这些 Handler 一次存储起来,在状态改变时依次调用。
_62function addToTaskQueue(fn) {_62 // 模拟浏览器微任务_62 setTimeout(fn, 0)_62}_62_62class MyPromise {_62 #state_62 #value // promise_62 #handlers = []_62_62 constructor(fn) {_62 this.#state = 'pending'_62 this.#value = null_62_62 fn(this.#fulfill, this.#reject)_62 }_62_62 #fulfill = (value) => {_62 this.#state = 'fulfilled'_62 this.#value = value_62 this.#handlers.forEach((handler) => {_62 handler.onFulfilled(this.#val)_62 })_62 this.#handlers = null // garbage collecting_62 }_62_62 #reject = (value) => {_62 this.#state = 'rejected'_62 this.#value = value_62 this.#handlers.forEach((handler) => {_62 handler.onRejected(this.#val)_62 })_62 this.#handlers = null // garbage collecting_62 }_62 then = (onFulfilled, onRejected) => {_62 addToTaskQueue(() => {_62 if (this.#state === 'pending') {_62 this.#handlers.push({ onFulfilled, onRejected })_62 }_62 if (this.#state === 'fulfilled') {_62 onFulfilled(this.#value)_62 }_62 if (this.#state === 'rejected') {_62 onRejected(this.#value)_62 }_62 })_62 }_62}_62_62// Test Code_62const p = new MyPromise((resolve, reject) => {_62 setTimeout(() => resolve('resolved!'), 1000)_62})_62p.then(_62 (res) => {_62 console.log(res)_62 },_62 (err) => {_62 console.log(err)_62 }_62)_62console.log('global')
由于 Promise 状态的改变可能在调用then
之前或之后,所以需要在多处进行判断。
在进行下一步之前,对代码的通用部分进行复用,添加类型检查:
_47class MyPromise {_47 #state_47 #value_47 #handlers_47_47 constructor(fn) {_47 this.#state = 'pending'_47 this.#value = null_47 this.#handlers = []_47 fn(this.#fulfill, this.#reject)_47 }_47_47 #fulfill = (value) => {_47 this.#state = 'fulfilled'_47 this.#value = value_47 this.#handlers.forEach(this.#handle)_47 this.#handlers = null // garbage collecting_47 }_47_47 #reject = (err) => {_47 this.#state = 'rejected'_47 this.#value = err_47 this.#handlers.forEach(this.#handle)_47 this.#handlers = null // garbage collecting_47 }_47_47 #handle = (handler) => {_47 if (this.#state === 'pending') {_47 this.#handlers.push(handler)_47 }_47 if (this.#state === 'fulfilled') {_47 handler.onFulfilled(this.#value)_47 }_47 if (this.#state === 'rejected') {_47 handler.onRejected(this.#value)_47 }_47 }_47_47 then = (onFulfilled, onRejected) => {_47 addToTaskQueue(() => {_47 this.#handle({_47 onFulfilled,_47 onRejected,_47 })_47 })_47 }_47}
.then
的链式调用
Promise 的一个重要特性,就是允许链式的调用,从而一定程度上避免了回调地狱的发生。为了做到这一点,我们需要将回调函数 onFulfilled/OnRejected 的返回值穿透到最外层,即创建并返回一个以回调函数返回值为内部状态的 Promise,以便进一步调用后续的.then
方法。
_34then = (onFulfilled, onRejected) => {_34 const nextPromise = new MyPromise((resolve, reject) => {_34 const fullfillmentTask = () => {_34 const value =_34 typeof onFulfilled === 'function' ? onFulfilled(this.#value) : this.#value_34 resolve(value)_34 }_34 const rejectionTask = () => {_34 const value =_34 typeof onRejected === 'function' ? onRejected(this.#value) : this.#value_34 resolve(value)_34 }_34_34 addToTaskQueue(() => {_34 this.#handle({_34 onFulfilled: fullfillmentTask,_34 onRejected: rejectionTask,_34 })_34 })_34 })_34_34 return nextPromise_34}_34_34// test_34const p = new MyPromise((resolve, reject) => {_34 setTimeout(() => resolve('resolved!'), 1000)_34})_34p.then((res) => {_34 return res + '!!'_34}).then((res) => {_34 console.log(res)_34})_34console.log('global')
允许.then
回调函数返回 Promise
有的时候我们会希望回调函数返回一个 Promise,并当它 resolved 之后再触发之后 then 链中的函数回调,比如我们需要先发起一个请求获取一个 Id,再根据返回的 Id 请求其他数据。如:
_10const follower = fetch('/followers')_10const p = follower.then((ids) => {_10 const user = fetch(`/api/user/${id[0]}`)_10 return user_10})
此时,除follower
外存在着两个 Promise,一个是user
,一个是.then
方法需要返回并赋值给 p 的 Promise。它们的状态和终值应该是相互锁定的,即当第一个 Promise 状态改变时,第二个 Promise 也随之发生相同变化。因此我们需要扩展一下我们的fulfill
方法
_23class MyPromise {_23 constructor(fn) {_23 this.#state = 'pending'_23 this.#value = null_23 this.#handlers = []_23 fn(this.#resolve, this.#reject)_23 }_23_23 #resolve = (value) => {_23 if (isPromise(value)) {_23 value.then(_23 (val) => {_23 this.#resolve(val)_23 },_23 (err) => {_23 this.#reject(err)_23 }_23 )_23 } else {_23 this.#fulfill(value)_23 }_23 }_23}
让我们来捋一捋当user
的这个 Promise 从服务器得到数据,状态改变前后都发生了什么:
- 调用
.then
,将 fullfillmentTask 加入微任务队列, 返回新 Promise p。 - 浏览器执行完主函数,开始执行微任务 fullfillmentTask。假设此时
follower
已完成,则立即执行.then
传入的回调函数(ids) => { return fetch(`/api/user/${id[0]}`) }
,并将返回值传给 p 的resolve
方法执行 - 由于返回值是一个 Promise,于是给它挂上回调函数,来监控其状态变化。注意此时 this 的上下文为 p
- 某一时刻,
user
fetch 从服务器得到数据 - 执行 3 中挂上的回调
(val) => { this.#revolve(val) }
, 调用 p 的this.#revolve
- 由于 p 没有后续的 then 链,所以无后续回调函数需要执行,p 内部状态变为
fulfilled
, 终值为user
的终值
Note
对于实际的 Promise 来说,返回值不仅可以是 Promise 对象,还可以是更广泛意义上的 Thenable Object,即任何带有then
方法的对象
_10function isThenable(value) {_10 return typeof value?.then === 'function'_10}
异常的集中处理
Promise 相比之前的一大优势,就是不再需要对每一步异步操作添加异常处理。只需要在 then 链的末尾添加就可以集中处理整个链上的异常
另外,由于在内部 Promise 会调用外部的函数,所以在使用的过程中应该进行一些保护,如异常处理。同时对于resolve
和reject
,最多只允许其中之一执行一次
_44class MyPromise {_44 constructor(fn) {_44 this.#state = 'pending'_44 this.#value = null_44 this.#handlers = []_44 this.#safeRun(fn, this.#resolve, this.#reject)_44 }_44_44 #safeRun = (fn, onFulfilled, onRejected) => {_44 let done_44 try {_44 fn(_44 (val) => {_44 if (done) {_44 return_44 }_44 done = true_44 onFulfilled(val)_44 },_44 (val) => {_44 if (done) {_44 return_44 }_44 done = true_44 onRejected(val)_44 }_44 )_44 } catch (err) {_44 if (done) {_44 return_44 }_44 done = true_44 onRejected(err)_44 }_44 }_44_44 #resolve = (value) => {_44 if (isThenable(value)) {_44 this.#safeRun(value.then, this.#resolve, this.#reject)_44 } else {_44 this.#fulfill(value)_44 }_44 }_44}
总结
到此,我们实现一个 Promise 所需的核心功能,完整代码请参见gist。