React18版本的发布标志着并发模式的正式应用,实际上自React16引入Fiber架构后,之后的版本工作之一就是为了后续并发模式的引入做铺垫。在具体说明并发模式之前首先需要明确并发的含义,这里会结合串行、并行概念对比并举例说明,假设此时需要做吃饭和看剧两件事:
React并发模式实际上就是让渲染可以中断从而响应用户操作,之后再恢复执行之前的渲染逻辑,说起来简单但实际上很复杂需要考虑很多事情,例如任务拆分、执行时长把控、整体调度等等。React并发模式建立在Fiber架构基础上使用时间分片机制实现的,即可以简单看成React并发机制 = Fiber架构 + 时间分片机制。本篇文章对应的React版本是18.2.0。
Fiber架构自React16版本引入,主要是为了解决之前React版本递归同步渲染造成的页面卡顿问题。React16新架构可以分为三层:
Fiber就工作在Reconciler中,这种新架构下的工作过程可以简要概述为:组件转变为vdom,vdom再转换为FiberNode节点,依据FiberNode节点创建Fiber树,依据双缓存机制会存在两个Fiber树来实现更新元素的替换,最后提交给渲染器替换页面DOM从而实现页面渲染更新。
Fiber架构使用链表结构来存储更新单元,整体工作由递归变成可中断的遍历,但都是同步执行的,但是Fiber内部会对任务进行更小粒度地拆分,保证每个任务只负责一个节点处理,这样任务内容足够小其单个执行时间就会更短,对这些颗粒度小的任务进行整体调度,就可以很好的保证性能。同时Fiber会针对每个任务提供优先级,优先级标记任务的重要程度,重要的任务会被优先执行。
Fiber架构对任务进行拆分保证更小的更新粒度,为后续提供高效合理的工作单元,之后就需要结合时间分片机制来调度这些任务合理有序的执行。
浏览器端渲染和JavaScript运行都运行在同一个线程上,长时间的JavaScript运行会导致渲染无法工作从而导致页面卡顿无响应,时间分片机制就是限制每次JavaScript运行时长,一旦到达指定时长就会中断执行,将控制权交给渲染等工作,从而避免长时间的JavaScript运行阻塞页面。
时间分片机制需要调度器调度工作,在React18.2中虽然开启了并发模式但默认还是同步渲染,只有使用相关API才会开启时间切片,准确的来说是如下API:
在React源码中的逻辑如下:
var shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && (!didTimeout);
var exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
当开启时间切片时,就会调用renderRootConcurrent函数,不开启时间切片时调用renderRootSync:
workLoopSync和workLoopConcurrent这两个函数的区别就是是否中断循环,具体代码如下:
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
而shouldYield就是判断是否中断的关键逻辑,具体逻辑如下:
function shouldYield() {
var timeElapsed = getCurrentTime() - startTime;
// frameInterval为5ms
if (timeElapsed < frameInterval) {
return false;
}
return true;
}
就是判断是否运行时长是否超过5ms,中断之后如何再次恢复呢?从源码中的两点逻辑去考虑:
结合上面两点可以在源码中找满足条件触发位置,即performWorkUntilDeadline函数,该函数是调度器提供的方法,debug以及调用栈进行溯源,发现整体的调用过程如下:
unstable_scheduleCallback -> requestHostCallback -> schedulePerformWorkUntilDeadline -> performWorkUntilDeadline
需要注意的是一旦调用schedulePerformWorkUntilDeadline,就会触发异步任务即使用宏任务触发performWorkUntilDeadline调用,这里的宏任务API根据浏览器是否支持依次降级使用setImmediate、MessageChannel、setTimeout。
performWorkUntilDeadline函数内部逻辑主要是执行flushWork最终执行workLoop方法,即任务开始被循环处理,实际上就是执行performConcurrentWorkOnRoot,从这里就可以串联出整体处理逻辑了。总结如下:
调度器是如何搭配时间分片机制工作的,这整个逻辑就比较清晰了。unstable_scheduleCallback方法作为调度器Scheduler外部接口被其他地方使用,该函数是触发调度器工作入口之一,其触发调用逻辑比较复杂,不过只要记住只要是需要开启时间切片的API最初都是由该函数触发调度器开始工作的就行。
现在需要专注的是问题是时间切片机制下是如何恢复渲染的?实际上这边逻辑就在调度器中而且还是递归实现的,具体逻辑如下:
var performWorkUntilDeadline = function() {
...
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
}
}
};
当开启时间分片机制后,只要是大任务分割出的子任务没有执行完,hasMoreWork就一直为true,然后开启新一轮宏任务,从而推迟到下一个事件循环中,实现渲染的中断和恢复,这个过程会一直递归到任务结束。至于为什么hasMoreWork一直是true,实际上是taskQueue中开启时间切片的任务没有被推出栈中,一直存在导致,这边的逻辑涉及到的细节还挺多,目前暂时不细究。
实际上从这整个逻辑可以知道时间分片机制合理有序的工作主要是不断触发宏任务直至子任务全部完成,每一次宏任务都会触发shouldYield判断是否超过时长,如果超过时长就会结束Fiber工作单元的工作,进行下一轮的处理。
React18.2版本下并发模式是默认开启,但是同步遍历渲染方式,只有使用并发特性的API例如useDeferredValue,才会开启时间分片机制从而实现并发模式。虽然React16之后就存在并发模式,但是不是默认开启,不断的版本迭代就是平稳的升级。截止到目前的React版本功能存在三种架构:
时间分片机制是并发模型的核心,其原理就是利用MessageChannel等宏任务API推迟Fiber处理逻辑到下一轮事件循环,从而释放控制权用于响应用户交互并实现渲染恢复,但是渲染恢复逻辑还有很多细节值得探究。调度器和时间分片机制的工作流程总结如下: