本篇来讲讲如何模拟实现一个 Promise 的基本功能,网上这类文章已经很多,本篇笔墨会比较多,由于想用自己的了解,用白话文来讲讲
Promise 的基本规范,参考了这篇:【翻译】Promises/A+规范
但说实话,太多的专业术语,以及基本按照标准规范格式翻译而来,有些内容,假如不是对规范的阅读方式比较熟习的话,那是很难了解这句话的内容的
我就是属于没直接阅读过官方规范的,所以即便在看中文译版时,有些表达依旧需要花费很多时间去了解,基于此,才想要写这篇
Promise 是一种异步编程方案,通过 then 方法来注册回调函数,通过构造函数参数来控制异步状态
Promise 的状态变化有两种,成功或者失败,状态一旦变更结束,就不会再改变,后续所有注册的回调都能接收此状态,同时异步执行结果会通过参数传递给回调函数
var p = new Promise((resolve, reject) => { // do something async job // resolve(data); // 任务结束,触发状态变化,通知成功回调的解决,并传递结果数据 // reject(err); // 任务异常,触发状态变化,通知失败回调的解决,并传递失败起因}).then(value => console.log(value)).catch(err => console.error(err));p.then(v => console.log(v), err => console.error(err));
上述例子是基本用法,then 方法返回一个新的 Promise,所以支持链式调用,可用于一个任务依赖于上一个任务的执行结果这种场景
对于同一个 Promise 也可以调用屡次 then 来注册多个回调解决
通过使用来了解它的功能,清楚它都支持哪些功能后,我们在模拟实现时,才能知道究竟需要写些什么代码
所以,这里来比较细节的罗列下 Promise 的基本功能:
then(null, onRejected)
的语法糖new Promise(task)
时,传入的 task 函数就会马上被执行了,但传给 then 的回调函数,会作为微任务放入队列中等待执行(浅显了解,就是降低优先级,推迟执行,不知道怎样模拟微任务的话,可以使用 setTimeout 生成的宏任务来模拟)这些基本功能就足够 Promise 的日常使用了,所以我们的模拟实现版的目标就是实现这些功能
Promise 的基本功能清楚了,那我们代码该怎样写,写什么?
从代码角度来看的话,无非也就是少量变量、函数,所以,我们即可以来针对各个功能点,思考下,都需要哪些代码:
task 解决函数和注册的回调解决函数都是使用者在使用 Promise 时,自行根据业务需要编写的代码
那么,剩下的也就是我们在实现 Promise 时需要编写的代码了,这样一来,Promise 的骨架其实也即可以出来了:
export type statusChangeFn = (value?: any) => void;/* 回调函数类型 */export type callbackFn = (value?: any) => any;export class Promise { /* 三种状态 */ private readonly PENDING: string = 'pending'; private readonly RESOLVED: string = 'resolved'; private readonly REJECTED: string = 'rejected'; /* promise当前状态 */ private _status: string; /* promise执行结果 */ private _value: string; /* 成功的回调 */ private _resolvedCallback: Function[] = []; /* 失败的回调 */ private _rejectedCallback: Function[] = []; /** * 解决 resolve 的状态变更相关工作,参数接收外部传入的执行结果 */ private _handleResolve(value?: any) {} /** * 解决 reject 的状态变更相关工作,参数接收外部传入的失败起因 */ private _handleReject(value?: any) {} /** * 构造函数,接收一个 task 解决函数,task 有两个可选参数,类型也是函数,其实也就是上面的两个解决状态变更工作的函数(_handleResolve,_handleReject),用来给使用者来触发状态变更使用 */ constructor(task: (resolve?: statusChangeFn, reject?: statusChangeFn) => void) {} /** * then 方法,接收两个可选参数,用于注册成功或者失败时的回调解决,所以类型也是函数,函数有一个参数,接收 Promise 执行结果或者失败起因,同时可返回任意值,作为新 Promise 的执行结果 */ then(onResolved?: callbackFn, onRejected?: callbackFn): Promise { return null; } catch(onRejected?: callbackFn): Promise { return this.then(null, onRejected); }}
注意:骨架这里的代码,我用了 TypeScript,这是一种强类型语言,可以标明各个变量、参数类型,便于讲述和了解,看不懂没关系,下面有编译成 js 版的
所以,我们要补充完成的其实就是三部分:Promise 构造函数都做了哪些事、状态变更需要做什么解决、then 注册回调函数时需要做的解决
Promise 的构造函数做的事,其实很简单,就是马上执行传入的 task 解决函数,并将自己内部提供的两个状态变更解决的函数传递给 task,同时将当前 promise 状态置为 PENDING(执行中)
constructor(task) { // 1. 将当前状态置为 PENDING this._status = this.PENDING; // 参数类型校验 if (!(task instanceof Function)) { throw new TypeError(`${task} is not a function`); } try { // 2. 调用 task 解决函数,并将状态变更通知的函数传递过去,需要注意 this 的解决 task(this._handleResolve.bind(this), this._handleReject.bind(this)); } catch (e) { // 3. 假如 task 解决函数发生异常,当做失败来解决 this._handleReject(e); }}
Promise 状态变更的相关解决是我觉得实现 Promise 最难的一部分,这里说的难并不是说代码有多复杂,而是说这块需要了解透,或者者看懂规范并不大容易,由于需要考虑少量解决,网上看了些 Promise 实现的文章,这部分都存在问题
状态变更的工作,是由传给 task 解决函数的两个函数参数被调用时触发进行,如:
new Promise((resolve, reject) => { resolve(1); });
resolve 或者 reject 的调用,就会触发 Promise 内部去解决状态变更的相关工作,还记得构造函数做的事吧,这里的 resolve 或者 reject 其实就是对应着内部的 _handleResolve 和 _handleReject 这两个解决状态变更工作的函数
但这里有一点需要注意,是不是 resolve 一调用,Promise 的状态就肯定发生变化了呢?
答案不是的,网上看了些这类文章,他们的解决是 resolve 调用,状态就变化,就去解决回调队列了
但实际上,这样是错的
状态的变更,其实依赖于 resolve 调用时,传递过去的参数的类型,由于这里可以传递任意类型的值,可以是基本类型,也可以是 Promise
当类型不一样时,对于状态的变更解决是不一样的,开头那篇规范里面有详细的说明,但要看懂并不大容易,我这里就简单用我的了解来讲讲:
x.then(this._handleResolve, this._handleReject)
x.then(this._handleResolve, this._handleReject)
所以你可以看到,其实 resolve 即便调用了,但内部并不肯定就会发生状态变化,只有当 resolve 传递的参数类型既不是 Promise 对象类型,也不是具备 then 方法的 thenable 对象时,状态才会发生变化
而当传递的参数是 Promise 或者具备 then 方法的 thenable 对象时,差不多又是相当于递归回到第一步的等待 task 函数的解决了
想想为什么需要这种解决,或者者说,为什么需要这么设计?
这是由于,存在这样一种场景:有多个异步任务,这些异步任务之间是同步关系,一个任务的执行依赖于上一个异步任务的执行结果,当这些异步任务通过 then 的链式调用组合起来时,then 方法产生的新的 Promise 的状态变更是依赖于回调函数的返回值。所以这个状态变更需要支持当值类型是 Promise 时的异步等待解决,这条异步任务链才能得到预期的执行效果
当你们去看规范,或者看规范的中文版翻译,其实有关于这个的更详细解决说明,比方开头给的链接的那篇文章里有专门一个模块:Promise 的处理过程,也表示成 [[Resolve]](promise, x)
就是在讲这个
但我想用自己的了解来形容,这样比较容易了解,尽管我也只能形容个大概的工作,更细节、更全面的解决应该要跟着规范来,下面就看看代码:
/** * resolve 的状态变更解决 */_handleResolve(value) { if (this._status === this.PENDING) { // 1. 假如 value 是 Promise,那么等待 Promise 状态结果出来后,再重新做状态变更解决 if (value instanceof Promise) { try { // 这里之所以不需要用 bind 来注意 this 问题是由于使用了箭头函数 // 这里也可以写成 value.then(this._handleResole.bind(this), this._handleReject.bind(this)) value.then(v => { this._handleResolve(v); }, err => { this._handleReject(err); }); } catch(e) { this._handleReject(e); } } else if (value && value.then instanceof Function) { // 2. 假如 value 是具备 then 方法的对象时,那么将这个 then 方法当做 task 解决函数,把状态变更的触发工作交由 then 来解决,注意 this 的解决 try { const then = value.then; then.call(value, this._handleResolve.bind(this), this._handleReject.bind(this)); } catch(e) { this._handleReject(e); } } else { // 3. 其余类型,状态变更、触发成功的回调 this._status = this.RESOLVED; this._value = value; setTimeout(() = { this._resolvedCallback.forEach(callback => { callback(); }); }); } }}/** * reject 的状态变更解决 */_handleReject(value) { if (this._status === this.PENDING) { this._status = this.REJECTED; this._value = value; setTimeout(() => { this._rejectedCallback.forEach(callback => { callback(); }); }); }}
then 方法负责的职能其实也很复杂,既要返回一个新的 Promise,这个新的 Promise 的状态和结果又要依赖于回调函数的返回值,而回调函数的执行又要看情况是缓存进回调函数队列里,还是直接取依赖的 Promise 的状态结果后,丢到微任务队列里去执行
尽管职能复杂是复杂了点,但其实,实现上,都是依赖于前面已经写好的构造函数和状态变更函数,所以只需前面几个步骤实现上没问题,then 方法也就不会有太大的问题,直接看代码:
/** * then 方法,接收两个可选参数,用于注册回调解决,所以类型也是函数,且有一个参数,接收 Promise 执行结果,同时可返回任意值,作为新 Promise 的执行结果 */then(onResolved, onRejected) { // then 方法返回一个新的 Promise,新 Promise 的状态结果依赖于回调函数的返回值 return new Promise((resolve, reject) => { // 对回调函数进行一层封装,主要是由于回调函数的执行结果会影响到返回的新 Promise 的状态和结果 const _onResolved = () => { // 根据回调函数的返回值,决定如何解决状态变更 if (onResolved && onResolved instanceof Function) { try { const result = onResolved(this._value); resolve(result); } catch(e) { reject(e); } } else { // 假如传入非函数类型,则将上个Promise结果传递给下个解决 resolve(this._value); } }; const _onRejected = () => { if (onRejected && onRejected instanceof Function) { try { const result = onRejected(this._value); resolve(result); } catch(e) { reject(e); } } else { reject(this._value); } }; // 假如当前 Promise 状态还没变更,则将回调函数放入队列里等待执行 // 否则直接创立微任务来解决这些回调函数 if (this._status === this.PENDING) { this._resolvedCallback.push(_onResolved); this._rejectedCallback.push(_onRejected); } else if (this._status === this.RESOLVED) { setTimeout(_onResolved); } else if (this._status === this.REJECTED) { setTimeout(_onRejected); } });}
由于目的在于理清 Promise 的主要功能职责,所以我的实现版并没有按照规范一步步来,细节上,或者者某些特殊场景的解决,可能欠缺考虑
比方对各个函数参数类型的校验解决,由于 Promise 的参数基本都是函数类型,但即便传其余类型,也依旧不影响 Promise 的使用
比方为了避免被更改实现,少量内部变量可以改用 Symbol 实现
但大体上,考虑了上面这些步骤实现,基本功能也差不多了,重要的是状态变更这个的解决要考虑全一点,网上少量文章的实现版,这个是漏掉考虑的
还有当面试遇到让你手写实现 Promise 时不要慌,可以按着这篇的思路,先把 Promise 的基本用法回顾一下,而后回想一下它支持的功能,再而后心里有个大概的骨架,其实无非也就是几个内部变量、构造函数、状态变更函数、then 函数这几块而已,但死记硬背并不好,有个思路,一步步来,总能回想起来
源码补上了 catch,resolve 等其余方法的实现,这些其实都是基于 Promise 基本功能上的一层封装,方便使用
class Promise { /** * 构造函数负责接收并执行一个 task 解决函数,并将自己内部提供的两个状态变更解决的函数传递给 task,同时将当前 promise 状态置为 PENDING(执行中) */ constructor(task) { /* 三种状态 */ this.PENDING = 'pending'; this.RESOLVED = 'resolved'; this.REJECTED = 'rejected'; /* 成功的回调 */ this._resolvedCallback = []; /* 失败的回调 */ this._rejectedCallback = []; // 1. 将当前状态置为 PENDING this._status = this.PENDING; // 参数类型校验 if (!(task instanceof Function)) { throw new TypeError(`${task} is not a function`); } try { // 2. 调用 task 解决函数,并将状态变更通知的函数传递过去,需要注意 this 的解决 task(this._handleResolve.bind(this), this._handleReject.bind(this)); } catch (e) { // 3. 假如 task 解决函数发生异常,当做失败来解决 this._handleReject(e); } } /** * resolve 的状态变更解决 */ _handleResolve(value) { if (this._status === this.PENDING) { if (value instanceof Promise) { // 1. 假如 value 是 Promise,那么等待 Promise 状态结果出来后,再重新做状态变更解决 try { // 这里之所以不需要用 bind 来注意 this 问题是由于使用了箭头函数 // 这里也可以写成 value.then(this._handleResole.bind(this), this._handleReject.bind(this)) value.then(v => { this._handleResolve(v); }, err => { this._handleReject(err); }); } catch(e) { this._handleReject(e); } } else if (value && value.then instanceof Function) { // 2. 假如 value 是具备 then 方法的对象时,那么将这个 then 方法当做 task 解决函数,把状态变更的触发工作交由 then 来解决,注意 this 的解决 try { const then = value.then; then.call(value, this._handleResolve.bind(this), this._handleReject.bind(this)); } catch(e) { this._handleReject(e); } } else { // 3. 其余类型,状态变更、触发成功的回调 this._status = this.RESOLVED; this._value = value; setTimeout(() => { this._resolvedCallback.forEach(callback => { callback(); }); }); } } } /** * reject 的状态变更解决 */ _handleReject(value) { if (this._status === this.PENDING) { this._status = this.REJECTED; this._value = value; setTimeout(() => { this._rejectedCallback.forEach(callback => { callback(); }); }); } } /** * then 方法,接收两个可选参数,用于注册回调解决,所以类型也是函数,且有一个参数,接收 Promise 执行结果,同时可返回任意值,作为新 Promise 的执行结果 */ then(onResolved, onRejected) { // then 方法返回一个新的 Promise,新 Promise 的状态结果依赖于回调函数的返回值 return new Promise((resolve, reject) => { // 对回调函数进行一层封装,主要是由于回调函数的执行结果会影响到返回的新 Promise 的状态和结果 const _onResolved = () => { // 根据回调函数的返回值,决定如何解决状态变更 if (onResolved && onResolved instanceof Function) { try { const result = onResolved(this._value); resolve(result); } catch(e) { reject(e); } } else { // 假如传入非函数类型,则将上个Promise结果传递给下个解决 resolve(this._value); } }; const _onRejected = () => { if (onRejected && onRejected instanceof Function) { try { const result = onRejected(this._value); resolve(result); } catch(e) { reject(e); } } else { reject(this._value); } }; // 假如当前 Promise 状态还没变更,则将回调函数放入队列里等待执行 // 否则直接创立微任务来解决这些回调函数 if (this._status === this.PENDING) { this._resolvedCallback.push(_onResolved); this._rejectedCallback.push(_onRejected); } else if (this._status === this.RESOLVED) { setTimeout(_onResolved); } else if (this._status === this.REJECTED) { setTimeout(_onRejected); } }); } catch(onRejected) { return this.then(null, onRejected); } static resolve(value) { if (value instanceof Promise) { return value; } return new Promise((reso) => { reso(value); }); } static reject(value) { if (value instanceof Promise) { return value; } return new Promise((reso, reje) => { reje(value); }); }}
网上有少量专门测试 Promise 的库,可以直接借助这些,比方:promises-tests
我这里就举少量基本功能的测试用例:
// 测试链式调用new Promise(r => { console.log('0.--同步-----'); r();}).then(v => console.log('1.-----------------')).then(v => console.log('2.-----------------')).then(v => console.log('3.-----------------')).then(v => console.log('4.-----------------')).then(v => console.log('5.-----------------')).then(v => console.log('6.-----------------')).then(v => console.log('7.-----------------'))
<details>
<summary>输出</summary>
<pre><code>0.--同步-----
1.-----------------
2.-----------------
3.-----------------
4.-----------------
5.-----------------
6.-----------------
7.-----------------
</code></pre>
</details>
// 测试屡次调用 then 注册多个回调解决var p = new Promise(r => r(1));p.then(v => console.log('1-----', v), err => console.error('error', err));p.then(v => console.log('2-----', v), err => console.error('error', err));p.then(v => console.log('3-----', v), err => console.error('error', err));p.then(v => console.log('4-----', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>1----- 1
2----- 1
3----- 1
4----- 1
</code></pre>
</details>
// 测试异步场景new Promise(r => { r(new Promise(a => setTimeout(a, 5000)).then(v => 1));}).then(v => { console.log(v); return new Promise(a => setTimeout(a, 1000)).then(v => 2);}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>1 // 5s 后才输出
success 2 // 再2s后才输出
</code></pre>
</details>
这个测试,可以检测出 resolve 的状态变更究竟有没有根据规范,区分不同场景进行不同解决,你可以网上随意找一篇 Promise 的实现,把它的代码贴到浏览器的 console 里,而后测试一下看看,就知道有没有问题了
// 测试执行结果类型为 Promise 对象场景(Promise 状态 5s 后变化)new Promise(r => { r(new Promise(a => setTimeout(a, 5000)));}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>success undefined // 5s 后才输出
</code></pre>
</details>
// 测试执行结果类型为 Promise 对象场景(Promise 状态不会发生变化)new Promise(r => { r(new Promise(a => 1));}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>// 永远都不输出
</code></pre>
</details>
// 测试执行结果类型为具备 then 方法的 thenable 对象场景(then 方法内部会调用传递的函数参数)new Promise(r => { r({ then: (a, b) => { return a(1); } });}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>success 1
</code></pre>
</details>
// // 测试执行结果类型为具备 then 方法的 thenable 对象场景(then 方法内部不会调用传递的函数参数)new Promise(r => { r({ then: (a, b) => { return 1; } });}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>// 永远都不输出
</code></pre>
</details>
// 测试执行结果类型为具备 then 的属性,但属性值类型非函数new Promise(r => { r({ then: 111 });}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>success {then: 111}
</code></pre>
</details>
// 测试当 Promise rejectd 时,reject 的状态结果会一直传递到可以解决这个失败结果的那个 then 的回调中new Promise((r, j) => { j(1);}).then(v => console.log('success', v)) .then(v => console.log('success', v), err => console.error('error', err)) .catch(err => console.log('catch', err));
<details>
<summary>输出</summary>
<pre><code>error 1
</code></pre>
</details>
// 测试传给 then 的参数是非函数类型时,执行结果和状态会一直传递new Promise(r => { r(1);}).then(1).then(null, err => console.error('error', err)).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>success 1
</code></pre>
</details>
// 测试 rejectd 失败被解决后,就不会继续传递 rejectdnew Promise((r,j) => { j(1);}).then(2).then(v => console.log('success', v), err => console.error('error', err)).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>输出</summary>
<pre><code>error 1
success undefined
</code></pre>
</details>
最后,当你自己写完个模拟实现 Promise 时,你可以将代码贴到浏览器上,而后自己测试下这些用例,跟官方的 Promise 执行结果比对下,你即可以知道,你实现的 Promise 基本功能上有没有问题了
当然,需要更全面的测试的话,还是得借助少量测试库
不过,自己实现一个 Promise 的目的其实也就在于理清 Promise 基本功能、行为、原理,所以这些用例能测通过的话,那么基本上也就掌握这些知识点了