前言
在中之前的文章中,我简单聊了聊JS中的异步发展史,从最开始的回调地狱,到后来es5的promise,再到通过发布订阅的思想去实现异步处理。之所以需要异步处理,是因为JS在设计之初只是为了方便浏览器与用户进行交互,所以不需要太高的性能而被设计为单线程语言。今天这篇文章主要是聊聊promise对象身上的面试高频考点------then,race以及all方法。
正文
在之前的文章中简单手撕了一下promise对象的源码,虽然十个"盖中盖"版本,但也算是基本实现了promise身上该有的东西。要是放在三五年前,这个已经差不多够拿个实习了,但是这行都卷烂了,不上点强度不行了。
js
constructor(excutor) {
this.state = 'pending'
// promise的状态
this.value = undefined
// resloved的参数
this.reason = undefined
// rejected的参数
this.onResolvedCallbacks = []
// resolved状态下调用的函数
this.onRejectedCallbacks = []
// rejected状态下调用的函数
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'resolved'
this.value = value
this.onResolvedCallbacks.forEach(item => {
item(value)
// 将所有then接受的回调函数全部执行
})
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach(item => {
item(reason)
// 将所有then接受的回调函数全部执行
})
}
}
excutor(resolve, reject)
}
从promise的用法就能看出,then,all,race这三个方法都是promise实例对象能拿到的,所以我们应该将这三个方法写在构造器的外面
js
class MyPromise {
constructor(excutor) {
}
then(){}
race(){}
all(){}
}
then
关于then有三个需要注意的地方,可以用下面的demo来解释
js
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a is running');
reject('a is done')
}, 1000);
})
}
function b() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('b is running');
reject('b is done')
}, 1000);
})
}
function c() {
console.log('c is done');
}
a()
.then(() => { }, (res) => {
console.log(res);
return b()
})
.then(() => { }, (res) => {
console.log(res);
c()
})
- 首先,这里可以看到then方法实际上可以接受两个参数,第二个参数就是在promise对象在rejected状态下被调用,这和catch方法几乎是一样的。所以then方法可以接受两个回调函数作为参数。所以then方法的雏形可以写成这样
js
then(onResolved, onRejected) {
onResolved = typeof onResolved === "function" ? onResolved : () => { }
onRejected = typeof onRejected === "function" ? onRejected : () => { }
// 保证onResolved和onRejected都是函数体
return newPromise
//返回默认的promise对象
}
-
其次,当我们调用then方法的时候,then是立即触发的,异步代码虽然是放在了then里面,但并没有被执行,也就是说,then方法是没有资格去调用自己的参数。 then的参数是否被调用实际上取决于promise对象的状态,而根据promise的用法以及之前文章中提到的,修改promise对象的方法就是调用resolve。那么我们就可以理解为,then方法只能将自己的参数存到上一篇文章提到的onResolvedCallbacks数组中,而resolve则可以遍历该数组并把所有数组元素(也就是then接受的函数)全部调用。
-
最后,then方法会默认返回一个promise对象,而当我们在then方法中返回了promise对象的时候,则会将默认的promise对象覆盖掉。
这样一来,promise的then方法就大概理清楚了。接下来就到了then身上最重要也是最复杂的一点了,那就是判断当前promise对象的状态。
首先要知道的是,根据原型链的调用规则以及this的指向,在then方法中的this指向的是当前的对象。还是拿刚刚的demo来举例
js
a()
.then(() => { }, (res) => {
console.log(res);
return b()
})
.then(() => { }, (res) => {
console.log(res);
c()
})
在这里第一个then(也就是第二行的then)的this指向的就是a函数返回的promise对象,在这里我就简称为AP。那么then函数就需要判断AP的状态,是未经改变的pending,还是resolved或rejected,那么then方法就可以写成这样。
js
then(onResolved, onRejected) {
onResolved = typeof onResolved === "function" ? onResolved : () => { }
onRejected = typeof onRejected === "function" ? onRejected : () => { }
// 保证onResolved和onRejected都是函数体
if (this.state === 'resolved'){}
if (this.state === 'rejected'){}
if (this.state === 'pending'){}
return newPromise
//返回默认的promise对象
}
resolved状态
当前一个promise对象(也就是前面提到的AP)状态为resolved的时候,意味着then里面的第一个参数该被执行了。不得不说的是,在官方打造的then方法中,这里是微任务,而要实现相同的微任务效果,属实是太为难我这个大三学生了,所以各位看官老爷,还请原谅菜的抠脚的笔者擅自使用定时器去用宏任务实现丐版的then方法。
另外还有一点比较复杂,也是令我脑婆头破的地方,就是之前提到的修改then方法默认返回的promise对象,并且还要求能实现then方法的链式调用,毕竟总不能AP的then方法用完了之后BP(也就是b()返回的promise对象)的then方法就没用了吧?所以这里需要拿到AP的then方法的第一个参数返回的promise对象作为resolve函数的参数,这样就能够将原有的默认promise对象修改成新的promise对象了(也就是BP)。
另一个在我学习过程中觉得有些绕的地方就是AP的then方法的第一个参数(回调函数)的参数(res)是怎么拿到的。这里可以看到在之前的构造器中我们将promise对象的value初始值设置为了undefined。随后当我们调用resolve函数的时候,传入的参数会被存到当前promise对象的value身上。所以当我们调用AP的then方法的第一个参数(回调函数)时,需要传的参数就是AP中resolve的值,也就是AP的value,所以通过this.value就可以拿到对应的参数了。最终代码如下
js
if (this.state === 'resolved') {
//then前面的promise对象状态已经同步变更完成了
setTimeout(() => {
// 官方是微任务,这里用宏任务简化一下
try {
const result = onResolved(this.value)
//将BP赋值给result
resolve(result)
//这里放的应该是result里面的resolve的参数
//这里将整个默认的promise对象更换成BP
} catch (error) {
reject(error)
}
})
}
rejected状态
聊完了resolved状态咱直接来聊rejected状态,二者几乎是一模一样,唯一不同的地方就在于前面提到过的then接受的第二个参数(回调函数)需要在当前promise对象(这里就是AP)状态为rejected时被调用。resolved调用onResolved,那rejected不就调用onRejected呗。直接copy,换一下调用的函数就行,依旧是采用try-catch的形式
js
if (this.state === 'rejected') {
setTimeout(() => {
// 官方是微任务,这里用宏任务简化一下
try {
const result = onRejected(this.value)
resolve(result)
} catch (error) {
reject(error)
}
})
}
pending状态
pending状态意味着AP还没有resolve过,但是then不等人啊,then里面是异步或同步关我then本身什么事?而之前也提到过,then本身确实没有任何调用自己参数的权利 就像我没有权利调用我心中的那个她
,当AP状态为pending的时候,then唯一能做的就是将拿到的所有参数全部一一对应存到相应的数组
js
if (this.state === 'pending') {//缓存then的回调函数
this.onResolvedCallbacks.push((value) => {
setTimeout(() => {
// 保证将来onresolved被调用的时候是一个异步函数
try {
const result = onResolved(value)
resolve(result)
} catch (error) {
reject(error)
}
})
})
this.onRejectedCallbacks.push((reason) => {
setTimeout(() => {
// 保证将来onresolved被调用的时候是一个异步函数
try {
const result = onResolved(reason)
resolve(result)
} catch (error) {
reject(error)
}
})
})
}
如此一来,then方法就算是基本实现了。接下来简单讲讲race和all方法。
race
首先需要知道的是,当我们调用race方法的时候并非通过某个promise对象去调用,而是通过构造函数去访问的。所以当我们在写race方法的时候需要通过static关键字去将其设置为静态方法,这样就只能通过构造函数访问而不能通过实例对象访问了。
另外,与then方法相同的是,race同样会返回一个promise对象。而race则是将自己内部所有的回调函数全部触发,然后看谁的状态最先变更,就拿着谁传出来的值resolve/reject出去。
原理大致清楚了,实现起来也不难。race接受一个数组,那么我们自己写的race方法也就收一个数组。随后通过for-of去遍历并直接调用每个元素的then方法,这个过程非常快,快到不同位置的then的调用的时间差几乎可以省略。接下来就静等最先被resolve或reject出来的值,并将它再次resolve或reject一遍,就可以改变race默认返回的promise对象了。
js
static race(promises) {
return new MyPromise((resolve, reject) => {
// 看promises里面谁的状态最先变更
for (let promise of promises) {
promise.then(
(value) => {
resolve(value)
},
(reason) => {
reject(reason)
}
)
}
})
}
all
all
方法与race
不同,它等待数组中的所有Promise都变为fulfilled(已完成),并将每个Promise的结果收集到一个数组中,然后以这个结果数组去resolve一个新的Promise。如果任何一个Promise被rejected(已拒绝),则all
方法会立即reject,不再等待其他Promise的结果。
首先明确一点,all
方法也是通过构造函数直接访问的静态方法,所以我们同样使用static
关键字定义。all
接收一个Promise数组作为参数,其目标是监视数组中的每个Promise,直至所有Promise都完成,然后以这些Promise的resolve值组成的新数组作为结果。
实现步骤如下:
- 创建一个新的Promise :这是
all
方法返回的Promise,我们将用它来封装所有输入Promise的最终结果或第一个出现的错误。 - 初始化结果数组:我们需要一个数组来收集所有Promise的resolve值。同时,设立一个计数器来跟踪已完成的Promise数量,当这个计数等于输入数组长度时,表示所有Promise均已完成。
- 遍历并处理每个Promise :通过
for...of
循环遍历输入的Promise数组。对于每个Promise,我们调用其.then
方法来处理成功的情况,并在回调中更新结果数组和计数器。同时,我们也调用.catch
来捕捉第一个错误,并立即reject我们的主Promise。 - 汇总结果:当所有Promise都成功resolve时,我们使用收集到的结果数组来resolve我们的主Promise。
js
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let completedCount = 0;
// 确保输入的是数组且非空
if (!Array.isArray(promises) || promises.length === 0) {
resolve(results);
return;
}
总结
以上便是我对promise的学习心得,希望能够帮助到各位,祝各位看官老爷0 waring(s),0 error(s)。