前言
“一千个读者就有一千个哈姆雷特”
这篇便是分享我心中自己的promise(当然也结合了其他优秀的文章和规范,最后融合了自己的想法)。
也许是重复的分享,但也会有新的体会。
白话文,由浅入深,一起揭开promise背后的原理。
在这之前希望你已经在工作中有接触promise,并使用它解决了一些异步问题。本篇就不做使用说明的讲解,基于大家都会使用的基础上,让彻底明白背后的实现,逻辑流。 在认真食用、仔细消化后。相信你对promise不会再生疏。
接下来你就 能在业务场景中运用更加自如,或是在面试过程中也能侃侃而谈,甚至直接手写promise。
这边有一些链接可以帮助理解:
——————————————————————————————————————————
👌开始
我们从基础实现入手,一步一步书写并理解完整的Promise。
为了方便理解,最初我们先不考虑错误和异常情况。整体流程梳理完后,再加上会更简单。
// 业务场景
function getData () {
return new Promise((resolve) => {
http.$get(url, (result) => {
resolve(result)
})
})
}
getData().then((data) => {
// 业务处理
console.log(data)
})
如上方示例,日常的业务场景中我们通过异步请求获取数据,基本我们都会将请求代码借助promise统一封装。
在业务逻辑中通过then方法注册我们自己的逻辑进行操作。
通过一些场景的展示再结合手写代码,让你更深刻理解。
——————————————————————————————————————————
动手先实现基础(版本1)
- 原型上的then能收集回调
- 在调用resolve后能统一执行收集的回调
class Promise {
// 回调收集
callbacks = []
constructor (fn) {
// resolve的执行作用域不一定会在当前,所以需要绑定this
fn(this._resolve.bind(this))
}
then (onFulfilled) {
this.callbacks.push(onFulfilled)
}
_resolve (val) {
const me = this
me.callbacks.forEach(fn => {
fn(val)
})
}
}
// 应用示例
const p = new Promise((resolve) => {
setTimeout(() => {
resolve(11)
}, 1000)
}).then((data) => {
console.log(data)
})
p.then((data) => {
console.log(data,'1')
})
p.then((data) => {
console.log(data,'2')
})
此时,我们的promise已经可以包裹异步操作,并处理收集好的回调了。而且还可以将这次promise赋值给变量,多次注册回调。我们理清一下目前的逻辑:
- promise中的callbacks定义为数组,为的是收集回调函数,回调可能会注册多个。
- 实例化promise时,将resolve方法传递给fn(用户实例化promise时的参数方法)
- promise通过then方法将用户注册的回调函数收集起来
- 在用户执行resolve方法时,统一依次处理之前收集的回调
但是目前出现了两个问题
const p = new Promise((resolve) => {
resolve(11)
})
p.then((data) => {
console.log(data,'1')
}).then((data)=>{
console.log(data,'2')
})
- 我们的promise目前只能包裹异步代码,如果resolve立即执行,此时then方法还没来得及收集回调,callbacks数组为空。回调函数就无法执行。
- 此时的promise还不支持链式调用
这些问题显然不行,A+规范规定了promise表示的是一个异步操作的结果,所以我们执行resolve的逻辑暂时得改改。
——————————————————————————————————————————
增加延迟机制和链式调用(版本2)
- 统一异步执行
- 简单的链式调用
class Promise {
// 回调收集
callbacks = []
constructor (fn) {
fn(this._resolve.bind(this))
}
then (onFulfilled) {
this.callbacks.push(onFulfilled)
return this
}
_resolve (val) {
const me = this
setTimeout(() => {
me.callbacks.forEach(fn => {
fn(val)
})
})
}
}
- 在then方法中返回当前this(支持链式调用)
- resolve中加入setTimeout
修改后,看起来已经有点东西了。链式调用和统一异步执行回调已经完成。其实到了这一步我们才走到了一小半。
仔细思考你会发现新的两个问题
// 应用示例
const p = new Promise((resolve) => {
resolve(1)
})
// 问题一
setTimeout(() => {
p.then((data) => {
console.log(data)
})
}, 1000)
// 问题二
p.then((data) => {
data += 1
return data
}).then((data) => {
console.log(data + 1)
})
- 如果resolve已经执行,后续在then注册的回调再也不会执行了。
- 思考一下此时的链式调用是真正的链式调用吗?(参考下图)
A+规范中说明每个promise的then方法必须返回一个promise实例(目前我们return this)。此时的链式调用,其实都被最初的promise对象收集了,所有回调函数获得的返回值都一样,这并不是我们想要的效果,也不符合真正链式调用。我们需要的是链式的数据流,从这开始,算是本文的一大重点,需要认真思考。
这种情况下,我们需要加入状态来管理我们promise。 也就是大家熟知的 pending、fulfilled、rejected,规定了promise中状态的转换是单向的,只能从pending改变成fulfilled或rejected。接着还需要重新修改一下then函数,在其中返回promise对象,使其更合理的链式调用。
——————————————————————————————————————————
状态管理、链式调用(版本3)
- 状态管理
- 更新后的链式调用(返回新promise)
class Promise {
// 回调收集
callbacks = []
// 返回值保存
value = null
// 状态管理
state = 'pending'
constructor (fn) {
fn(this._resolve.bind(this))
}
then (onFulfilled = null) {
const me = this
// 返回内置promise
return new Promise((resolve) => {
// 将收集的回调封装成对象
me._handle({
onFulfilled,
resolve
})
})
}
_handle (cbObj) {
// 首先判断状态
if (this.state === 'pending') {
this.callbacks.push(cbObj)
return
}
// 如果没有传递回调函数
if (!cbObj.onFulfilled) {
this._resolve(this.value)
return
}
const val = cbObj.onFulfilled(this.value)
// 此处已经脱离了最初的promise,进入内置promise处理中
cbObj.resolve(val)
}
_resolve (val) {
const me = this
setTimeout(()=>{
me.value = val
me.state = 'fulfilled'
me.callbacks.forEach(cbObj => {
me._handle(cbObj)
})
})
}
}
这个版本代码量增加了不少,但逻辑并不复杂。我们再次理一下目前的逻辑:
- 用户在then方法中注册的回调,我们进行简单封装,用新的promise包裹。我们先称呼为内置promise,目的是将下一个then回调收集到内置promise中,实现真正的链式数据流。
- 新增加handle方法,处理包装后的回调对象。如果状态还是pending,则收集,如果状态为fulfilled则执行resolve(统一执行收集的回调),获取返回值。再将返回值传递给内置promise的resolve。
- 增加了状态state,初始为pending,执行resolve方法后,将状态更改为fulfilled。有了状态判断,即使已经fulfilled的promise,后续注册的then回调也是能执行。
// 应用示例
const p = new Promise((resolve) => {
resolve(1)
})
setTimeout(() => {
p.then((data) => {
console.log(data)
})
}, 1000)
p.then((data) => {
data += 1
return data
}).then((data) => {
console.log(data + 1)
})
至此,上方的应用案例已经可以实现。我们已经可以将结果按照流水线般传递给then回调,处理完成后再传递给下一个then回调。可能有些同学还不是很明白,那我们再用白话文理解一下。
- 我们需要的是每一个注册在then上的回调函数依次处理结果,并传递下去。
- then是promise中的方法。所以then返回的必然是一个promise对象。
- 那我们就在then方法中新建一个promise(内置promise),并返回。
- 最关键的地方是,内置的这个promise的resolve方法什么时候执行?当然,肯定是用户当前的回调函数执行完毕,我们就可以改变内置promise的状态。
- 到这,已经可以把内置promise想象成一个全新的、最初始的实例,它便开始了新一轮的回调收集并执行。
新问题又出现咯
- 如果用户此时注册的回调是异步函数,或者返回一个promise对象呢?
// 应用示例
getData().then((data) => {
// 在回调中再次执行并返回一个新的promise
return new Promise((resolve) => {
// 模拟通过获取data后,处理并再次发起请求
http.$get(url + data.id, (result) => {
resolve(result)
})
})
}).then((result) => {
// 经过两个请求后 获取最终数据,并处理
console.log(result)
})
那如果用户注册的回调函数,返回的是一个promise对象?我们只需要将内置promise的resolve注册成用户promise的回调不就好了吗。就是追加到用户promise的then中。
——————————————————————————————————————————
(版本3.1)
class Promise {
// 回调收集
callbacks = []
// 返回值保存
value = null
// 状态管理
state = 'pending'
constructor (fn) {
fn(this._resolve.bind(this))
}
then (onFulfilled = null) {
const me = this
return new Promise((resolve) => {
// 将收集的回调封装成对象
me._handle({
onFulfilled,
resolve
})
})
}
_handle (cbObj) {
// 首先判断状态
if (this.state === 'pending') {
this.callbacks.push(cbObj)
return
}
// 如果没有传递回调函数
if (!cbObj.onFulfilled) {
this._resolve(this.value)
return
}
const val = cbObj.onFulfilled(this.value)
cbObj.resolve(val)
}
_resolve (val) {
const me = this
// 判断val是否是promise对象,有没有then方法可以挂载
if (val && (typeof val === 'object' || typeof val === 'function')) {
const valThen = val.then
if (valThen && typeof valThen === 'function') {
// 保持this指向正确,如果不绑定,此时valThen中的this会指向内置promise,而不是用户自己的promise
valThen.call(val, me._resolve.bind(me))
return
}
}
setTimeout(()=>{
me.value = val
me.state = 'fulfilled'
me.callbacks.forEach(cbObj => {
me._handle(cbObj)
})
})
}
}
- 我们只在resolve方法中增加了判断,处理结果值可能是promise实例或者类似promise对象的情况。
- 在通过一个作用域绑定把内置promise的resolve注册成用户的回调。
修改之后已经可以处理上方的应用示例啦。看到这,其实你已经基本掌握啦promise的整体逻辑。后面我们再加上错误状态和异常处理即可。
——————————————————————————————————————————
完善状态管理、异常处理(版本4)
- 加入rejected状态和reject方法
- 加入异常处理
class Promise {
// 回调收集
callbacks = []
// 返回值保存
value = null
// 错误原因
reason = null
// 状态管理
state = 'pending'
constructor (fn) {
// 最初实例化,传递resolve、和reject方法,异常处理
try {
fn(this._resolve.bind(this), this._reject.bind(this))
} catch (error) {
this._reject(error)
}
}
then (onFulfilled = null, onRejected = null) {
const me = this
return new Promise((resolve, reject) => {
// 将收集的回调封装成对象
me._handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
_handle (cbObj) {
// 首先判断状态
if (this.state === 'pending') {
this.callbacks.push(cbObj)
return
}
// 依据状态获取用户回调
const cb = this.state === 'fulfilled' ? cbObj.onFulfilled : cbObj.onRejected
const stateF = this.state === 'fulfilled' ? cbObj.resolve : cbObj.reject
// 如果没有传递回调函数
if (!cb) {
stateF(this.state === 'fulfilled' ? this.value : this.reason)
return
}
// 异常处理
try {
const val = cb(this.state === 'fulfilled' ? this.value : this.reason)
// 注意,这里我们先用内置函数的resolve执行
cbObj.resolve(val)
} catch (error) {
cbObj.reject(error)
}
}
_resolve (val) {
const me = this
// 限制状态转化
if (me.state !== 'pending') return
// 判断结果值是否是当前promise
if (me === val) {
return me._reject(new TypeError("cannot return the same promise object from onfulfilled or on rejected callback."))
}
// 判断val是否是promise对象,有没有then方法可以挂载
if (val && (typeof val === 'object' || typeof val === 'function')) {
const valThen = val.then
if (valThen && typeof valThen === 'function') {
// 保持this指向正确,如果不绑定,此时valThen中的this会指向内置promise,而不是用户自己的promise
// resolve和reject也需要绑定this
valThen.call(val, me._resolve.bind(me), me._reject.bind(me))
return
}
}
setTimeout(()=>{
me.value = val
me.state = 'fulfilled'
me._execute()
})
}
_reject (reason) {
const me = this
// 限制状态转化
if (me.state !== 'pending') return
setTimeout(()=>{
me.reason = reason
me.state = 'rejected'
me._execute()
})
}
_execute () {
const me = this
me.callbacks.forEach(cbObj => {
me._handle(cbObj)
})
}
}
乍一看,代码量又增加了不少。其实主要逻辑还是一样的,我们再来理解一遍逻辑:
- 在最初实例化中加入了异常捕获
- 在then方法中,把错误回调也收集了起来。封装回调对象时把成功回调和错误回调都包裹在一起。
- handle函数中,依据promise状态获取对应的用户回调(成功、失败)函数。这里需要注意的是内置函数究竟是用resolve还是reject执行?为了方便理解,我们这里统一用resolve执行用户回调函数的结果。但是,promise/A+规范中,最复杂的就是对这块的逻辑判断。在规范中,我们需要考虑更多情况,回调函数是否是我们定义的promise、或是其他promise规范?如果结果值是promise对象,状态又是怎么样的等等
- resolve函数中,并没有增加新逻辑。只是增加了状态单一变化的判断、判断返回值是否是当前promise对象,如果相同,依据规范需要抛出错误。
同样的,增加错误回调后。需要在用户的promise实例的then方法中多传递reject处理。 - 最后就是新增了reject方法和excute方法,也很好理解。
终于,这个promise已经很健壮了
如果看到这里,还没有很清晰的认识,可以多回顾几遍。直到你很熟悉它,甚至能直接手写实现。
——————————————————————————————————————————
其实并未结束
版本4的代码,还是不足以通过promise/A+规范的。还是有很多细节的判断需要补充。但是后续补充的细节并不会影响主要逻辑,相信你先能理解版本4的代码,再补充至完整的规范也不是什么难事。
完整的符合promise/A+规范的代码已经在我的仓库中⬇️,有兴趣的同学可以深入研究
我们可以通过 promises-aplus-tests测试自己的promise是否符合规范。
// 在你的promise文件中补充下方代码,并将方法名更改一致
MyPromise.deferred = function() {
const defer = {}
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve
defer.reject = reject
})
return defer
}
try {
module.exports = MyPromise
} catch (e) {
}
按照并在终端中运行即可
npm install promises-aplus-tests -D
npx promises-aplus-tests promise.js
后续还会补上promise原型方法、静态方法的实现。