模拟实现 Promise(小白版)
本篇来说讲如何模拟实现一个 Promise 的基本功能,网上这类文章已经不少,本篇笔墨会比较多,由于想用本身的理解,用白话文来说讲html
Promise 的基本规范,参考了这篇:【翻译】Promises/A+规范git
但说实话,太多的专业术语,以及基本按照标准规范格式翻译而来,有些内容,若是不是对规范的阅读方式比较熟悉的话,那是很难理解这句话的内容的github
我就是属于没直接阅读过官方规范的,因此即便在看中文译版时,有些表达仍旧须要花费不少时间去理解,基于此,才想要写这篇面试
Promise 基本介绍
Promise 是一种异步编程方案,经过 then 方法来注册回调函数,经过构造函数参数来控制异步状态typescript
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
对于同一个 Promise 也能够调用屡次 then 来注册多个回调处理浏览器
经过使用来理解它的功能,清楚它都支持哪些功能后,咱们在模拟实现时,才能知道到底须要写些什么代码缓存
因此,这里来比较细节的罗列下 Promise 的基本功能:异步
- Promise 有三种状态:Pending(执行中)、Resolved(成功)、Rejected(失败),状态一旦变动结束就再也不改变
- Promise 构造函数接收一个函数参数,能够把它叫作 task 处理函数
- task 处理函数用来处理异步工做,这个函数有两个参数,也都是函数类型,当异步工做结束,就是经过调用这两个函数参数来通知 Promise 状态变动、回调触发、结果传递
- Promise 有一个 then 方法用于注册回调处理,当状态变化结束,注册的回调必定会被处理,即便是在状态变化结束后才经过 then 注册
- then 方法支持调用屡次来注册多个回调处理
- then 方法接收两个可选参数,这两个参数类型都是函数,也就是须要注册的回调处理函数,分别是成功时的回调函数,失败时的回调函数
- 这些回调函数有一个参数,类型任意,值就是任务结束须要通知给回调的结果,经过调用 task 处理函数的参数(类型是函数)传递过来
- then 方法返回一个新的 Promise,以便支持链式调用,新 Promise 状态的变化依赖于回调函数的返回值,不一样类型处理方式不一样
- then 方法的链式调用中,若是中间某个 then 传入的回调处理不能友好的处理回调工做(好比传递给 then 非函数类型参数),那么这个工做会继续往下传递给下个 then 注册的回调函数
- Promise 有一个 catch 方法,用于注册失败的回调处理,实际上是
then(null, onRejected)
的语法糖 - task 处理函数或者回调函数执行过程发生代码异常时,Promise 内部自动捕获,状态直接当作失败来处理
new Promise(task)
时,传入的 task 函数就会立刻被执行了,但传给 then 的回调函数,会做为微任务放入队列中等待执行(通俗理解,就是下降优先级,延迟执行,不知道怎么模拟微任务的话,可使用 setTimeout 生成的宏任务来模拟)
这些基本功能就足够 Promise 的平常使用了,因此咱们的模拟实现版的目标就是实现这些功能
模拟实现思路
第一步:骨架
Promise 的基本功能清楚了,那咱们代码该怎么写,写什么?
从代码角度来看的话,无非也就是一些变量、函数,因此,咱们就能够来针对各个功能点,思考下,都须要哪些代码:
- 变量上至少须要:三种状态、当前状态(_status)、传递给回调函数的结果值(_value)
- 构造函数 constructor
- task 处理函数
- task 处理函数的两个用于通知状态变动的函数(handleResolve, handleReject)
- then 方法
- then 方法~~注册的两个回调函数~~
- 回调函数队列
- catch 方法
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
当类型不同时,对于状态的变动处理是不同的,开头那篇规范里面有详细的说明,但要看懂并不大容易,我这里就简单用个人理解来说讲:
- resolve(x) 触发的 pending => resolved 的处理:
- 当 x 类型是 Promise 对象时:
- 当 x 这个 Promise 的状态变化结束时,再以 x 这个 Promise 内部状态和结果(_status 和 _value)做为当前 Promise 的状态和结果进行状态变动处理
- 能够简单理解成当前的 Promise 是依赖于 x 这个 Promise 的,即
x.then(this._handleResolve, this._handleReject)
- 当 x 类型是 thenable 对象(具备 then 方法的对象)时:
- 把这个 then 方法做为 task 处理函数来处理,这样就又回到第一步即等待状态变动的触发
- 能够简单理解成
x.then(this._handleResolve, this._handleReject)
- 这里的 x.then 并非 Promise 的 then 处理,只是简单的一个函数调用,只是恰好函数名叫作 then
- 其他类型时:
- 内部状态(_status)置为 RESOLVE
- 内部结果(_value)置为 x
- 模拟建立微任务(setTimeout)处理回调函数队列
- 当 x 类型是 Promise 对象时:
- reject(x) 触发的 pending => rejected 的处理:
- 不区分 x 类型,直接走 rejected 的处理
- 内部状态(_status)置为 REJECTED
- 内部结构(_value)置为 x
- 模拟建立微任务(setTimeout)处理回调函数队列
- 不区分 x 类型,直接走 rejected 的处理
因此你能够看到,其实 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
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 注册多个回调处理
// 测试屡次调用 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 对象场景(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 方法的 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 失败被处理后,就不会继续传递 rejectd new 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 基本功能、行为、原理,因此这些用例能测经过的话,那么基本上也就掌握这些知识点了
原文出处:https://www.cnblogs.com/dasusu/p/12047873.html