告别 setTimeout,前端调度新选择


浏览器是单线程的,这意味着 JavaScript 的执行、页面布局和绘制都在同一个主线程上进行。

任何一个耗时过长的 JS 任务,都会阻塞后续的渲染工作,导致页面卡顿、掉帧,甚至无响应。

setTimeout(fn, 0)
被用来处理一些非紧急但又希望尽快执行的任务,我们期望它能将一个任务推迟到未来的某个时刻执行,从而让主线程得以喘息:

setTimeout(() => {  
  // 在未来的某个时间点执行我  
  console.log("Hello, after 1000ms!");  
}, 1000);  

**setTimeout 的困境:不守时 **

然而, setTimeout 很快暴露了它的致命缺陷:它并不关心浏览器正在做什么。

  • 延迟不准:到点不一定执行,主线程忙就得排队
  • 时机不佳:当浏览器正渲染时插队,导致掉帧卡顿

requestAnimationFrame

为了解决动画卡顿的问题,浏览器厂商们意识到,需要一个与“视觉”同步的调度API。于是, requestAnimationFrame (rAF)
应运而生。

rAF 的设计目标非常明确:在浏览器下一次重绘(repaint)之前执行回调函数。

**rAF 的局限 **

rAF 完美地解决了高优先级视觉任务的调度问题,但如果我们将一个非紧急、耗时较长的后台任务(比如发送日志、数据预处理)放进 rAF
,就又回到了老问题:它仍然会阻塞关键的渲染路径,导致卡顿。

requestIdleCallback

requestIdleCallback (rIC) 的出现,标志着浏览器调度进入了一个“智能”和“协作”的时代。

rIC 的核心理念是:在浏览器主线程的“空闲时期”执行低优先级的回调函数。

requestIdleCallback((deadline) => {  
  // 只有在浏览器有空的时候,我才会被执行  
  console.log('Browser is idle, let\'s do some work!');  
});  

**rIC 的智慧:见缝插针 **

  1. rIC 调度器会在一帧的末尾(当布局、绘制等高优任务完成后)检查是否有剩余时间,空闲时才会执行 rIC 的回调
  2. 协作式调度与 deadline :其回调函数会接收一个 deadline 对象,其中最重要的属性是 timeRemaining() 方法,返回空闲时间
let tasks = [task1, task2, task3, task4, ...];  
  
function processTasks(deadline) {  
 // 只要还有空闲时间,并且还有任务,就继续处理  
 while (deadline.timeRemaining() > 0 && tasks.length > 0) {  
    let task = tasks.shift();  
    execute(task);  
  }  
  
 // 如果任务还没做完,就预约下一次空闲时间继续  
 if (tasks.length > 0) {  
    requestIdleCallback(processTasks);  
  }  
}  
  
requestIdleCallback(processTasks);  

这个模式将一个可能阻塞线程的大任务,分解成多个小任务块。在每个空闲时期,只处理一小部分,并通过 deadline.timeRemaining()
判断是否应该“让出”主线程,将剩余的工作推迟到下一个空闲周期。

代码不再是霸道地占用线程,而是与浏览器友好协商,共同维护用户体验。