前言
最近总是听到前端面试要求手写promise,promise作为解决前端回调地狱的方案被广泛运用在前端异步调用上,nodejs更是出了一版返回promise IO接口,所以无论从面试还是实际工作中熟悉promise都是一个合格前端必备技能。
分析Promise
我们知道要实现一个功能,首先要了解需求是什么,实现Promise也是如此。
那么Promise到底提供了哪些功能?我们先把Promise中常用的方法列出来
// 使用Promise的标准开头 new Promise(),可见Promise是一个类
class Promise {
/**
* 常用方式Promise.resolve(something).then(...)
* 可见Promise.resolve返回一个新的Promise实例
**/
static resolve() {}
// 同上
static reject() {}
/**
* 构造函数接收一个函数作为入参
* 该函数会接收resolve,reject作为入参,并且立即执行
* Promise将在fn调用resolve/reject后决议
*/
constructor(fn) {}
/**
* 根据constructor我们可以推断Promise有三个状态分别是
* pending,fullfilled,rejected
* 我们需要一个内部变量来保存promise的当前状态
*/
_status = 'pending' // promise的初始状态为pending
/**
* 我们需要一个变量保存promise的决议结果
* 考虑 new Promise((resolve) => resolve(2));
**/
_result
/**
* 考虑一下resolve做了什么事情
* 其实resolve改变promise的状态并将入参作为回调函数的入参
**/
resolve(ret) {}
// 同上
reject(ret) {}
/**
* then称得上是promise的核心方法,到底then做了什么,我们考虑一下
* then会接收两个函数,在promise的状态发生改变后会调用对应的函数
* 所以在这里then的作用应当是个事件注册器。
* 需要注意的是then是能多次调用的
* const promise = new Promise(fn)
* promise.then(fn1)
* promise.then(fn2)
* 另外then是支持链式调用的,如promise.then(fn3).then(fn4)
* 所以调用then还应当返回一个promise对象
**/
then(fn, fn2) {}
// 描述完then后,我们发现我们需要两个回调队列来保存使用then注册的回调
_successCallback = []
_errorCallback = []
// 我们再定义一个内部方法来异步执行这些回调函数
// 使用入参type区分执行successCallback还是errorCallback
_runCallbackAsync(type) {}
}
实现
分析完Promise的核心功能后,让我们依次开始实现这些接口。
// 先不看静态方法和实例方法,从构造器开始实现
class Promise {
...
constructor(fn) {
// 根据之前描述构造器作用主要是运行传入的fn
// 将fn放在try catch中运行,防止fn执行出错
try {
//new Promise((resolve, reject) => {})
fn(this.resolve.bind(this), this.reject.bind(this))
} catch(e) {
this.reject(e)
}
}
// resolve调用之后改变后promise的状态,并且异步执行callback
resolve(result) {
// promise的状态不是pending,说明该promise已经调用过reject/resolve
if(this._status !== 'pending') throw new Error('重复决议promise')
// 保存决议结果
this._result = result
// 异步执行callback
this._runCallbackAsync('success')
}
// reject也是同理
reject(err) {
if(this._status !== 'pending') throw new Error('重复决议promise')
this._result = err
// 这里我们执行错误回调
this._runCallbackAsync('error')
}
...
}
在写promise.then之前我们重新考虑下这个方法到底做了什么?我们逐步实现这个功能点。
- then接收两个函数,我们暂且称它为成功回调和错误回调,分别在fullFilled和rejected的时候执行
- 当promise状态已经是决议状态时立即执行对应的成功回调/错误回调
- 返回一个新的promise,新的promise在成功回调/错误回调执行完成后决议,并使用回调的返回作为决议结果
class Promise {
...
// 省略前后代码
then(fn1, fn2) {
// 支持链式调用,立即回复一个新的promise对象
return new Promise((resolve, reject) => {
/** 然后我们要将对应的回调函数推入事件队列,他们将在本个promise
* 决议后由_runCallbackAsync执行。
* 但执行完对应事件后需要将执行结果传到下一个promise中
* 所以我们需要对回调函数进行小小的处理
*/
// 接收决议结果
const successCallBack = (result) => {
try {
// 将promise的决议结果传递给回调函数去执行,并把回调函数的结果作为下一个promise的决议结果
resolve(fn1(result))
} catch(e) {
// 如果执行出错,应该将错误信息传递给promise
reject(e)
}
}
const errorCallback = (e) => {
try {
reject(fn2(e))
} catch(err) {
reject(err)
}
}
// 将回调函数推入事件队列
if (fn1 && this._status !== 'error') this._successCallback.push(fn1)
if (fn2 && this._status !== 'success') this._errorCallback.push(fn2)
// 如果promise已经决议,则立即执行相应的回调
if(this._status === 'success') this._runCallbackAsync('success')
if(this._status === 'error') this._runCallbackAsync('error')
})
}
// 实现
_runCallbackAsync(type) {
let eventQueue;
if(type === 'error') eventQueue = this._errorCallback;
else eventQueue = this._successCallback;
// 执行回调, 使用settimeout模拟异步执行
setTimeout(() => {
eventQueue.forEach(callback => callback(this._result));
// 清空事件队列,两个事件队列都需要删除,因为promise决议后状态不可变更,决议执行完应当清空所有队列,以解除引用关系
this._errorCallback.length = 0;
this._successCallback.length = 0;
}, 0)
}
...
}
到此Promise的核心方法已经实现完毕,剩下的方法我们很容易使用现有方法进行实现,例如:
class Promise {
···
catch(fn) {
return this.then(undefined, fn);
}
static resolve(any) {
// 使用鸭子类型去判断any的类型
if (
typeof any.then === 'function'
typeof any.catch === 'function'
...
) return any;
// 如果不是一个promise则返回一个新的promise
return new Promise((resolve) => {
resolve(any)
})
}
···
}
总结
所以实现promise关键点在于,理解promise只决议一次,有三个状态(pending,fullFilled,rejected),then/catch支持链式调用(返回一个新的promise),并且理解then/catch本质是个事件注册器,类似于then = subscribe('resolve', callback),理解这些自己手动实现一个promise还是不难的。
最后的最后:刚开始写技术文章,如果有错漏希望能够不吝指出,如果觉得写的还不错点个赞是对我最大的支持,谢谢。
最后附上源码链接:github.com/MinuteWong/…
欢迎阅读我的其他文章: