本文内容涉及到不少渲染链路中的原理以及源码方法,因此在看本文以前,须要对于React
的render
渲染流程有大体的了解。不清楚的同窗能够先看个人第一篇源码解析文章。react
写给本身看的React源码解析(一):你的React代码是怎么渲染成DOM的? git
本文主要解析fiber
架构更新链路的双缓冲
模式以及Concurrent
模式下时间切片
,优先级
的实现原理。github
双缓冲模式的主要用处,是可以帮咱们较大限度地实现Fiber
节点的复用,减小性能方面的开销。浏览器
在以前的文章中,咱们知道在首次渲染的时候会建立出两颗树,current
树与workInProgress
树。current
树与workInProgress
树,其实就是两套缓冲数据:当current
树被渲染到页面上时,全部的数据更新都会由workInProgress
树来承接。workInProgress
树将会在内存里悄悄地完成全部改变,直到下次进行渲染的commit
阶段执行完毕以后,fiberRoot
对象的current
会指向workInProgress
树,workInProgress
树就会变成渲染到页面上的current
树。性能优化
咱们用一个实际例子来帮助理解:markdown
import { useState } from 'react';
function App() {
const [state, setState] = useState(0)
return (
<div className="App"> <div onClick={() => { setState(state + 1) }}> <p>{state}</p> </div> </div>
);
}
复制代码
这个例子的功能很简单,就是点击一次,数字加1。上面的demo在render
阶段结束后,commit
阶段结束前的两颗fiber
树以下图所示架构
commit
阶段完成,workInProgress
树被渲染到页面上,这时候fiberRoot
对象的current
会指向workInProgress
树,这个当前被渲染的fiber
树。app
点击一次数字,咱们进入第一次的更新流程。重点看beginWork
调用链路中的createWorkInProgress
方法。异步
上图中,workInProgress
树下面的子节点的current.alternate
对应的就是current
树的子节点,可是current
树目前没有子节点,因此为null,进入等于null的流程。按照workInProgress
的子节点的属性给current
树建立出相同的子节点。ide
而后在commit
阶段结束后,current
树会被渲染到页面上,fiberRoot
对象的current
会指回到current
树,具体以下图
再点击一次数字,触发state的第二次更新,仍是看以前的createWorkInProgress
方法。
这时候,由于两颗树都已经构建完成,因此current.alternate
是存在的。因此以后每次经过beginWork
触发createWorkInProgress
调用时,都会一致地走入else
里面的逻辑,也就是直接复用现成的节点。 这也就是双缓冲机制实现节点复用的方法。
React
源码解析第一篇分析了首次渲染的链路,更新的链路其实跟首次渲染大体同样。
首次渲染能够理解为一种特殊的更新,ReactDOM.render
,setState
,useState
同样,都是一种触发更新的姿式。这些方法发起的调用链路很类似,是由于它们最后“异曲同工”,都会经过建立update
对象来进入同一套更新工做流。
按demo的流程来,点击数字以后,会触发一个dispatchAction
方法,在该方法中,会完成update
对象的建立
update
建立完成以后,会跟首次渲染同样,进入updateContainer
方法(首次渲染链路中的update
会在这个方法里建立),这里主要是两个方法
enqueueUpdate(current, update);
scheduleUpdateOnFiber(current, lane, eventTime);
复制代码
enqueueUpdate
:将update
入队。每个Fiber
节点都会有一个属于它本身的updateQueue
,用于存储多个更新,这个updateQueue
是以链表的形式存在的。在render
阶段,updateQueue
的内容会成为 render
阶段计算Fiber
节点的新state
的依据。
scheduleUpdateOnFiber
:调度update
。这个方法后面紧跟的就是performSyncWorkOnRoot
所触发的render
阶段。
这里有一个点须要提示一下:dispatchAction
中,调度的是当前触发更新的节点,这一点和挂载过程须要区分开来。在挂载过程当中,updateContainer
会直接调度根节点。其实,对于更新这种场景来讲,大部分的更新动做确实都不是由根节点触发的,而render
阶段的起点则是根节点。因此在scheduleUpdateOnFiber
中,有这样一个方法
它会从当前Fiber
节点开始,向上遍历直至根节点,并将根节点返回。因此,咱们说React
的更新流程,是从根节点开始,从新遍历整个fiber
树,这也是为何咱们平时的性能优化的重点都在减小组件的从新render
上。
在scheduleUpdateOnFiber
中,还有一个重要的判断,那就是对于同步和异步的判断逻辑。
以前咱们分析同步的首次渲染流程的时候,走的是performSyncWorkOnRoot
方法,可是对于异步模式,会运行ensureRootIsScheduled
方法。来看下一段核心逻辑
if (newCallbackPriority === SyncLanePriority) {
// 同步更新的 render 入口
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 将当前任务的 lane 优先级转换为 scheduler 可理解的优先级
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
// 异步更新的 render 入口
newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
复制代码
从这段逻辑中咱们能够看出,React
会以当前更新任务的优先级类型为依据,决定接下来是调度 performSyncWorkOnRoot
仍是performConcurrentWorkOnRoot
。这里调度任务用到的函数分别是 scheduleSyncCallback
和scheduleCallback
,这两个函数在内部都是经过调用 unstable_scheduleCallback
方法来执行任务调度的。这个方法是Scheduler
(调度器)中导出的一个核心方法。
Scheduler
的核心能力,就是让fiber
架构实现了时间切片
与优先级调度
这两个核心特征。
先来了解一下时间切片究竟是作了什么事情?
import React from 'react';
function App() {
const arr = new Array(1000).fill(0);
return (
<div className="App"> <div className="container"> { arr.map((i, index) => <p>{`测试文本第${index}行`}</p>) } </div> </div>
);
}
复制代码
上面的代码就是渲染1000条p
标签到页面上,当咱们使用ReactDOM.render
进行渲染,由于它是一个同步的过程,全部的链路都会在一个宏任务里执行掉。根据不一样用户电脑和浏览器的性能不一样,这个宏任务的执行时间,多是100ms、200ms、300ms甚至更多。由于js
线程和渲染线程是互斥的,在执行这个比较长时间的宏任务时,咱们浏览器的渲染线程将被阻塞。咱们知道浏览器的刷新频率为60Hz
也就是说每16.6ms
就会刷新一次,这种长时间的宏任务致使的渲染线程阻塞,将会产生明显的卡顿、掉帧。
而时间切片,就是把这段须要较长时间运行的宏任务“切”开,变成一段段尽可能保证运行时间在浏览器刷新间隔时间之下的宏任务。给渲染线程留出时间,保证渲染的流畅度。咱们来看两张图,第一张是同步模式下的调用栈
下一张是把ReactDOM.render
调用改成createRoot
,用Concurrent
(异步)模式来进行渲染
咱们能够看到,原本一个长时间的“大任务”被切成了一个个短期的“小任务”。
根据上文对scheduleUpdateOnFiber
的分析,在同步的模式下,React
会调用performSyncWorkOnRoot
,在这个链路下,会经过workLoopSync
方法来循环建立Fiber
节点、构建Fiber
树。
function workLoopSync() {
// 若 workInProgress 不为空
while (workInProgress !== null) {
// 针对它执行 performUnitOfWork 方法
performUnitOfWork(workInProgress);
}
}
复制代码
这是一个没法中断的过程,开始了就没法中止。
而在异步的模式下,React
会调用performConcurrentWorkOnRoot
,经过renderRootConcurrent
调用 workLoopConcurrent
来构建Fiber
树。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
复制代码
咱们能够发现,异步的方法里,其实就只是多了一个shouldYield()
方法,当shouldYield()
为true
的时候,while循环将中止,将主线程让给渲染线程。
shouldYield
的本体其实也是调度器里导出的一个方法Scheduler.unstable_shouldYield
,方法很简单。源码地址
export function unstable_shouldYield() {
return getCurrentTime() >= deadline;
}
复制代码
就是当当前时间大于deadline
这个当前时间切片的到期时间时,就返回true
,中止workLoopConcurrent
循环。
咱们来看下deadline
是怎么定义的
deadline = getCurrentTime() + yieldInterval;
复制代码
getCurrentTime()
就是当前时间,而yieldInterval
是一个常量,5ms,源码地址
const yieldInterval = 5;
复制代码
因此说,时间切片的间隔是5ms
(实际应该都是比5ms
稍大,由于必须等当前的fiber
节点构建完成以后,才会经过shouldYield()
方法判断是否到期)
当workLoopConcurrent
循环中断以后,React
会从新发起调度(setTimeout
或者MessageChannel
方式),检查是否存在事件响应、更高优先级任务或其余代码须要执行,若是有则执行,若是没有则从新建立工做循环workLoopConcurrent
,执行剩下的工做中Fiber
节点构建。
在更新链路中,不管是scheduleSyncCallback
仍是scheduleCallback
,最终都是经过调用 unstable_scheduleCallback
来发起调度的。 unstable_scheduleCallback
是Scheduler
导出的一个核心方法,它将结合任务的优先级信息为其执行不一样的调度逻辑。源码地址
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 获取当前时间
var currentTime = getCurrentTime();
// 声明 startTime,startTime 是任务的预期开始时间
var startTime;
// 如下是对 options 入参的处理
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
// 若入参规定了延迟时间,则累加延迟时间
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
// timeout 是 expirationTime 的计算依据
var timeout;
// 根据 priorityLevel,肯定 timeout 的值
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
// 优先级越高,timout 越小,expirationTime 越小
var expirationTime = startTime + timeout;
// 建立 task 对象
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
// 若当前时间小于开始时间,说明该任务可延时执行(未过时)
if (startTime > currentTime) {
// 将未过时任务推入 "timerQueue"
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 若 taskQueue 中没有可执行的任务,而当前任务又是 timerQueue 中的第一个任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 那么就派发一个延时任务,这个延时任务用于检查当前任务是否过时
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// else 里处理的是当前时间大于 startTime 的状况,说明这个任务已过时
newTask.sortIndex = expirationTime;
// 过时的任务会被推入 taskQueue
push(taskQueue, newTask);
......
// 执行 taskQueue 中的任务
requestHostCallback(flushWork);
}
return newTask;
}
复制代码
unstable_scheduleCallback
的主要工做是针对当前任务建立一个task
,而后结合startTime
信息将这个task
推入timerQueue
或taskQueue
,最后根据timerQueue
和taskQueue
的状况,执行延时任务或即时任务。
这里须要知道几个概念
堆是一种特殊的彻底二叉树。若是对一棵彻底二叉树来讲,它每一个结点的结点值都不大于其左右孩子的结点值,这样的彻底二叉树就叫“小顶堆”。小顶堆自身特有的插入和删除逻辑,决定了不管咱们怎么增删小顶堆的元素,其根节点必定是全部元素中值最小的一个节点。
咱们来看下核心逻辑
if (startTime > currentTime) {
// 将未过时任务推入 "timerQueue"
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 若 taskQueue 中没有可执行的任务,而当前任务又是 timerQueue 中的第一个任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
......
// 那么就派发一个延时任务,这个延时任务用于检查当前任务是否过时
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// else 里处理的是当前时间大于 startTime 的状况,说明这个任务已过时
newTask.sortIndex = expirationTime;
// 过时的任务会被推入 taskQueue
push(taskQueue, newTask);
......
// 执行 taskQueue 中的任务
requestHostCallback(flushWork);
}
复制代码
若判断当前任务是未过时任务,那么该任务会在sortIndex
属性被赋值为startTime
后,被推入timerQueue
。taskQueue
里存储的是已过时的任务,peek(taskQueue)
取出的任务若为空,则说明taskQueue
为空、当前并无已过时任务。在没有已过时任务的状况下,若当前任务(newTask
)就是timerQueue
中须要最先被执行的未过时任务,那么unstable_scheduleCallback
会经过调用requestHostTimeout
,为当前任务发起一个延时调用。
注意,这个延时调用(也就是handleTimeout
)并不会直接调度执行当前任务——它的做用是在当前任务到期后,将其从 timerQueue
中取出,加入taskQueue
中,而后触发对flushWork
的调用。真正的调度执行过程是在flushWork
中进行的。flushWork
中将调用workLoop
,workLoop
会逐一执行taskQueue
中的任务,直到调度过程被暂停(时间片用尽,将从新发起Task
调度)或任务所有被清空。
当下React
发起Task
调度的姿式有两个:setTimeout
、MessageChannel
。在宿主环境不支持MessageChannel
的状况下,会降级到setTimeout
。但不论是setTimeout
仍是MessageChannel
,它们发起的都是异步任务(宏任务,将在下次eventLoop
中被调用)。
若是本文对你有所帮助,请帮忙点个赞,感谢!