之前我还讲过 如何为实现 Promise 搭建 TDD 环境,而本文的目的主要是探讨如何基于 TDD 这样的方式实现一个 Promise,并且描述了部分 Promises/A+ 规范,以此为基础实现的一个 Promise
先实现一个最基础版本的 Promise
有了之前的准备,现在就可以进行 TDD(Test Driven Develop)测试驱动开发了,注意,以下的测试代码都写在测试文件 test/index.ts
中,并且文件大致的代码结构为如下所示
import { assert } from 'chai' |
并且源码文件为 src/promise.ts
Promise 是一个类
测试用例
测试用例是这样写
it('是一个类', () => { |
即 assert
断言
- Promise 是一个
function
- Promise 的原型
prototype
是一个对象
那么就可以证明 Promise 是一个类了
源码
class PromiseEasy { |
很简单吧,其实就是用 测试的需求 来推动 开发
new Promise() 如果接受的不是函数就报错
这里的逻辑是 Promise 需要接受一个函数作为参数,所以测试用例应该这么写
测试用例
it('new Promise() 如果接受的不是函数就报错', () => { |
这里
// @ts-ignore
的意思就是 注释会忽略下一行中产生的所有错误,但是因为这里要是不通过就很麻烦,所以还是加上
assert.throw
这个 API 的意思就是断言一段会报错的代码
源码
class PromiseEasy { |
逻辑也很简单,对传入的参数做一个判断即可
new Promise(fn) 会生成一个对象,该对象会有个 then 方法
嗯,该往 Promise 里面添加 then
方法了,不过之前还是得写测试用例
测试用例
it('new Promise(fn) 会生成一个对象,该对象会有个 then 方法', () => { |
源码
class PromiseEasy { |
new Promise(fn) 中的函数立即执行
这个的测试代码就很简单了
测试用例
it('new Promise(fn) 中的函数立即执行', () => { |
判断是否被调用就是判断 called
这个变量有没有变化
源码
那就很简单了,直接调用构造函数里面传入的 fn
即可
class PromiseEasy { |
Promise.then(success) 中的 success 会在 resolve 被调用后执行
所以这里的核心就是需要验证 then
之后里面 resolve
的函数有没有被执行过了
测试用例
it('promise.then(success) 中的 success 会在 resolve 被调用后执行', done => { |
注意这里在 Promise
中使用了 setTimeout
,这是因为
- 这里只有等一会才能断言
called = true
- 因为顺序是先 then -> 调用 fn -> 调用 succeed
- 而 succeed 是放入了 setTimeout 中的
并且还使用了 done
,这是因为
- 如果代码里面需要异步的测试,则需要加
done
- 表示异步测试的完成,告诉 mocha 可以检查其测试结果了
- 不然很多个任务都是异步测试的话,mocha 就不知道哪个是先完成的(这里 mocha 对于测试用例是一个一个同步执行的)
源码
首先需要在 Promise
里面声明两个变量 succeed
和 fail
分别保存成功和失败回调
class PromiseEasy { |
在 then
函数执行时保存回调
class PromiseEasy { |
那么什么时候这个 succeed
函数被调用呢,就是 resolve
的时候。众所周知 Promise 不仅仅有 resolve
,还有 reject
,并且在 new Promise
中使用 resolve
时并不会立刻调用成功回调,而是在执行 then
之后才调用,所以 resolve
和 reject
函数是异步的,这里仅就用 setTimeout
做一个简单的处理
class PromiseEasy { |
然后在构造函数中需要将 resolve
和 reject
传给 fn
当参数就可以了,当然这里需要 bind
下 this
,不然 resolve
和 reject
函数中的 this
是访问不到的
class PromiseEasy { |
Promise.then(null, fail) 中的 fail 会在 reject 被调用后执行
这里的测试代码不用说了,和上面的基本差不多
it('promise.then(null, fail) 中的 fail 会在 reject 被调用后执行', done => { |
至此,一个基本的简单的 Promise 框架就算完成了,剩下就是按照 A+ 规范标准在上面添砖加瓦了
遵循比较重要的 Promise/A+ 规范增加功能
2.2.1 Both onFulfilled and onRejected are optional arguments
onFulfilled
和 onRejected
都是可选的参数
测试用例
这里的测试用例很简单
it('2.2.1 onFulfilled 和 onRejected 都是可选的参数', () => { |
参数可选什么意思,就是 then
传的俩参数都是可以传或者可以不传,所以这里的测试用例很简单
源码
所以这个时候就需要在 3 个地方对参数做判断了
resolve
函数reject
函数then
函数
class PromiseEasy { |
2.2.2 If onFulfilled is a function
2.2.2 这里有三点
- it must be called after promise is fulfilled, with promise’s value as its first argument
- it must not be called before promise is fulfilled
- it must not be called more than once
总的来说,大意就是 此函数必须在 promise 完成(fulfilled) 后被调用,并把 promise 的 result 值作为它的第一个参数
这里先在测试用例中将以上 3 点都测试一遍,但是为了方便起见就只用一个标题
测试用例
it('2.2.2.1 此函数必须在 promise 完成(fulfilled) 后被调用,并把 promise 的 result 值作为它的第一个参数', done => { |
这里使用了
sinon.fake
作为假函数,然后使用assert.isTrue/assert.isFalse
判断sinon.fake.called
是否为true
,为true
说明其已经被调用了,使用sinon.fake.calledOnce
判断其是否只被调用一次,使用sinon.fake.calledWith
判断其是否在调用resolve
时传了对应的参数
所以要实现这一条规范,我们就需要在 Promise 内部维护一个 state
状态判断当前是否是处于 初始状态
/resolve
/reject
,现在我们定义 state
有 3 种状态
pending
: 表示初始状态fulfilled
: 表示用户调用了resolve
函数rejected
: 表示用户调用了reject
函数
这里还要说明的是,
state
状态只能是单向的变化,即只能是pending
->fulfilled
或者pending
->rejected
,而不能三者两两互相变化
源码
所以源码应该是要对 resolve
以及 reject
做修改,并且还要增加一个 state
class PromiseEasy { |
在进入 resolve
/ reject
之后,先判断其是否是在 pending
状态,否则就 return
,即保证上述规范第3点,之后才将状态改变,并且对 succeed
/fail
函数做判断,只有其是函数时才能被调用。其中还添加了 resolve
的参数 result
以及 reject
参数 reason
2.2.3 If onRejected is a function
- it must be called after promise is rejected, with promise’s reason as its first argument
- it must not be called before promise is rejected
- it must not be called more than once
这个跟 2.2.2 其实很像的,就是做 reject
之后的逻辑
测试用例
it('2.2.3.1 此函数必须在 promise 失败(rejected) 后被调用,并把 promise 的值作为它的第一个参数', done => { |
源码
参考 2.2.2 If onFulfilled is a function
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code
这句话给人很疑惑的感觉,因为官方的翻译就是
在执行上下文堆栈(execution context)仅包含平台代码之前,不得调用 onFulfilled 和 onRejected
再去看对应的解释(3.1)
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that
onFulfilled
andonRejected
execute asynchronously, after the event loop turn in whichthen
is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such assetTimeout
orsetImmediate
, or with a “micro-task” mechanism such asMutationObserver
orprocess.nextTick
. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
翻译一下,就是
这里的 “platform code” 表示 (JS 的) 引擎,(JS 的)环境和 promise 执行的代码。实际上,这个(规范)要求能确保,
onFulfilled
和onRejected
在 eventLoop 之后调用then
, 它们可以(得到)异步地执行,然后(继续)使用新的堆栈。这里既可以使用setTimeout
/setImmediate
这样的 “宏任务” 机制实现,也可以使用MutationObserver
/process.nextTick
这样的 “微任务” 机制实现。由于 promise 的实现被看作是 platform code,所以它可能本身就包含一个任务调度队列或者用来处理调用(关系)的 “trampoline”
还是不理解?我举个例子好了,比如有一个函数叫做 XXX
吧,一个 promise 做了如下的操作
promise.then(XXX) |
但是此时函数 XXX
还没有执行哦,因为规范要求,一定要在调用 then
之后执行。因为这才可以产生一个异步的操作,而这个异步的操作如何来?规范里面也写了,可以利用 setTimeout
/setImmediate
这样的宏任务来解决,或者也可以使用 MutationObserver
/process.nextTick
这样的微任务来解决。解决的关键点是什么呢?就是在 Promise 内部,其传入的函数里面执行的优先级比外面通过 promise.then
方式调用 XXX
的优先级 要高。那么怎么做到这种优先级要高呢? 就是在 resolve
/reject
中加上 setTimeout
。当然了,这里只是简单的处理,比如我写如下的测试代码你大概就懂了
如果实在不明白宏任务/微任务机制的可以点击参考我之前写的这篇
测试用例
it('2.2.4 在我的代码执行完之前,不得调用 then 后面的两个函数', done => { |
所以你可以看到,外面的 setTimeout
要 靠后 执行了,为什么,因为 success
函数先进入了 Promise 内部,其内部有个 setTimeout
函数将其包裹了起来,eventLoop 时运行环境会将这个里面的代码放到了一个宏任务队列中,而这个外面调的 setTimeout
也将其紧随其后放到这个队列里面。执行时会优先执行先进去的任务,那么就是 success
先在里面执行了,然后外部的 setTimeout
自然就会觉察到 success
被调用了。
源码
参考最开始写的 先实现一个最基础版本的 Promise
2.2.5 onFulfilled and onRejected must be called as functions (i.e. with no this value)
这里的意思是 onFulfilled
和 onRejected
必须被当作函数调用(例如: 没有 this
)
又懵逼了,啥玩意儿???还是看看说明 3.2吧
That is, in strict mode
this
will beundefined
inside of them; in sloppy mode, it will be the global object
意思就是说
严格模式下,
this
在onFulfilled
以及onRejected
内部是undefined
; 而在非严格模式下,this
在它们内部就是一个全局对象
所以这里我们就选择严格模式就好了,因为本来就是由类封装起来的对象,在这种情况下,this
在 onFulfilled
以及 onRejected
内部只能是 undefined
,那么测试用例应该这么写
测试用例
it('2.2.5 onFulfilled 和 onRejected 必须被当做函数调用(并且里面没有 this)', done => { |
源码
源码就好改了,对应的 resolve
以及 reject
函数里面将对应的函数调用 call
一个 undefined
就好,如下
class PromiseEasy { |
2.2.6 then may be called multiple times on the same promise
then
可以在同一个 Promise 里被多次调用。那么基本上测试代码可以这么写
测试用例
it('2.2.6 then可以在同一个promise里被多次调用', done => { |
这里
sinon.fake.calledAfter
表示在某个目标函数之后被调用
源码
这里的 promise
要求能够多次调用 then
,而我们原来 Promise 的逻辑是 then
只是保存了一次 succeed
和 fail
,这样的话多次 then
就只会覆盖掉之前传入的函数。要能通过测试用例中的代码,我们很容易想到,每次 then
时,用数组来保存 succeed
和 fail
即可。
class PromiseEasy { |
那么在调用的时候怎么办呢?只需要在 resolve
和 reject
中 遍历调用 即可
class PromiseEasy { |
可以看到,本质就是两步
- 添加一个二维数组,
then
时保存succeed
和fail
的数组 - 在
resolve
/reject
时遍历,取出对应的succeed
/fail
,并执行
与 Promises/A 规范的区别
至此,我们已经完成一个 Promise 的基本功能,这个功能包括了 Promise 的核心 API,即 new Promise
以及 then
和里面的成功以及失败回调,下面就来讲(fan)讲(yi) Promises/A+ 规范和 Promises/A 规范的几个比较重要的区别
Omissions
下面的几点都是 Promises/A 规范所遗漏的:
- 进度处理: 实际上,它并未被指定,并且当前在实现 promise 的社区中没有形成一致的意见
- 可交互的 promises: 这被认为超出了可互操作保证所必需的最小 API 的范围
- 对于
var promise2 = promise1.then(onFulfilled, onRejected)
这样的代码,promise1 !== promise2
并不是必要的
Clarifications
Promises/A+ 规范与 Promises/A 规范使用不同的术语,这体现在 Promise 的实现过程中,它们已经成为事实(标准)意义上的词汇。 特别是如下:
- 给定的 promise 的状态是 “pending”、”fulfilled”、”rejected”
- 当 promises 被 fulfilled 时,它们会有 “value”; 当 promises 被 rejected 时,它们会有 “reason”
- 它引入了与 “promises” 不同的术语叫 “thenable”,以便更准确地讨论实现互操作所必需的 duck 测试
Additions
Promises/A+ 规范另外指定:
- 当
onFulfilled
或onRejected
返回一个thenable
的行为时,应包括解析过程的详细信息 - 传给
onRejected
的reason
在处理throws
必须要抛异常 onFulfilled
和onRejected
必须是被异步调用的onFulfilled
和onRejected
必须是当作函数来调用- 对
onFulfilled
和onRejected
的调用的严格排序,以便在同一个 promise 上调用then
总结
一个基础的 Promises/A+ 规范的 Promise 的实现需要注意以下几点
- 构造函数需要传一个函数,并在构造函数内部调用这个函数,而这个函数传了两个参数作为参数:
resolve
和reject
,这两个函数都是 Promise 内部实现的函数 - 每次
then
时都将传入的succeed
和fail
函数 push 进一个数组当中,而这个数组是一个二维数组,用来维护这一堆succeed
和fail
函数 - Promise 内部的
resolve
函数和reject
函数中都做了如下的操作- 判断
state
是否为pending
状态,不是就 return - 修改状态为
fulfilled
/rejected
- 遍历上述的二维数组,找到
succeed
/fail
函数并调用(.call(undefined, result/reason)
)
- 判断
resolve
/reject
函数里面使用setTimeout
做异步执行
另外,对应的项目链接在这里