浅谈面试中的高频考点—异步发展史(二)

前言

在中之前的文章中,我简单聊了聊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值组成的新数组作为结果。

实现步骤如下:

  1. 创建一个新的Promise :这是all方法返回的Promise,我们将用它来封装所有输入Promise的最终结果或第一个出现的错误。
  2. 初始化结果数组:我们需要一个数组来收集所有Promise的resolve值。同时,设立一个计数器来跟踪已完成的Promise数量,当这个计数等于输入数组长度时,表示所有Promise均已完成。
  3. 遍历并处理每个Promise :通过for...of循环遍历输入的Promise数组。对于每个Promise,我们调用其.then方法来处理成功的情况,并在回调中更新结果数组和计数器。同时,我们也调用.catch来捕捉第一个错误,并立即reject我们的主Promise。
  4. 汇总结果:当所有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)。

相关推荐
一条晒干的咸魚2 分钟前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷13 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
sinat_3842410940 分钟前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
小牛itbull1 小时前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i1 小时前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_1 小时前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
zhang-zan2 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
ZBY520312 小时前
【Vue】 npm install amap-js-api-loader指南
javascript·vue.js·npm
前端拾光者3 小时前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化