一次在开发中遇到的异步代码问题

前言

这天早上leader突然过来跟我说,首页的这个折线图为什么比别的图表数据渲染出来的慢,你看下是什么问题解决一下。经过排查找到了问题,代码如下:

JavaScript 复制代码
// 模拟异步请求
function request(value) {
  return new Promise((resolve) => {
      setTimeout(() => {
          resolve(value);
          // 模拟接口响应慢
      }, 3000);
  });
}

// 获取折线图数据
async function getLineData() {
    const lineData = [];
    // 模拟请求7天的数据
    for (let i = 0; i < 7; i++) {
      const result = await request(i);
      lineData.push(result);
    }
    console.log('所有结果:', lineData);
    // 剩下操作...
}

问题出现在我在循环中使用await关键字,由于接口原因需要一天一天的去查所以我这里做了个循环去调用接口,展示需要7天的统计所以需要等待获取到所有数据后在渲染页面,刚开始因为数据量不大接口响应速度快这样写问题就没有出现,随着数据量变大这个接口响应速度变慢了问题就出现了,上面模拟一次接口请求响应需要3秒的时间的情况,请求7次最后完成的时间就是21秒,也就是说实际需要21秒后才会渲染折线图的数据。修改后的代码如下:

JavaScript 复制代码
// 获取折线图数据
// 写法一:
async function getLineData() {
    const promises = [];
    // 模拟请求7天的数据
    for (let i = 0; i < 7; i++) {
       promises.push(request(i))
    }
    // 需要获取到所有请求的结果在渲染图表,所以这里使用 allSettled 
    const results = await Promise.allSettled(promises)
    const lineData = results.map(item => item.value)
    console.log('所有结果:', lineData);
    // 剩下操作...
}

// 写法二:
function getLineData() {
    const lineData = [];
    for (let i = 0; i < 7; i++) {
        request(i).then((res) => {
            lineData.push(res);
            if (i == 6) {
                console.log('所有结果:', lineData);
                // 剩下操作...
            }
        });
    }
}

修改完后渲染时间就只有3秒了比之前快了7倍

虽然解决了问题,但是在解决问题中我产生了一系列的问题让我不得不重新彻底学习了一下关于Promise、async、await、事件循环等异步知识。

生成器函数、yield关键字

生成器函数执行后不会执行函数体中的代码,会返回一个生成器(特殊的迭代器),在生成器函数中可以通过yield关键字控制函数中代码的执行流程(暂停代码往下执行),需要调用生成器的.next()方法让代码往下执行,如果有多个yield关键字需要不断的去执行.next()方法,直到遇到return就表示代码执行完了。

async、await的没有出现前,是如何使用生成器函数、yield解决异步代码问题的。将上面修改前的getLineData函数使用生成器函数实现一下,代码如下:

JavaScript 复制代码
// 获取折线图数据
function* getLineData() {
    const lineData = [];
    // 模拟请求7天的数据
    for (let i = 0; i < 7; i++) {
        // promise有结果后获取到result
        const result = yield request(i);
        lineData.push(result);
        console.log('result', result);
    }
    console.log('所有结果:', lineData);
    // 剩下操作...
}

// 自动执行生成器的函数
function execGen(genFn) {
    const gen = genFn();
    function execCode(res) {
        const { value: promise, done } = gen.next(res);
        if (done) return;
        promise.then((res) => {
            // 需要等到promise有结果后在继续执行.next()操作
            execCode(res);
        });
    }
    execCode();
}

execGen(getLineData);

看到execGen函数在递归调用.next()方法的时候,是等待Promise有结果后才进行调用的,request方法需要等待3秒后才会有结果,这样的话我们执行完getLineData函数的时候就需要21秒,是不是跟使用async、await很像,只不过我们需要去编写一个自动化执行生成器的函数。实际上async、await就是生成器函数、yield、Promise的语法糖,其实内部就是帮助我们做这件事情。

async、awiat

通过在函数前添加async关键字可以让函数变成异步函数(默认情况下异步函数的执行跟普通函数一样是同步执行的),在异步函数中可以使用await关键字,await关键字后面跟着是普通的代码和不使用await关键字是没有区别的,如果await关键字后面的代码返回的是一个Promise的话,那么await关键字会等待Promise有结果后才会继续执行之后的代码。

JavaScript 复制代码
async function foo() {
   // 不需要等待
   const value = await 123
   console.log('value', value)
   // 需要等待 promise 有结果后才会往下执行
   const result = await request(value)
   // promise有结果后下面执行的代码....
}

foo()

回过头来看看修改前的getLineData函数,我们就明白为什么修改前的代码需要21秒之后才渲染图表了,因为在循环中我们使用了await关键字,而await关键字后面执行request方法返回值是一个Promiseawait关键字就会去等待Promise有结果后才会执行下一次的代码,request需要等待3秒才会有结果代码中又循环7次,所以就需要21秒后才会去渲染图表。

事件循环

什么是事件循环?

在浏览器中每个tab标签都会开启一个进程,每js就是进程中的一个线程,我们执行的js代码都是由这个线程去执行的也称为主线程,而js代码执行时遇到异步代码的时候,如网络请求、setTimeout等,会交给浏览器的另一个线程去执行执行完成后会将通知的回调方法,如setTimeout传入的回调方法放入到一个队列当中(微任务队列),并不会堵塞主线程的执行,这时候等到主线程的代码都执行完后,主线程会去不断的查看任务队列中是否有任务,如果有就取出来去执行,执行发现又有异步代码的时候又交给浏览器的另一个线程,这样循环反复就被称之为事件循环

宏任务、微任务

在执行异步任务的线程当中维护着两个任务队列,一个是微任务队列then方法传入的回调、setTimeout传入的回调等,一个是宏任务队列setTimeout、网络请求等,之后主线程执行完代码后就会去宏任务队列中去取任务执行,但是浏览器有个机制,在执行每个宏任务之前会先查看微任务队列中是否有任务,如果有就执行完后再执行宏任务,如果没有就直接执行宏任务,以此循环。

JavaScript 复制代码
function exec() {
    setTimeout(() => {
            console.log('setTimeout');
    }, 3000);

    request('request').then((res) => {
            console.log(res);
    });
}

exec();

Promise.allSettled

Promise.allSettled方法会等到所有Promise有结果后才会决定状态,无论是fulfilled,还是rejected,并且Promise.allSettled返回的Promise一定是fulfilled状态。

简单的实现下allSettled方法:

JavaScript 复制代码
function allSettled(promises) {
    return new Promise((resolve) => {
        const results = [];
        promises.forEach((promise) => {
            const fulfilled = (res) => {
                results.push({ status: 'fulfilled', value: res });
                if (results.length === promises.length) resolve(results);
            };
            const rejected = (err) => {
                results.push({ status: 'rejected', reason: err });
                if (results.length === promises.length) resolve(results);
            };
            promise.then(fulfilled, rejected);
        });
    });
}

// 简单实现了下 allSettled 方法,可以看到效果是一样的
allSettled([request(1), request(2), request(3)]).then((res) => {
    console.log(res);
});

可以看到allSettled做的事情很简单就是把传入进来的Promise循环了一遍.then()等到所有Promise有结果后决定当前返回Promise的状态,并把所有结果传给.then()的成功回调函数。

修改前后的执行过程

修改前执行过程

首先第一次循环去执行request,原本主线程遇到异步代码时之后浏览器执行完异步代码将then方法的回调放入到微任务队列当中,等待主线程执行完后来取回调任务执行,可是使用await关键字,那么就需要等待Promise有结果后才会执行之后的代码下一次循环,而每次等待时间都是3秒,执行完循环的时间就是21秒

修改后执行过程

修改后的代码并没有在循环中去使用await关键字去等待Promise有结果后执行代码在执行下一循环,首先第一次循环去执行request,主线程执行过程中遇到了异步代码主线程就把这个代码交给了浏览器的另一个线程,主线程发现这次循环的代码执行完了,就开始下一次循环执行过程中又遇到了异步任务又交给了浏览器的另一个线程以此类推,循环结束后执行之后的代码,这时候遇到了await allSettled,我们就需要等到allSettled方法返回的Promise有结果后才会执行之后的代码,allSettled需要等待所有的Promise有结果,等待3秒Promise有结果了执行之后的操作。

总结

这次因为习惯了使用async、await去编写异步代码而导致这次问题发生。在解决问题的期间产生许多的疑问,发现对之前学东西的知识不够扎实没有理解到位,又重新学习了一遍关于异步相关的知识并写下了这篇文章记录下过程。

相关推荐
还是大剑师兰特41 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
吖秧吖1 小时前
three.js 杂记
开发语言·前端·javascript
前端小超超1 小时前
vue3 ts项目结合vant4 复选框+气泡弹框实现一个类似Select样式的下拉选择功能
前端·javascript·vue.js
大叔是90后大叔1 小时前
vue3中查找字典列表中某个元素的值
前端·javascript·vue.js
IT大玩客1 小时前
JS如何获取MQTT的主题
开发语言·javascript·ecmascript