Promise
作为当前前端异步任务处理的基本类,在面试中也经常遇到,除了常见的 Promise.all/race
等方法手写代码,对于 Promise
本身的实现也经常遇到。
今天带来一篇一步步实现 Promise
类的文章,希望看完之后,对大家日常工作中 Promise
的使用和面试能有一些帮助。
本文主要介绍如何理解链式调用以及resolvePromise的作用,有需要的可以直接翻到下文(重点)部分看即可。
promise是一种异步编程的解决方案,用于处理多层回调嵌套的问题。
promise是一个对象,从它可以获取异步操作的消息,能够解决回调地狱的问题。
目前promise已经是es6的内置对象。
基于此我们可以初步构建promise的雏形
class myPromise {
// 定义promise的基础状态
static PENDING = "pending";
static FULFILLED = "fulfilled";
static REJECTED = "rejected";
// promise构造方法
constructor(func) {
this.promiseState = myPromise.PENDING // 初始化Promise的状态
this.promiseResult = null // 初始化Promise的result
try {
// 向func传入resolve和reject并且绑定当前的promise实例
// 当func执行到对应位置的时候就可以去显式的调用resolve或reject更新当前promise
// 的状态和结果
func(this.resolve.bind(this), this.reject.bind(this))
}
catch(err) {
// 如果异常则将promise的状态改为失败并且将错误设置为结果
this.reject(err)
}
}
resolve(value) {
// 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
// 因为状态被更新了后续都不会是等待,所以状态就唯一了
if(this.promiseState == myPromise.PENDING) {
this.promiseState = myPromise.FULFILLED
this.promiseResult = value
}
}
reject(reason) {
// 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
// 因为状态被更新了后续都不会是等待,所以状态就唯一了
if(this.promiseState == myPromise.PENDING) {
this.promiseState = myPromise.REJECTED
this.promiseResult = reason
}
}
}
简单测试下上面的代码:
使用过Promise都知道Promise有.then的操作,这里会传入两个函数分别代表成功的回调,失败的回调。
同时传入的函数会接收promiseResult作为参数,然后通过判断当前promise的结果选择执行成功的回调还是失败的回调。
基于此我们可以构建promise.then的雏形
class myPromise {
...
then(fulfilledFunc, rejectedFunc) {
if(this.promiseState == myPromise.FULFILLED) {
fulfilledFunc(this.promiseResult);
}
if(this.promiseState == myPromise.REJECTED) {
rejectedFunc(this.promiseResult);
}
}
}
简单测试下then操作
看着是没啥问题,到这里.then操作已经基本实现了,then中可以执行promise的回调。
还是上面的代码我们执行的时候顺序如下所示
但是如果执行原生的Promise,顺序如下所示:
所以我们在处理的时候需要注意resolve和reject,调用的时候要设置异步执行,所以基于此我们可以更新下代码
class myPromise {
...
then(fulfilledFunc, rejectedFunc) {
if(this.promiseState == myPromise.FULFILLED) {
setTimeout(() => {
fulfilledFunc(this.promiseResult);
})
}
if(this.promiseState == myPromise.REJECTED) {
setTimeout(() => {
rejectedFunc(this.promiseResult);
})
}
}
}
更改之后重新测试下代码,这下子对胃了。
此时我们的代码执行已经和原生Promise很类似了,但是如果在Func里面添加setTimeout这种异步操作,可以看到最后我们什么都没输出。
原因是setTimeout是宏任务,then是微任务,执行Func的时候resolve被加到宏任务里面了,然后因为微任务这里会先执行所以就执行了。
但是此时并没有发生resolve或reject,因此状态还是pending,而之前的then操作里面没有处理pending的情况,当执行完宏任务之后then操作已经执行过了,因此什么都不会输出
为了处理内部存在定时器的情况,可以使用发布订阅的方式去解决。
class myPromise {
...
// promise构造方法
constructor(func) {
this.promiseState = myPromise.PENDING // 初始化Promise的状态
this.promiseResult = null // 初始化Promise的result
this.fulfilledFuncCallback = [] // 定义数组暂存promise的成功回调
this.rejectedFuncCallback = [] // 定义数组暂存promise的失败回调
...
}
resolve(value) {
// 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
// 因为状态被更新了后续都不会是等待,所以状态就唯一了
if(this.promiseState == myPromise.PENDING) {
this.promiseState = myPromise.FULFILLED
this.promiseResult = value
this.fulfilledFuncCallback.forEach((item)=>{
item()
})
}
}
reject(reason) {
// 因为promise的状态只能改变一次,所以这里判断是等待就去更新这个状态
// 因为状态被更新了后续都不会是等待,所以状态就唯一了
if(this.promiseState == myPromise.PENDING) {
this.promiseState = myPromise.REJECTED
this.promiseResult = reason
this.rejectedFuncCallback.forEach((item)=>{
item()
})
}
}
then(fulfilledFunc, rejectedFunc) {
// 如果是Pending状态将回调收集起来
if(this.promiseState == myPromise.PENDING) {
this.fulfilledFuncCallback.push(()=>{
setTimeout(() => {
fulfilledFunc(this.promiseResult);
})
})
this.rejectedFuncCallback.push(()=>{
setTimeout(() => {
rejectedFunc(this.promiseResult);
})
})
}
if(this.promiseState == myPromise.FULFILLED) {
setTimeout(() => {
fulfilledFunc(this.promiseResult);
})
}
if(this.promiseState == myPromise.REJECTED) {
setTimeout(() => {
rejectedFunc(this.promiseResult);
})
}
}
}
同样是执行上面的代码,可以看到这里就可以正常输出了。
!!!重点 !!!
介绍下,promise具有链式调用的特点,即每一次promise的.then操作都会返回一个新的promise,然后新的promise又可以继续.then,如下所示:
那么如何实现promise这个功能呢?
很简单在then中返回一个新的promise因为promise自带then,因此就可以实现链式调用!
很多文章对于这块其实讲述的并不是很清楚,这里我说一下我对这部分的理解:
1.promise在new的时候会在执行构造方法时,执行传入func。
2.resolve和reject会被作为参数传入到func中。
3.因为resolve和reject可以更新promise的状态和结果,所以func执行完之后promise的状态和结果就决定了。
4.所以在new promise实例这个过程中,无论同步或异步,这个过程完成之后都会确定当前promise的状态和结果。
5.链式调用需要返回一个promise,所以会在then函数中new promise。
6.实例化promise的时候需要传入一个func,并执行,如(3)所示,那这个func怎么获得呢?
7.所以我们会通过前一个promise的状态和结果以及then方法的两个入参(成功回调,失败回调)去为新的promise构造一个func。
8.在then中实例化一个promise,并执行(7)中构造的func,当func执行完之后,我们新实例化的promise状态也确定了,最后返回的就是一个状态和结果确定的promise实例。
9.如果针对上一个实例继续执行.then,相当于继续从(5)到(8)的操作
基于前一个promise的状态,结果以及then的两个参数(成功回调,失败回调)去为新的promise创建func,创建新的promise实例。
执行完之后,得到一个状态确定结果确定promise实例,并return出去就行。继续then就继续循环即可。
不懂链式调用的朋友,可以细品一下上面这段话。希望有所帮助。
基于此我们可以设计一下简单的链式调用。
在正式开始之前先看看es6内置的promise在链式调用的时候是什么样子的。
可以看到第一个then是接收第一个promise的失败结果,而后续的then返回的promise都是成功的状态。
因此我们知道then中promise都会用resolve去接收执行结果。
说明无论第一个promise状态是成功或失败,后续的promise都会用resolve去接收成功回调或失败回调中函数的执行结果。
有朋友可能会疑惑?
那reject来干嘛,其实reject更多的是处理函数的异常情况。
我在这里定义了一个未存在的变量 fakeres 所以执行成功回调时报错了返回的promise就是reject状态的。
(注:这里为什么会执行成功回调呢,因为前一个promise的.then已经执行完,这里面resolve了then的失败回调的执行结果,所以第二个then的时候,它的promise就是个成功的状态)
class myPromise {
...
then(fulfilledFunc, rejectedFunc) {
let newPromise = new myPromise((resolve, reject) => {
// 如果是Pending状态将回调收集起来
if (this.promiseState == myPromise.PENDING) {
this.fulfilledFuncCallback.push(() => {
setTimeout(() => {
try {
let x = fulfilledFunc(this.promiseResult);
resolve(x);
} catch (error) {
reject(error);
}
});
});
this.rejectedFuncCallback.push(() => {
setTimeout(() => {
try {
let x = rejectedFunc(this.promiseResult);
resolve(x);
} catch (error) {
reject(error);
}
});
});
}
if (this.promiseState == myPromise.FULFILLED) {
setTimeout(() => {
try {
let x = fulfilledFunc(this.promiseResult);
resolve(x);
} catch (error) {
reject(error);
}
});
}
if (this.promiseState == myPromise.REJECTED) {
setTimeout(() => {
try {
let x = rejectedFunc(this.promiseResult);
resolve(x);
} catch (error) {
reject(error);
}
});
}
});
return newPromise;
}
}
还是用上面的例子,执行一下:
可以看到链式调用这里就实现了。
基于上文我们简单的实现了一个promise,但是并不完整,为啥不完整?
我们可以看then执行回调的地方,我们获取到回调执行结果之后就直接resolve出去了。
try {
let x = rejectedFunc(this.promiseResult);
resolve(x);
} catch (error) {
reject(error);
}
乍一看没有问题 , 但是我们需要考虑到 回调执行结果的多样性 。
有可能是成功回调或失败回调不是一个函数而是一个基本数据类型。
成功回调或失败回调的返回结果也可能返回了一个函数或者promise。
也是基于promise规范的 2.2.7.1,当执行函数的 rejectedFunc或fulfilledFun返回一个 x 的时候要执行promise的解决过程。
我们需要将执行回调的地方修正到如下所示
try {
// 如果成功回调不是函数则直接resolve
if(typeof fulfilledFunc !== 'function') {
resolve(this.promiseResult)
} else {
// 如果执行完成功回调返回x,就promise的执行过程resolvePromise
let x = fulfilledFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, rejec);
}
} catch (error) {
reject(error);
}
基于此我们完善一下promise(看注释处)
class myPromise {
...
then(fulfilledFunc, rejectedFunc) {
let newPromise = new myPromise((resolve, reject) => {
// 如果是Pending状态将回调收集起来
if (this.promiseState == myPromise.PENDING) {
this.fulfilledFuncCallback.push(() => {
setTimeout(() => {
try {
// 如果成功回调不是函数则直接resolve
if(typeof fulfilledFunc !== 'function') {
resolve(this.promiseResult)
} else {
// 如果执行完成功回调返回x,就promise的执行过程resolvePromise
let x = fulfilledFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
});
this.rejectedFuncCallback.push(() => {
setTimeout(() => {
try {
// 如果失败回调不是函数则直接reject
if(typeof rejectedFunc !== 'function') {
reject(this.promiseResult)
} else {
// 如果执行完失败回调返回x,就promise的执行过程resolvePromise
let x = rejectedFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
});
}
if (this.promiseState == myPromise.FULFILLED) {
setTimeout(() => {
try {
// 如果成功回调不是函数则直接resolve
if(typeof fulfilledFunc !== 'function') {
resolve(this.promiseResult)
} else {
// 如果执行完成功回调返回x,就promise的执行过程resolvePromise
let x = fulfilledFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
}
if (this.promiseState == myPromise.REJECTED) {
setTimeout(() => {
try {
// 如果失败回调不是函数则直接reject
if(typeof rejectedFunc !== 'function') {
reject(this.promiseResult)
} else {
// 如果执行完失败回调返回x,就promise的执行过程resolvePromise
let x = rejectedFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
}
});
return newPromise;
}
}
到这里我们的then操作基本就完成了!!!
最后实现一下resolvePromise(注释说明了每一步的作用,根据promise规范操作的)
function resolvePromise(newPromise, x, resolve, reject) {
if (x === newPromise) {
// 因为x是回调的结果值,如果x指向newPromise即自己,那么会重新解析自己,导致循环调用
throw new TypeError("禁止循环调用");
}
// 如果x是一个Promise,我们必须等它完成(失败或成功)后得到一个普通值时,才能继续执行。
// 那我们把要执行的任务放在x.then()的成功回调和失败回调里面即可
// 这就表示x完成后就会调用我们的代码。
// 但是对于成功的情况,我们还需要再考虑下,x.then成功回调函数的参数,我们称为y
// 那y也可能是一个thenable对象或者promise
// 所以如果成功时,执行resolvePromise(promise2, y, resolve, reject)
// 并且传入resolve, reject,当解析到普通值时就resolve出去,反之继续解析
// 这样子用于保证最后resolve的结果一定是一个非promise类型的参数
if (x instanceof myPromise) {
x.then((y) => {
resolvePromise(newPromise, y, resolve, reject);
}, r => reject(r));
}
// (x instanceof myPromise) 处理了promise的情况,但是很多时候交互的promise可能不是原生的
// 就像我们现在写的一个myPromise一样,这种有then方法的对象或函数我们称为thenable。
// 因此我们需要处理thenable。
else if ((typeof x === "object" || typeof x === "function") && x !== null ) {
try {
// 暂存x这个对象或函数的then,x也可能没有then,那then就会得到一个undefined
var then = x.then;
} catch (e) {
// 如果读取then的过程中出现异常则reject异常出去
return reject(e);
}
// 判断then是否函数且存在,如果函数且存在那这个就是合理的thenable,我们要尝试去解析
if (typeof then === "function") {
// 状态只能更新一次使用一个called防止反复调用
// 因为成功和失败的回调只能执行其中之一
let called = false;
try {
then.call(
x,
(y) => {
// called就是用于防止成功和失败被同时执行,因为这个是thenable,不是promise
// 需要做限制如果newPromise已经成功或失败了,则不会再处理了
if (called) return;
called = true;
resolvePromise(newPromise, y, resolve, reject);
},
(r) => {
// called就是用于防止成功和失败被同时执行,因为这个是thenable,不是promise
// 需要做限制如果newPromise已经成功或失败了,则不会再处理了
if (called) return;
called = true;
reject(r);
}
);
// 上面那一步等价于,即这里把thenable当作类似于promise的对象去执行then操作
// x.then(
// (y) => {
// if (called) return;
// called = true;
// resolvePromise(newPromise, y, resolve, reject);
// },
// (r) => {
// if (called) return;
// called = true;
// reject(r);
// }
// )
} catch (e) {
// called就是用于防止成功和失败被同时执行,因为这个是thenable,不是promise
// 需要做限制如果newPromise已经成功或失败了,则不会再处理了
if (called) return;
called = true;
reject(e);
}
} else {
// 如果是对象或函数但不是thenable(即没有正确的then属性)
// 当成普通值则直接resolve出去
resolve(x);
}
}
// 如果既不是promise,也不是非null的对象或函数,当成普通值则直接resolve出去
else {
return resolve(x);
}
}
在理解resolvePromise的时候可以这样子去思考:
promise需要resolve或reject出去一个普通值。(前提)
但是我们在获取成功回调结果或失败回调结果时。
可能拿到的结果是promise或者thenable,因此 resolvePromise 就针对结果去解析promise,thenable,使其最后也返回一个普通值的过程。
promise的核心是链式调用,我们需要注意的是到底promise在then的时候做了什么事情,以及promise在执行then里面的成功回调或失败回调的时候是一个什么状态。
then的时候返回了一个新的promise,而且此时调用then的promise的状态和结果已经决定了。
创建promise的时候我们需要往里面设置一个func,并往里面传入resolve和reject,因为我们在then的时候会创建一个promise,那么这个promise的func怎么来呢?
func就是通过上一个promise的状态和结果以及then传入的成功回调以及失败回调去构建的,并且在构建完之后执行这个func,确定我们新的promise的状态和结果。
当我们把这个新的promise return出去之后在then的时候就可以无限链式调用了。
值得注意的时候我们每一次promise的结果都应该是一个普通值,而不是promise或者thenable,如果我们在调用成功回调或失败回调时得到一个function就应该判断是否promise或者thenable,如果是则继续解析,这就是resolvePromise的作用。
通过promise我们解决了回调地狱的问题,并且作为异步编程的一种解决方案,广泛的应用于日常开发。
class myPromise {
static PENDING = "pending";
static FULFILLED = "fulfilled";
static REJECTED = "rejected";
constructor(func) {
this.promiseState = myPromise.PENDING;
this.promiseResult = null;
this.fulfilledFuncCallback = [];
this.rejectedFuncCallback = [];
try {
func(this.resolve.bind(this), this.reject.bind(this));
} catch (err) {
this.reject(err);
}
}
resolve(value) {
if (this.promiseState == myPromise.PENDING) {
this.promiseState = myPromise.FULFILLED;
this.promiseResult = value;
this.fulfilledFuncCallback.forEach((item) => {
item();
});
}
}
reject(reason) {
if (this.promiseState == myPromise.PENDING) {
this.promiseState = myPromise.REJECTED;
this.promiseResult = reason;
this.rejectedFuncCallback.forEach((item) => {
item();
});
}
}
then(fulfilledFunc, rejectedFunc) {
let newPromise = new myPromise((resolve, reject) => {
if (this.promiseState == myPromise.PENDING) {
this.fulfilledFuncCallback.push(() => {
setTimeout(() => {
try {
if (typeof fulfilledFunc !== "function") {
resolve(this.promiseResult);
} else {
let x = fulfilledFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
});
this.rejectedFuncCallback.push(() => {
setTimeout(() => {
try {
if (typeof rejectedFunc !== "function") {
reject(this.promiseResult);
} else {
let x = rejectedFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
});
}
if (this.promiseState == myPromise.FULFILLED) {
setTimeout(() => {
try {
if (typeof fulfilledFunc !== "function") {
resolve(this.promiseResult);
} else {
let x = fulfilledFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
}
if (this.promiseState == myPromise.REJECTED) {
setTimeout(() => {
try {
if (typeof rejectedFunc !== "function") {
reject(this.promiseResult);
} else {
let x = rejectedFunc(this.promiseResult);
resolvePromise(newPromise, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
}
});
return newPromise;
}
}
function resolvePromise(newPromise, x, resolve, reject) {
if (x === newPromise) {
throw new TypeError("禁止循环调用");
}
if (x instanceof myPromise) {
x.then(
(y) => {
resolvePromise(newPromise, y, resolve, reject);
},
(r) => reject(r)
);
}
else if ((typeof x === "object" || typeof x === "function") && x !== null) {
try {
var then = x.then;
} catch (e) {
return reject(e);
}
if (typeof then === "function") {
let called = false;
try {
then.call(
x,
(y) => {
if (called) return;
called = true;
resolvePromise(newPromise, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
else {
return resolve(x);
}
}
觉得本文有用的小伙伴,可以帮忙点个“在看”,让更多的朋友看到咱们的文章。
最后,再给“前端面试题宝典”的辅导服务打下广告,目前有面试全流程辅导、简历指导、模拟面试、零基础辅导和付费咨询的增值服务,如果有感兴趣的伙伴,可以联系小助手(微信号:interview-fe)了解详情哦~