前言
主要介绍了新的 scheduler.yield()
方法作为 setTimeout()
的替代方案,用于优化长时间运行的 JavaScript 任务,以提高页面响应性和性能。今日前端早读课文章由 @飘飘翻译分享。
译文从这开始~~
浏览器执行 JavaScript 代码,响应用户触发的事件,并使用相同的执行线程渲染 DOM。当 JavaScript 代码正在运行时,网页会变得无响应,因为浏览器除了等待代码执行完成之外别无他法。
为了说明长时间运行任务的问题及其解决方案,我用表情符号编了一个生动的例子。
每个字符都有其代码,但并非所有代码都与某个字符相关联。所有非字符代码以白色垂直矩形的形式显示。在显示某个范围内代码所对应的字符的页面上,您可以看到许多这样的代码:
有许多垂直的矩形。我想将它们筛选出来。当然,还有更有效的方法,但为了举例说明耗时的任务,我故意使用了一种缓慢且低效的图像比较算法。
识别所有未分配的代码点需要 4279 毫秒,在此期间页面保持冻结状态。尽管在点击后代码先会移除按钮,但只要代码还在运行,浏览器就无法更新屏幕。它也无法显示任何字符,按钮被移除,所有字符一起显示出来,直到代码运行完成:
const MIN = 127734, MAX = 129686;
function insertChar(code, parent) {
parent.insertAdjacentText('beforeend', String.fromCodePoint(code));
}
function add(i, parent) {
if (!likeNull(i))// // compares canvases of character i and 0
insertChar(i, parent);
}
function one(div) {
for (let i = MIN; i < MAX; i++)
add(i, div);
}
function onClick(func) {
btn.addEventListener('click', async () => {
btn.remove();
const start = Date.now();
await func(div);
div.insertAdjacentText("afterend", Date.now() - start);
});
}
onClick(one);
因此,在执行长时间的任务时,重要的是要先暂停,让浏览器更新屏幕。
继续之前,就必须明确 “任务” 这个概念。网页加载完成后,大多数或全部任务都是事件处理程序。在上面代码中,任务是通过点击按钮启动。不是事件处理程序的任务是通过使用 setTimeout()
或 requestAnimationFrame()
等这样的函数调度安排的回调。任务按顺序在一个线程中运行,它们实际上形成了一个队列。
长代码执行可以通过类似语句暂时挂起或中断:
await new Promise(resolve => setTimeout(resolve));
采用这种方法,我将点击监听器分解为许多任务,每个任务对应一个代码点。页面看起来好多了:
代码中的差异在于,点击监听器不再调用 one()
,而是调用与 one()
完全相同的函数 two()
,并且还额外调用 pause()
。
function pause() {
return new Promise(resolve => setTimeout(resolve));
}
async function two(div ) {
for (let i = MIN; i < MAX; i++) {
await pause();
add(i, div);
}
}
onClick(two);
基于 setTimeout 的方案的缺点
有两点明显的不便之处。
即使浏览器无事可做,主任务也会暂停至少 4 毫秒。即使指定为 0, setTimeout()
的最小超时时间也大于 4 毫秒。确实,让我们来计算一下。在第一页中,评估 1952 个代码点需要 4279 毫秒,即每个代码大约 2 毫秒。在第二页中,需要 17568 毫秒,即每个代码大约为 17568/1952=9 毫秒。页面仍然响应灵敏,但性能下降也相当显著,主要原因是超时的最短可能持续时间。
其次,当任务在 某个 Promise 内的 setTimeout()
被挂起时,其后续操作会被作为新任务添加到队尾。因此,浏览器不仅会更新屏幕,还会在继续暂停任务的执行之前执行队列中的所有任务。下面的示例页面演示了这一现象:
在上方的页面中,两个函数 two()
同时运行:
onClick(()=>Promise.race([two(div),two(div2)]));
当第一个 two()
向主线程让步时,第二个 two()
在第一个 two()
继续执行之前被执行。执行时间增加了一点,从 17 秒增加到 23 秒,但并没有增加一倍,因为大部分运行时间是由于增加的最小超时时间造成的。
一种让主线程优先执行的方法
自最近开始,也就是 Chrome 129 版本起,提供了一种更出色的方法,可以将主执行线程借给浏览器使用。
5646 毫秒更接近于完成不间断任务所需的 4279 毫秒,而不是将同一任务分割为 setTimeout()
所需的 17568 毫秒。
async function three(div) {
for (let i = MIN; i < MAX; i++) {
await scheduler.yield();
add(i, div);
}
}
onClick(three);
所以 scheduler.yield()
的性能明显优于 setTimeout()
。
此外,scheduler.yield()
还应该优先处理暂停的任务 —— 它将暂停的任务放在队列的前面而不是末尾。如果能实现这一点,那就太棒了。下一页同时执行两个 three()
函数:
onClick(()=>Promise.race([three(div),three(div2)]));
结果仅在执行时间上与基于 setTimeout()
的 index4.html 不同。10183ms 相当于 5645ms 的两倍。这两个任务似乎都没有被优先处理。
优先级排序似乎不起作用,因为两个功能 three()
都没有在队列中等待。但看看这个:
在上面的页面中, three()
的优先级显而易见,因为它不是与 three()
一起执行的,而是与基于 setTimeout()
的 two()
一起执行的。
onClick(()=>Promise.race([three(div),two(div2)]));
结论
scheduler.yield()
是一个比缓慢且无选择性的 setTimeout()
更优秀的新型替代方案。
示例网页的源代码可以从 https://github.com/marianc000/yield 下载,或者从 https://marianc000.github.io/yield/ 访问页面。
关于本文
译者:@飘飘
作者:@Marian C.
原文:https://marian-caikovski.medium.com/novel-alternative-to-settimeout-26eb240c0bdb
这期前端早读课
对你有帮助,帮” 赞 “一下,
期待下一期,帮” 在看” 一下 。