大家好,我是渡一前端子辰老师。
在前端开发中,我们经常会遇到一些高级量的任务和数据,比如渲染大量的列表、处理复杂的业务逻辑、执行耗时的计算等。
这些任务如果不加以优化,很容易导致页面卡顿或响应速度变慢,严重影响用户体验。
那么,我们该如何优化这些高级量任务的执行呢?有没有一些通用的方法和技巧呢?
本文将通过一个实际的面试题来展示和分析不同的任务调度和页面渲染机制,以及如何利用微任务、宏任务、requestAnimationFrame 和 requestIdleCallback 等技术来实现高级量任务执行的优化。
希望你能从中受益,并在你的项目中运用起来。
温馨提示:请同学们自备温水,因为这篇文章太干了!
面试题
这是一个非常有诚意的面试题,为了让你做题,还专门写了一个页面。
我们再来看一下面试题要求。
js
/**
* 运行一个耗时任务
* 如果要异步执行任务,请返回Promise
* 要尽快完成任务,同时不要让页面产生卡顿
* 尽量兼容更多的浏览器
* @param {Function} task
*/
function runTask(task) {}
他的要求是让我们实现一个函数,当点击按钮的时候会执行 500 个耗时任务,其实就是调用 500 次这个 task()
函数。
那么我们该如何实现这个函数呢?让我们一起来探索一下。
解题
同步执行
最简单直接的方法就是同步执行这些任务,也就是直接调用 task() 函数。
js
function runTask(task) {
task();
}
当然如果你仅仅这么做的话,结果就是下图这样的。
你会看到,点击按钮开始执行任务的时候,动画出现了阻塞,执行完毕后显示耗时 2500 毫秒,所以直接执行肯定是不行的。
阻塞的原因也很简单,因为 task 是同步任务,由于它比较耗时,执行 500 次,所以就阻塞了页面的渲染。
既然直接执行不行,我们试试异步的方式。
题目要求说:如果要异步执行任务,请返回 Promise。
微任务
我们按照他的要求写了,并将任务放在微队列里执行。
js
function runTask(task) {
return new Promise((resolve) => {
Promise.resolve().then(() => {
task();
resolve();
});
});
}
再看下效果如何。
你会发现微任务还是阻塞,微任务阻塞其实也很好理解,是因为在事件循环里边,微队列一定要全部清空才能做后边的事情,当然也包括渲染。
我们知道浏览器大概每 16.6 毫秒渲染一次页面(具体时间取决于屏幕刷新率),由于中间有 500 个微任务,那么渲染帧就要就被推迟了,只能等到微任务全部完成之后才可以重新渲染,所以说也会看到阻塞。
既然微任务也不行,我们换成 setTimeout。
setTimeout 按照过去的说法会产生宏任务,这个任务会进入宏队列;按照最新的说法是产生延时任务,这个任务会进入延时队列。
不管怎么说,setTimeout 的效果都是类似的。
宏任务
js
function runTask(task) {
return new Promise((resolve) => {
setTimeout(() => {
task();
resolve();
}, 0);
});
}
我们再来看一下 setTimeout 的效果如何。
你会发现没有发生阻塞,但是变得卡顿了。
那么宏任务为什么会造成卡顿而不是阻塞呢?我们来看一下宏任务是如何执行的。
同学们应该都知道,事件循环实际上是一个死循环,我们单说宏任务在这个循环里做了什么事情。
js
for (; ;) {
1 - 取出宏队列中第一个(最早加入)的宏任务并执行
2 - 执行任务
3 - 判断是否到达渲染时机,可以简单的理解为到了 16.6 毫秒就进行渲染
}
宏任务不阻塞的原因就是因为宏任务每次只取一个任务执行,执行完了之后如果说渲染时机到了之后就渲染,然后下一次循环再取下一个任务,所以它就不会阻塞。
至于卡顿的原因就比较有意思了,这是因为不同的浏览器来判断这个渲染时机的方式不同,W3C 并没有非常明确的规定这个渲染时机,所以很多时候需要浏览器自己去判断。
那么每个浏览器的判断方式是有差异的,比如谷歌浏览器就认为,队列里有非常多的任务了,有很高的计算需求了,它觉得要匀一部分计算资源给这些正在等待的宏任务,所以会稍微的把这个渲染时机向后延一延,Edge 浏览器和谷歌大同小异,这就是界面卡顿的原因。
我们回归正题,现在 setTimeout 会造成卡顿,也不能用,那我们试试 requestAnimationFrame 方法。
requestAnimationFrame
requestAnimationFrame 是一个专门用于动画渲染的方法,它会在浏览器下一次重绘之前执行回调函数。我们可以利用这个方法来执行任务。
js
function runTask(task) {
return new Promise((resolve) => {
requestAnimationFrame(() => {
task();
resolve();
});
});
}
我们再来看一下 requestAnimationFrame 的效果如何。
很明显也阻塞了,因为在渲染帧之前,执行了 500 次耗时任务,每一次都把这个渲染帧向后延了,所以现在 requestAnimationFrame 也不可以用。
想来想去我们就只能手动的控制这个任务的执行时机了,不能简单粗暴的放在这个函数里执行了,我们得找到一种办法,在这个任务执行之前判断一下现在运行是否合适,不合适的话在等一会,那么我们就得写一个辅助函数了。
requestIdleCallback
那么思路有了,我们根据什么判断是否合适运行呢?我们现在要不阻塞且不造成卡顿,是不是就要看渲染帧是否有剩余时间,判断 16.6 毫秒一帧还剩下多少时间可以供我们操作,所以如果有时间就执行,如果没有时间了就等一会。
这个时候你就应该想到一个方法,叫做 requestIdleCallback,requestIdleCallback 方法插入一个函数,这个函数将在浏览器空闲时期被调用,刚好符合我们的设想,我们去试一试。
js
function _runTask(task, callback) {
// 使用 requestIdleCallback 方法,返回一个参数 idle 对象
// idle 中有一个方法 timeRemaining,可以获取到剩余时间
requestIdleCallback((idle) => {
// 如果有空闲时间就调用反之则递归继续等待
if (idle.timeRemaining() > 0) {
task();
callback();
} else {
_runTask(task, callback);
}
});
}
function runTask(task) {
return new Promise((resolve) => {
_runTask(task, resolve);
});
}
效果呢就是如此丝滑,当然任务有时候会等待,所以任务完成时间也会相对的延长。
到这呢已经非常的不错了,但是呢面试题里的最后一条要求我们尽量兼容更多的浏览器,而 requestIdleCallback 兼容性确不是非常好,我们去 caniuse 看看兼容性。
可以看到,requestIdleCallback 的兼容性并非非常的好。
我们在看看 requestAnimationFrame 的兼容性。
那我们就把 requestIdleCallback 换成 requestAnimationFrame。
js
function _runTask(task, callback) {
// 通过 Date 的时差,记录一个时间
let start = Date.now();
requestAnimationFrame(() => {
// 计算一下剩余时间
if (Date.now() - start < 16.6) {
task();
callback();
} else {
_runTask(task, callback);
}
});
}
function runTask(task) {
return new Promise((resolve) => {
_runTask(task, resolve);
});
}
可以看到非常的流畅了,只不过消耗的时间同样也会延长。
总结
今天主要就是解决如何优化高级量任务执行的方法和技巧,通过一个实际的面试题来展示了不同的任务调度和页面渲染机制的效果和原理。
其中包括同步任务、微任务、宏任务、requestAnimationFrame 和 requestIdleCallback 等技术。
子辰还分析了每种方法的优缺点,希望你可以从这篇文章中学到前端开发中的任务调度和页面渲染机制的基本概念和原理,以及如何利用现有的技术来优化高级量任务执行的方法和技巧。
本文来源
本文来源自渡一官方公众号:Duing ,欢迎关注,获取最新 、最全 、最深入的技术讲解
感谢你阅读本文,如果你有任何疑问或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友