前言
这天早上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方法返回值是一个Promise
,await
关键字就会去等待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
去编写异步代码而导致这次问题发生。在解决问题的期间产生许多的疑问,发现对之前学东西的知识不够扎实没有理解到位,又重新学习了一遍关于异步相关的知识并写下了这篇文章记录下过程。