【第3403期】一种新颖的替代setTimeout()的方法

科技   2024-10-30 08:03   福建  

前言

主要介绍了新的 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

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。

前端早读课
探索前端技术,体验产品的情感, 项目思考的指引,塑造独立开发者的未来。
 最新文章