在工作过程中,我们总会利用 Promise 去实现部分功能,但总是知其然不知其所以然。因此,本篇文章主要探究 Promise 的实现原理,关于用法不过多赘述,就是一步一步实现一个 Promise。
1. Promise基本结构
const p1 = new Promise((resolve, reject) => {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
首先,Promise的构造函数接收一个回调函数作为参数。
其次,回调函数接收了两个参数,分别是resolve、reject,函数体中去选择性的执行resolve或者reject。
最后,Promise 实例生成以后,就要定义 resolve 状态、reject 状态分别需要执行的函数,这个操作需要在then方法中完成。
p1.then(() => {
// success
}, () => {
// failure
})
当 resolve(value) 被执行,意味着状态从“未完成”变更为“成功“,then的第一个方法被执行,状态不变更则不执行;
当reject(error) 被执行,意味着状态从”未完成“变更未”失败“,then的第二个方法被执行,状态不变更则不执行;
那么Promise的基本结构就是以上,来分析一下它的实现逻辑。
实现逻辑
- 定义Promise的三个状态,分别是未完成状态(pending)、成功状态(fulfilled)、失败状态(rejected);
- 需要创建一个类,类的构造函数接收一个函数(executor)作为参数,内部变量有:
- status,存储Promise状态;
- value,存储失败时的error或者成功时的value的变量;
- resolveQueue,fulfilled 状态的函数变量,存储 then 方法传入的函数;
- rejectQueue,rejected 状态的函数变量,存储 then 方法传入的函数;
- 执行传入的函数executor,executor函数需要传入两个参数resolve、reject,那么必须在构造函数内有 resolve、reject 函数变量,在 executor 内部被执行,那么这两个函数需要完成以下内容:
- 变更Promise状态;
- 接收一个任意类型的值作为实参,表示Promise成功或者失败的值,并同步到value;
- 执行 resolveQueue 或者 rejectQueue;
- 需要有一个then方法,then 方法接收两个参数,一个是 fulfilled 时执行的函数,一个是 rejected 时执行的函数变量,then 方法内需要执行的操作是将这两个函数 变量分别存储到 resolveQueue、rejectQueue,等待状态变更后被执行
那么代码实现为:
// 实现基本的then函数
// 以及Promise 的状态实时变更
// 实现一个Promise多次then的调用
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class ourPromise {
constructor(executor) {
// this指向实例对象
// 状态和返回值
this.status = PENDING
this.value = null
// 回调函数存储
this.resolveQueue = []
this.rejectQueue = []
// 成功时执行的函数
let resolve = (val)=>{
if(this.status !== PENDING) return;
this.value = val
this.status = FULFILLED
this.resolveQueue.forEach(callback=>{
callback(val)
})
}
// 失败时执行的函数
let reject = (err) =>{
if(this.status !== PENDING) return;
this.value = err
this.status = REJECTED
this.rejectQueue.forEach(callback=>{
callback(err)
})
}
// 执行定义的实例传回来的回调函数,并传入两个参数
executor(resolve,reject)
}
// then方法的两个参数是两个回调函数
then(fulFilledFun,rejectedFun){
this.resolveQueue.push(fulFilledFun)
this.rejectQueue.push(rejectedFun)
}
}
测试一下
const p1 = new ourPromise((resolve,reject)=>{
setTimeout(()=>{
resolve('success')
},500)
})
p1.then((res)=>{
console.log(res)
})
// 在500ms之后输出
// success
那么基本的Promise就实现了,来梳理一下它的运行流程:
- 创建一个 ourPromise 实例,传入一个实参,是一个函数;
- 进入构造函数进行初始化,最后执行传入的实参函数,内部是异步函数,放入宏任务,继续执行同步语句;
- then方法被传入一个成功时执行的回调函数,那么此时,resolveQueue 变更,也就是说现在的 Promise 实例变更为
status = PENDING
value = null
resolveQueue = [
(res)=>{
console.log(res)
}
]
rejectQueue = []
可以看到,then并非是执行了什么语句,只是对当前实例的属性进行了改写。
- 同步任务执行完毕转去执行宏任务,执行到
resolve('success'),以下函数被调用
let resolve = (val)=>{
this.value = val
this.status = FULFILLED
this.resolveQueue.forEach(callback=>{
callback(val)
})
}
为什么这里的then方法传入的两个函数要存储到一个数组内?
为了支持then 方法 可以被同一个Promise实例调用多次
比如:
const p1 = new ourPromise((resolve,reject)=>{
setTimeout(()=>{
resolve('success')
},500)
})
p1.then((res)=>{
console.log(res)
})
p1.then((res)=>{
console.log('第二个then方法'+res)
})
// 500ms后输出
// success
// 第二个then方法success
2. then方法的实现
ES6 的 Promise 的 then 方法
p1.then(res,rej)
该then方法支持以下功能:
- res,rej 参数可选,但必须是函数
- 当 p1 状态变为成功、失败时必须分别调用 res、rej,其参数为执行 resolve(value)、reject(err ) 时传入的 value、err
- 在 p1 状态改变前其不可被调用
- 调用次数不可超过一次
- then 方法可以被同一个 Promise 实例调用多次
- 当 p1 成功状态时,所有 res 需按照其注册顺序依次回调
- 当p1 失败状态时,所有 rej 需按照其注册顺序依次回调
- then 方法有返回值,返回值为 Promise 对象,支持链式调用
现在,我们来实现以上三个功能,其中第二点,在上面的 第1节 已经实现。那么先来实现 Promise 的核心的链式调用。
在学习 Promise 时,经常回遇到这样的例子,来测试 Promise 的 then 方法的链式调用。
const p1 = new Promise(
(resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 500);
}
)
p1
.then(res => {
console.log(res)
return 2
})
.then(res => {
console.log(res)
return 3
})
.then(res => {
console.log(res)
})
// 顺序输出
// 1
// 2
// 3
在这段代码可以明确的看到几点:
- p1.then() 仍然支持 then 方法,说明 p1.then() 也是一个 Promise 对象,即 then() 返回了一个 Promise 对象;
- p1.then() 方法既然返回了 Promise 对象,但成功的回调函数内部又没有写 resolve 语句,只写了 return 2。那么是在内部怎么处理的呢?p1.then() 返回的 Promise 对象又是什么时候变更状态的呢?
then 方法返回 Promise 实例
那么,先来是实现第一点,令其返回一个Promise对象,也就是来更改then方法。
then(fulFilledFun,rejectedFun){
return new ourPromise((resolve,reject)=>{
this.resolveQueue.push(fulFilledFun),
this.rejectQueue.push(rejectedFun)
})
}
经过以上的改写,就可以成功的返回Promise,也可以支持链式的then方法且不会报错,但不能链式执行我们所写的函数。
基于此,各个Promise实例的注册时机如下图。总的来说,在 then 方法被调用时,不仅创建了新的Promise实例,也更改了上一实例的 resolveQueue,等待状态改变的时候被执行。其中,p1.then().then().then() 创建的Promise实例,它的resolveQueue实例未被更改,仍然是[]。
链式执行 then 方法
现在来实现第二点,链式执行我们所写的then方法内的函数。换言之,是如何确保各个Promise实例的resolveQueue按顺序执行。
从链式执行的例子的结果来说,第一个 then() 方法的 return 内容,被第二个 then() 方法内函数拿到。经过上面的改写,实际上是:
- p1 实例转为成功状态,执行成功状态时的回调函数 resolve(),resolve 内部执行 resolveQueue ,其长度为1;
- p1 的 resolveQueue 被执行,执行完毕后立即执行 p1.then() 的成功的回调函数,以此类推;
那么,重点就是立即执行, 即前一个 resolveQueue 的执行 和后一个 状态更改为 fullfiled 两个时机是绑定在一起的。也就是需要重新包装 then() 方法传入的成功、失败的回调函数。
then()方法继续改写
then(fulFilledFun,rejectedFun){
// 首先返回一个 ourPromise 对象
return new ourPromise((resolve,reject)=>{
// 包装成功时得回调函数
const resolveFun = value =>{
// 执行回调函数,得到返回值 returnV
let returnV = fulFilledFun(value)
// 置 ourPromise 的状态为 fulfilled ,并且将返回值传递
resolve(returnV)
}
// 包装失败时得回调函数
const rejectFun = value =>{
let returnV = rejectedFun(value)
resolve(returnV)
}
// 更改实例的 resolveQueue
this.resolveQueue.push(resolveFun)
this.rejectQueue.push(rejectFun)
})
}
经过以上,就可以将开头的例子,成功打印出1,2,3了。注册流程上一小节已经阐述,来梳理注册以后的流程,如图所示。
经过改写之后,注册完成的各个实例的 resolveQueue 属性如图所示。现跟着图来梳理一下注册以后的运行流程:
- resolve(1) 执行,执行p1实例的成功的回调函数;
- 状态置为 fulfilled,执行对应resolveQueue,形参value 为 1,执行对应 fulFilledFun,打印1,返回值为 2;
- 执行到 resolve(2),执行p1.then() 实例的成功的回调函数;
- 状态置为 fulfilled,执行对应resolveQueue,形参value 为 2,执行对应 fulFilledFun,打印2,返回值为 3;
- 执行到 resolve(3),执行p1.then().then() 实例的成功的回调函数;
- 状态置为 fulfilled,执行对应resolveQueue,形参value 为 3,执行对应 fulFilledFun,打印3,返回undefined;
- 执行 resolve(undefined),状态置为 fulfilled,对应resolveQueue为空,结束。
这样简单的链式调用就基本实现了,现在来考虑一些细节问题。
then 细节实现
- then 方法接收的参数可能不是函数,要进行错误处理,并且不影响下一个then方法的执行,即值穿透;
const p1 = new Promise(
(resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 500);
}
)
p1
.then()
.then(res => {
console.log(res)
return 3
})
.then(res => {
console.log(res)
})
// 顺序输出
// 1
// 3
- 成功、失败状态下的回调函数,可能返回一个 Promise 对象;
const p1 = new Promise(
(resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 500);
}
)
p1
.then(res => {
console.log(res)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 500))
})
.then(res => {
console.log(res)
return 3
})
.then(res => {
console.log(res)
})
// 顺序输出
// 1
// 3
- Promise 实例的executor函数,是一个同步任务;
const p1 = new Promise(
(resolve, reject) => {
// 同步任务,所有代码顺序执行
resolve(1)
}
)
p1
.then(res => {
console.log(res)
return 2
})
.then(res => {
console.log(res)
return 3
})
.then(res => {
console.log(res)
})
// 顺序输出
// 1
// 2
// 3
- 需要支持 Promise.resolve(x)、Promise.reject(x),且返回一个Promise实例,状态直接是fulfilled、rejected;
Promise.resolve(1).then(
res=>{
console.log(res)
}
)
// 输出 1
Promise.resolve(
new ourPromise((resolve,reject)=>{
resolve('2')
})
)
ourPromise.reject(1)
代码如下:
then(fulFilledFun,rejectedFun){
// 第一个问题,接收参数不是函数时,构造一个新函数
// 成功的回调函数,需要完成值的传递任务,需要让下一个 then 方法获得上一个 then 方法的value
typeof fulFilledFun !== 'function' ? fulFilledFun = value => value :null
// 失败的回调函数,需要完成将错误原因输出,并结束
typeof rejectedFun !== 'function' ? rejectedFun = reason =>{
throw reason
} :null
return new ourPromise((resolve,reject)=>{
const resolveFun = value =>{
let returnV = fulFilledFun(value)
// 第二个问题,得到返回值后进行分类讨论
// 如果是 Promise 对象,就执行该对象的 then 方法,并绑定 当前Promise对象的resolve、reject
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}
const rejectFun = value =>{
let returnV = rejectedFun(value)
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}
// 第三个问题,如果是同步的情况,判断当前状态
// PENDING,则依次将函数放入队列
// FULFILLED,即执行到then方法时,已经执行了resolve,那么直接执行resolveFun
// REJECTED,即执行到then方法时,已经执行了reject,那么直接执行rejectFun
switch(this.status){
case PENDING:
this.resolveQueue.push(resolveFun)
this.rejectQueue.push(rejectFun)
break
case FULFILLED:
resolveFun(this.value)
break
case REJECTED:
rejectFun(this.value)
}
})
}
// 第四个问题,静态的resolve、reject方法
static resolve(value){
if(value instanceof ourPromise) return value
return new ourPromise(resolve => resolve(value))
}
static reject(value){
return new ourPromise((resolve,reject) => reject(value))
}
then 方法的错误捕获
前面的部分,我们没有注意到如果是函数内部出错,比如
const p1 = new ourPromise((resolve, reject) => {
throw new Error('执行器错误')
})
p1.then(res => {
console.log(res)
}, err => {
console.log(err)
console.log(err.message)
})
// 不会被打印
console.log('aaaa')
可以看到是executor抛出错误,那么
constructor(excutor) {
// ...
// 执行定义的实例传回来的回调函数,并传入两个参数
// excutor(resolve,reject)
try{
excutor(resolve,reject)
}catch(error){
reject(error)
}
}
还有可能是,then方法内传入的成功、失败的两个回调函数内部抛出错误,同上,进行改写then方法
const resolveFun = value =>{
try{
let returnV = fulFilledFun(value)
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}catch(error){
reject(error)
}
}
const rejectFun = value =>{
try{
let returnV = rejectedFun(value)
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}catch(error){
reject(error)
}
}
这样写,当前fulFilledFun、rejectedFun内部如果抛出错误,会被下一个then方法的reject捕捉到。
then 方法的微任务处理后完整代码
很多手写版本都是使用 setTimeout 去做异步处理,但是 setTimeout 属于宏任务,这与 Promise 是个微任务相矛盾,所以我打算选择一种创建微任务的方式去实现我们的手写代码。
这里我们有几种选择,一种就是 Promise A+ 规范中也提到的,process.nextTick( Node 端 ) 与MutationObserver( 浏览器端 ),考虑到利用这两种方式需要做环境判断,所以在这里我们就推荐另外一种创建微任务的方式 queueMicrotask。
// 实现基本的then函数
// 以及promise 的状态实时变更
// 实现一个promise多次then的调用
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class ourPromise {
constructor(excutor) {
// this指向实例对象
// 状态和返回值
this.status = PENDING
this.value = null
// 函数存储
this.resolveQueue = []
this.rejectQueue = []
let resolve = (val)=>{
if(this.status !== PENDING) return;
this.value = val
this.status = FULFILLED
this.resolveQueue.forEach(callback=>{
callback(val)
})
}
let reject = (err) =>{
if(this.status !== PENDING) return;
this.value = err
this.status = REJECTED
this.rejectQueue.forEach(callback=>{
callback(err)
})
}
// 执行定义的实例传回来的回调函数,并传入两个参数
try{
excutor(resolve,reject)
}catch(error){
reject(error)
}
}
// then方法的两个参数是两个回调函数
then(fulFilledFun,rejectedFun){
// 判断当前函数是否为函数
typeof fulFilledFun !== 'function' ? fulFilledFun = value => value :null
typeof rejectedFun !== 'function' ? rejectedFun = reason =>{
throw reason
} :null
return new ourPromise((resolve,reject)=>{
const resolveFun = value =>{
// 创建一个微任务等待 Promise 初始化
queueMicrotask (()=>{
try{
let returnV = fulFilledFun(value)
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}catch(error){
reject(error)
}
})
}
const rejectFun = value =>{
queueMicrotask(()=>{
try{
let returnV = rejectedFun(value)
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}catch(error){
reject(error)
}
})
}
switch(this.status){
case PENDING:
this.resolveQueue.push(resolveFun)
this.rejectQueue.push(rejectFun)
break
case FULFILLED:
resolveFun(this.value)
break
case REJECTED:
rejectFun(this.value)
}
})
}
static resolve(value){
if(value instanceof ourPromise) return value
return new ourPromise(resolve => resolve(value))
}
static reject(value){
return new ourPromise((resolve,reject) => reject(value))
}
}