There are a lot of ways to break up long tasks in JavaScript.
在前端开发中,处理长时间运行的任务是一个常见的挑战。长时间占用主线程会导致用户体验下降,例如页面无法及时响应用户的点击操作或无法正常更新显示内容。为了解决这一问题,开发者可以将长时间任务拆分为多个小任务,并在事件循环的多个时间点上执行,从而让浏览器有机会处理其他重要任务,如用户输入和页面重绘。
文章中介绍了多种在 JavaScript 中拆分长时间任务的方法,每种方法都有其特点和适用场景:
setTimeout()
+ 递归:这是一种经典的解决方案,通过在 setTimeout()
的回调中递归调用自身,将任务拆分到不同的事件循环时间点上。这种方法简单易懂,但随着 ES6 的出现,有更好的替代方案。async/await
+ 延时:利用 async/await
和 Promise
,可以在一个简单的 for
循环中实现任务拆分。通过 await
一个立即解析的 Promise
,可以让代码在每次迭代后暂停执行,从而释放事件循环。这种方法比递归更简洁,且易于理解。scheduler.postTask()
:这是 Chromium 浏览器中相对较新的 API,用于更高效地调度任务。它允许开发者控制任务的优先级,例如设置为“用户可见”或“背景”优先级。与 setTimeout()
不同,postTask()
调度的任务会被放置在任务队列的前面,避免被其他任务延迟执行。不过,该 API 的浏览器支持还不够广泛,但可以通过 polyfill 解决。scheduler.yield()
:该方法是 Scheduler
接口的一部分,专门用于在任务中释放主线程,并稍后继续执行。它允许开发者在长时间任务中暂停执行,让浏览器有机会处理其他任务,从而保持页面响应性。与 postTask()
一样,yield()
也需要 polyfill 来支持非 Chromium 浏览器。requestAnimationFrame()
:该 API 通常用于与浏览器的重绘周期同步执行任务。虽然它可以精确地调度回调,但在执行长时间任务时可能会对渲染性能产生负面影响,例如导致部分帧无法完整呈现。因此,它更适合用于与动画相关的任务,而不是拆分长时间任务。MessageChannel()
:通过创建一个消息通道并在通道中传递消息,可以在不依赖定时器的情况下实现任务拆分。这种方法的性能表现较好,但实现相对复杂,且并非其设计初衷。在选择适合的方法时,需要根据具体需求和浏览器支持情况来决定。如果任务可以在主线程之外执行,Web Workers 是首选;如果需要简单易用的解决方案,可以考虑 scheduler.yield()
或 async/await
+ 延时;如果需要更精细的任务调度控制,则可以使用 scheduler.postTask()
。