此文只为调度的概念篇,暂不涉及源码分析。
浏览器的时间片
首先,你需要了解浏览器的重绘机制。一般的浏览器为经典的60hz,既一秒钟进行60次刷新。一帧的时间大概为:1000/60 约等于16.6ms。在每个16.6ms浏览器会对页面进行重绘。

众所周知,JavaScript是单线程的,如果有一个很大的任务,这个任务所需要的时间远远大于16.6ms,在这个任务霸占主线程的时候,浏览器是无法响应任何其他任务。这个时候就会造成了页面卡死问题。
render() { let BLOCKDIVNUMBER = 100000; let temp = []; while (BLOCKDIVNUMBER) { temp.push( <div style={{ display: "inline-block", borderRadius: "1px solid black", height: 200, width: 200, backgroundColor: "blue", color: "#ffffff" }} > {this.state.number} </div> ); BLOCKDIVNUMBER--; } return ( <div className="app"> <button onClick={this.click.bind(this)}>add</button> {temp} </div> ); }
如上代码,重绘这10万条数据需要5s。如果没有使用任务调度在这5s内浏览器的主线程完全被霸占,无法响应其他事件。
Fiber架构
在Fiber Reconciler前,React使用的是Stack Reconciler,Stack Reconciler始终会一次性地同步处理整个Virtual DOM ,无法暂停,这也就是上文代码一次渲染10万条数据会导致页面卡死的原因。
与之相比,Fiber Reconciler 重新实现了 React 的核心算法,整个 Virtual DOM 的更新任务拆分成一个个小的任务。每个小任务占用的很小,每次做完一个小任务之后,放弃一下自己的执行将主线程空闲出来,看看有没有其他的任务。如果有的话,就暂停本次任务,执行其他的任务,如果没有的话,就继续下一个任务。
任务调度
在 React 16 改写的新 Fiber 架构帮助下,React 现在可以允许渲染过程分段完成,中间可以返回至主线程执行其他任务。现在重要的是理解,当启用这个模式之后,React 会把同步渲染的 React 组件切分成小块,然后在多个帧上运行。
如上文提到的时间片概念,在60hz的刷新频率下,每16.6ms为一帧。而拆分的小任务正是在每帧的渲染完成后的idle时间段中执行。

react调度目前的任务优先级有:
- Immediate 立即执行优先级,需要同步执行的任务。
- UserBlocking 用户阻塞型优先级(250 ms 后过期),需要作为用户交互结果运行的任务(例如,按钮点击)。
- Normal 普通优先级(5 s 后过期),不必让用户立即感受到的更新。
- Low 低优先级(10 s 后过期),可以推迟但最终仍然需要完成的任务(例如,分析通知)。
- Idle 空闲优先级(永不过期),不必运行的任务(例如,隐藏界面以外的内容)。
五种优先级任务对应的时间常数:
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;在引擎中,调度器将所有已经注册的回调函数按照过期时间(回调函数注册的时间加上该优先级的过期时间)排序然后存储在列表中。接着,调度器将自己注册在浏览器绘制下一帧之后的回调函数里。在这个回调函数中,浏览器将执行尽可能多的已注册回调函数,直到浏览开始绘制下一帧为止。
// 过期时间计算
expirationTime = window.performance.now() + taskExpirationTime小结
调度的核心概念可以概括为:
- 将庞大的DOM处理任务进行拆分
- 对拆分的任务进行优先级定义,计算过期时间。
- 使用requestAnimationFrame在每一帧的渲染任务完成后的时间片里执行优先级最高的任务。