欢送来这里 前端杂谈, 聊聊前端
代码在github
《手写 Promise》是一个经典的问题,基本上大家上手都能够依照本人的了解,写进去一个 promise, 有一天个敌人问我,"手写 Promise 要写到什么水平才是合格的 ?", 这也引起了我的趣味和思考, "怎么样的 Promise ,才是完满的呢 ? "
完满的 Promise
第一个问题就是怎么样才算是一个完满的 Promise 呢, 其实这个问题也不难,实现一个和原生 Promise "雷同"的 Promsie,不就是完满的了, 那么第二个问题也就来了,原生的 Promise 是依照什么规范来实现的呢, 查阅了材料之后晓得是依照 [Promises/A+] (https://promisesaplus.com/)规范来实现的, 具体的实现在 ECMA - sec-promise-objects 上有记录, 当初规范有了,咱们就能够来实现一个"完满的 Promise"了
Promises/A+
接下来咱们来看看Promises/A+
规范说了啥, 次要是两局部,一个是名词定义,一个是规范形容,其中规范形容由三个局部组成, 接下来咱们简略介绍下:
Terminology
这部分是名词定义,次要是形容了各个名词在规范中的定义
promise
: 是具备then
行为符合规范的办法的object
或function
, 这里须要留神的是不是function
是then
,是function
中有then
办法thenable
: 是定义then
办法的object
或函数
,这个和下面promise
的区别在于then
是一个函数,不肯定须要符合规范行为value
: 是任何非法的 javascript 值,包含undefined
、thenable
、promise
,这里的value
蕴含了thenable
和promise
,联合上面的标准,会发现是一个可嵌套的关系exception
: 是一个通过throw
关键词抛出来的值reason
: 示意一个promise
状态是rejected
的起因
Requirements
这部分是规范的定义,分为以下三个局部
Promise States
一个promise
必须是以下三种状态之一
pending
- 能够转变成
fulfilled
或者rejected
状态
- 能够转变成
fulfilled
- 须要存在一个
value
- 须要存在一个
rejected
- 须要存在一个
reason
- 须要存在一个
当状态是fulfilled
或者 rejected
时,状态不能够再变动成其余状态,而value
和reason
也不能够再变动
The then
Method
这部分定义了 promise
中 then
办法的行为,then
办法是用来拜访promise
状态变成fulfilled
或者 rejected
的value
或者reason
的, then
有两个参数,如下:
promise.then(onFulfilled,onRejected)
onFulfilled
/onRejected
- 都是可选参数,如果这两个参数不是函数类型,那么疏忽
- 在
promise
状态变成fulfilled
/rejected
之后被调用,会带上value
/reason
作为函数的参数 - 只会被调用一次
- 须要在
宏工作
或者微工作
事件循环中实现。 注: 这里对于执行机会的形容比拟乏味,能够看看文档 2.2.4 - 两个函数须要被绑定在
global this
上运行
- 同一个 Promise能够被屡次
then
调用,then
中的onFulfilled
和onRejected
必须依照then
的调用顺序调用 then
函数调用之后须要返回一个promise
, 这也是promise
能够链式调用then
的根底promise2 = promise1.then(onFulfilled,onRejected)
- 如果
onFulfilled
或者onRejected
函数返回了值x
, 则运行 Promise Resolution Procedure - 如果
onFulfilled
或者onRejected
抛出谬误e
, 则promise2
的状态是rejected
,并且reason
是e
- 如果
onFulfilled
或者onRejected
不是一个函数,而且promise1
的状态曾经确定fulfilled/rejected
, 则promise2
- 如果
The Promise Resolution Procedure
其实大体的规范局部在Promise States
和 The then Method
曾经形容完了,这部分次要规定了一个形象的操作promise resolution procedure
, 用来形容当then
的 onFulfilled
或者onRejected
返回值x
时,须要怎么样去进行操作,把表达式记为[[Resolve]](promise,x)
, 这部分也是整个 Promise 实现最简单的局部,咱们一起看看他规定了什么
[[Resolve]](promise,x)
当
promise
和x
是同一个对象时,promise
为rejected
,reason
是TypeError
const promise = Promise.resolve().then(()=>promise); // TypeError
- 如果
x
是一个Promise时,则promise
的状态要与x
同步 如果
x
是一个object
或者一个function
, 这部分是最简单的- 首先要把
x.then
存储在一个两头变量then
, 为什么要这么做能够看文档 3.5,而后依据不同条件进行解决 - 如果获取
x.then
的时候就抛出谬误e
,则promise
状态变成rejected
,reason
是e
如果
then
是一个函数,那么这就是咱们定义外面的thenable
, 这时候绑定x
为 this并调用then
,传入promise
的resolvePromise
和rejectPromise
作为两个参数then.call(x, resolvePromise, rejectPromise)
接下来判断调用的后果
- 如果
resolvePromise
被调用,value
是y
, 则调用[[Resolve]](promise,y)
- 如果
rejectPromise
被调用,reason
是e
, 则promise
状态变成rejected
,reason
是e
- 如果
resolvePromise
和rejectPromise
都被调用,则以第一个调用会准,后续的调用都被疏忽 如果调用过程中抛出了谬误
e
- 如果抛出之前
resolvePromise
或者rejectPromise
曾经被调用了,那么就疏忽谬误 - 后者的话,则
promise
状态变成rejected
,reason
是e
- 如果抛出之前
- 如果
- 如果
then
不是一个函数,那么promise
状态变成fulfilled
,value
是x
- 首先要把
- 如果
x
不是一个object
或者function
, 则promise
状态变成fulfilled
,value
是x
这外面最简单的就是在 resolvePromise
被调用,value
是y
这部分,实现的是thenable
的递归函数
下面就是如何实现一个"完满"的 Promise 的标准了,总的来说比较复杂的是在The Promise Resolution Procedure
和对于谬误和调用边界的状况,上面咱们将开始入手,实现一个"完满"的Promise
如何测试你的 Promise
后面介绍了 Promise/A+
标准, 那么如何测试你的实现是齐全实现了标准的呢, 这里Promise/A+
提供了 [promises-tests
](https://github.com/promises-a...), 外面目前蕴含872个测试用例,用于测试 Promise 是否规范
注释开始
首先阐明下这边是依照已实现的代码对实现 promise 进行介绍代码在这里, 这里应用的是最终版本,外面正文大抵表明了实现的规定编号,其实整体来说通过了很多批改,如果要看整个便携过程,能够commit history, 关注promise_2.js
和promise.js
两个文件
编写的关键点
整体的实现思路次要就是下面的标准了,当然咱们也不是说逐条进行实现,而是对标准进行分类,对立去实现:
- promise的状态定义及转变规定和根底运行
- then的实现
- onFulfilled和onRejected的执行及执行机会
- thenable的解决
- promise和then及thenable中对于谬误的解决
resolve
和reject
函数的调用次数问题
promise的状态定义及转变规定和根底运行
const Promise_State = { PENDING: "pending", FULFILLED: "fulfilled", REJECTED: "rejected",};class MyPromise { constructor(executerFn) { this.state = Promise_State.PENDING; this.thenSet = []; try { executerFn(this._resolveFn.bind(this), this._rejectedFn.bind(this)); } catch (e) { this._rejectedFn.call(this, e); } }}
在构造函数中初始化状态为pending
,并且运行传入构造函数的executerFn
,传入resovlePromise
、rejectePromise
两个参数
而后咱们接下去就要实现 resolvePromise
,rejectPromise
这两个函数
_resolveFn(result) { // 2.1.2 if (this._checkStateCanChange()) { this.state = Promise_State.FULFILLED; this.result = result; this._tryRunThen(); } } _rejectedFn(rejectedReason) { //2.1.3 if (this._checkStateCanChange()) { this.state = Promise_State.REJECTED; this.rejectedReason = rejectedReason; this._tryRunThen(); } } _checkStateCanChange() { //2.1.1 return this.state === Promise_State.PENDING; }
这里次要是通过_checkStateCanChange
判断是否可执行的状态,而后进行状态变更,value
、reason
的赋值,而后尝试运行then
办法注册的函数
这时候咱们的promise 曾经能够这么调用了
const p = new MyPromise((resolve,reject)=>{ resolve('do resolve'); // reject('do reject');});
then的实现
接下来咱们实现then
函数,首先有个简略的问题: 『then办法是什么时候执行的?』,有人会答复,是在 promise 状态变成resolve
或者rejected
的之后执行的,这个乍一看如同没故障,然而其实是有故障的,正确的说法应该是
『then办法是立刻执行的,then办法传入的onFulfilled
、onRejected
参数会在 promise 状态变成resolve
或者rejected
后执行
咱们先上代码
then(onFulfilled, onRejected) { const nextThen = []; const nextPromise = new MyPromise((resolve, reject) => { nextThen[1] = resolve; nextThen[2] = reject; }); nextThen[0] = nextPromise; //2.2.6 this.thenSet.push([onFulfilled, onRejected, nextThen]); this._runMicroTask(() => this._tryRunThen()); return nextThen[0]; }
代码看起来也挺简略的,次要逻辑就是结构一个新的 promise,而后把 onFulfilled
、onRejected
还有新结构的 promise 的resolve
、reject
存储到thenSet
汇合中,而后返回这个新构建的promise, 这时候咱们的代码曾经能够这样子调用
const p = new MyPromise((resolve,reject)=>{ resolve('do resolve'); // reject('do reject');});p.then((value)=>{ console.log(`resolve p1 ${value}`);},(reason)=>{ console.log(`reject p1 ${reason}`);}).then((value)=>console.log(`resolve pp1 ${value}`));p.then((value)=>{ console.log(`resolve p2 ${value}`);},(reason)=>{ console.log(`reject p2 ${reason}`);});
onFulfilled和onRejected的执行及执行机会
onFulFilled
和onRejected
会在 promise 状态变成fulfilled
或者rejected
之后被调用,联合then
办法被调用的机会,判断时候状态能够调用须要在两个中央做
- 在
resolvePromise
、resolvePromise
被调用的时候(判断是否有调用了then注册了onFulfilled
和onRejected
) 在
then
函数被调用的时候(判断是否 promise状态曾经变成了fulfilled
或rejected
)这两个时机会调用以下函数
_tryRunThen() { if (this.state !== Promise_State.PENDING) { //2.2.6 while (this.thenSet.length) { const thenFn = this.thenSet.shift(); if (this.state === Promise_State.FULFILLED) { this._runThenFulfilled(thenFn); } else if (this.state === Promise_State.REJECTED) { this._runThenRejected(thenFn); } } }}
这里会判断时候须要调用
then
注册的函数,而后依据 promise 的状态将thenSet
中的函数进行对应的调用
_runThenFulfilled(thenFn) { const onFulfilledFn = thenFn[0]; const [resolve, reject] = this._runBothOneTimeFunction( thenFn[2][1], thenFn[2][2] ); if (!onFulfilledFn || typeOf(onFulfilledFn) !== "Function") { // 2.2.73 resolve(this.result); } else { this._runThenWrap( onFulfilledFn, this.result, thenFn[2][0], resolve, reject ); } }
_runThenFulfilled
和_runThenRejected
类似,这里就通过一个进行解说,
首先咱们判断onFulfilled
或者onRejected
的合法性
- 如果不非法则不执行,间接将promise 的
value
或reason
透传给之前返回给then
的那个 promise,这个时候相当于then
的 promise 的状态和原来的 promise 的状态雷同 - 如果非法,则执行
onFulfilled
或者onRejected
_runThenWrap(onFn, fnVal, prevPromise, resolve, reject) { this._runMicroTask(() => { try { const thenResult = onFn(fnVal); if (thenResult instanceof MyPromise) { if (prevPromise === thenResult) { //2.3.1 reject(new TypeError()); } else { //2.3.2 thenResult.then(resolve, reject); } } else { // ... thenable handler code // 2.3.3.4 // 2.3.4 resolve(thenResult); } } catch (e) { reject(e); } }); }
这里先截取一小段_runThenWrap
,次要是阐明onFulfilled
或onRejected
的运行,这部分在标准中有这样子的一个形容
onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
简略来说就是onFulfilled
和onRejected
要在执行上下文外面没有除了platform code
之后能力执行,这段听起来有点拗口,其实说人话就是咱们常常说的要在微工作
、宏工作
所以咱们这里包装了_runMicroTask
办法,用于封装这部分执行的逻辑
_runMicroTask(fn) { // 2.2.4 queueMicrotask(fn); }
这里应用queueMicrotask
作为微工作的实现, 当然这个有兼容性问题,具体能够看caniuse
实现的办法还有很多,比方setTimeout
、setImmediate
、 MutationObserver
、process.nextTick
而后将value
或reason
作为参数执行onFulfilled
或onRejected
,而后获取返回值thenResult
,接下来就会有几个判断的分支
如果
thenResult
是一个 promise- 判断是否和
then
返回的 promise 是雷同的,如果是抛出TypeError
- 传递
then
返回的 promise 的resolve
和reject
,作为thenResult.then
的onFulFilled
和onRejected
函数
- 判断是否和
如果
thenResult
不是一个 promise- 判断是否是
thenable
,这部分咱们在上面进行解说 - 如果以上判断都不是,那么将
thenResult
作为参数,调用resolvePromise
- 判断是否是
thenable的解决
thenable
应该说是实现外面最简单的一个局部了,首先,咱们要依据定义判断上局部的thenResult
是否是thenable
if ( typeOf(thenResult) === "Object" || typeOf(thenResult) === "Function" ) { //2.3.3.1 const thenFunction = thenResult.then; if (typeOf(thenFunction) === "Function") { // is thenable } }
能够看到 须要判断是否是一个Object
或者Function
,而后再判断thenResult.then
是不是个 Function
,那么有人会问,能不能写成这样子
if ( (typeOf(thenResult) === "Object" || typeOf(thenResult) === "Function") && (typeOf(thenResult.then) === 'Function') ) { // is thenable }
刚开始我也是这么写的,而后发现测试用例跑不过,最初去看了标准,有这么一段3.5
简略来说就是为了保障测试和调用的一致性,先把thenResult.then
进行存储再判断和运行是有必要的,屡次拜访属性可能会返回不同的值
接下去就是thenable
的解决逻辑了
简略来说thenable
的解决逻辑有两种状况
- 在 promise 的
then
或者resolve
中解决thenable
的状况 - 在
thenable
的then
回调中解决value
还是thenable
的状况
这里用在 promise 的then
的thenable
调用进行讲述:
_thenableResolve(result, resolve, reject) { try { if (result instanceof MyPromise) { // 2.3.2 result.then(resolve, reject); return true; } if (typeOf(result) === "Object" || typeOf(result) === "Function") { const thenFn = result.then; if (typeOf(thenFn) === "Function") { // 2.3.3.3 thenFn(resolve, reject); return true; } } } catch (e) { //2.3.3.3.4 reject(e); return true; } } const [resolvePromise, rejectPromise] = this._runBothOneTimeFunction( (result) => { if (!this._thenableResolve(result, resolve, reject)) { resolve(result); } }, (errorReason) => { reject(errorReason); } ); try { thenFunction.call(thenResult, resolvePromise, rejectPromise); } catch (e) { //2.3.3.2 rejectPromise(e); }
这里咱们结构了resolvePromise
和rejectPromise
,而后调用 thenFunction
, 在函数逻辑中解决实现之后将会调用resolvePromise
或者rejectPromise
, 这时候如果result
是一个 thenable
,那么就会持续传递上来,直到不是thenable
,调用resolve
或者reject
咱们要留神的是 promise 的then
办法和thenable
的then
办法是有不同的中央的
- promise 的
then
有两个参数,一个是fulfilled
,一个是rejected
,在后面的 promise状态扭转之后会回调对应的函数 thenable
的then
也有两个参数,这两个参数是提供给thenable
调用实现进行回调的resolve
和reject
办法,如果thenable
的回调值还是一个thenable
,那么会依照这个逻辑调用上来,直到是一个非thenable
,就会调用离thenable
往上回溯最近的一个 promies 的resolve
或者reject
到这里,咱们的promise 曾经能够反对
thenable
的运行new MyPromise((resolve)=>{ resolve({ then:(onFulfilled,onRejected)=>{ console.log('do something'); onFulfilled('hello'); } })}).then((result)=>{ return { then:(onFulfilled,onRejected)=>{ onRejected('world'); } }});
promise和then及thenable中对于谬误的解决
错误处理指的是在运行过程中呈现的谬误要进行捕捉解决,基本上应用 try/catch
在捕捉到谬误之后调用 reject
回调,这部分比较简单,能够间接看代码
resolve和reject函数的调用次数问题
一个 promise 中的resolve
和reject
调用能够说是互斥而且惟一的,就是这两个函数只能有一个被调用,而且调用一次,这个说起来比较简单,然而和谬误场景在一起的时候,就会有肯定的复杂性
原本可能是这样子的
if(something true){ resolve();}else { reject();}
加上谬误场景之后
try{ if(something true){ resolve(); throw "some error"; }else { reject(); }}catch(e){ reject(e);}
这时候判断就会有效了, 因而咱们依照通过一个工具类来包装出两个互斥的函数,来达到目标
_runBothOneTimeFunction(resolveFn, rejectFn) { let isRun = false; function getMutuallyExclusiveFn(fn) { return function (val) { if (!isRun) { isRun = true; fn(val); } }; } return [ getMutuallyExclusiveFn(resolveFn), getMutuallyExclusiveFn(rejectFn), ]; }
至此,咱们一个完全符合Promise/A+
规范的 Promise,就实现了, 残缺代码在这里
等等,是不是少了些什么
有人看到这里会说,这就完了吗?
我常常应用的catch
、finally
,还有静态方法Promise.resolve
、Promise.reject
、Promise.all/race/any/allSettled
办法呢?
其实从规范来说,Promise/A+
的规范就是后面讲述的局部,只定义了then
办法,而咱们日常应用的其余办法,其实也都是在then
办法下面去派生的,比方catch
办法
MyPromise.prototype.catch = function (catchFn) { return this.then(null, catchFn);};
具体的办法其实也实现了,具体能够看promise_api
最初
最初是想分享下这次这个 promise 编写的过程,从下面的讲述看似很顺利,然而其实在编写的时候,我基本上是简略了过了以下规范,而后依照本人的了解,联合promises-tests
单元测试用例来编写的,这种开发模式其实就是TDD(测试驱动开发 (Test-driven development)),这种开发模式会大大加重发人员编程时候对于边界场景没有笼罩的心智累赘,然而反过来,对于测试用例的便携品质要求就很高了
总体来说这次便携 promise 是一个比拟乏味的过程,下面如果有什么问题的,欢送留言多多交换