共计 7826 个字符,预计需要花费 20 分钟才能阅读完成。
这篇文章是 React 架构演变的第二篇,上一篇次要介绍了更新机制从同步批改为异步,这一篇重点介绍 Fiber 架构下通过循环遍历更新的过程,之所以要应用循环遍历的形式,是因为递归更新过程一旦开始就不能暂停,只能一直向下,直到递归完结或者出现异常。
递归更新的实现
React 15 的递归更新逻辑是先将须要更新的组件放入脏组件队列(这里在上篇文章曾经介绍过,没看过的能够先看看《React 架构的演变 – 从同步到异步》),而后取出组件进行一次递归,不停向下寻找子节点来查找是否须要更新。
上面应用一段代码来简略形容一下这个过程:
updateComponent (prevElement, nextElement) {
if (
// 如果组件的 type 和 key 都没有发生变化,进行更新
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
) {
// 文本节点更新
if (prevElement.type === 'text') {if (prevElement.value !== nextElement.value) {this.replaceText(nextElement.value)
}
}
// DOM 节点的更新
else {
// 先更新 DOM 属性
this.updateProps(prevElement, nextElement)
// 再更新 children
this.updateChildren(prevElement, nextElement)
}
}
// 如果组件的 type 和 key 发生变化,间接从新渲染组件
else {
// 触发 unmount 生命周期
ReactReconciler.unmountComponent(prevElement)
// 渲染新的组件
this._instantiateReactComponent(nextElement)
}
},
updateChildren (prevElement, nextElement) {
var prevChildren = prevElement.children
var nextChildren = nextElement.children
// 省略通过 key 从新排序的 diff 过程
if (prevChildren === null) { } // 渲染新的子节点
if (nextChildren === null) { } // 清空所有子节点
// 子节点比照
prevChildren.forEach((prevChild, index) => {const nextChild = nextChildren[index]
// 递归过程
this.updateComponent(prevChild, nextChild)
})
}
为了更清晰的看到这个过程,咱们还是写一个简略的 Demo,结构一个 3 * 3 的 Table 组件。
// https://codesandbox.io/embed/react-sync-demo-nlijf
class Col extends React.Component {render() {
// 渲染之前暂停 8ms,给 render 制作一点点压力
const start = performance.now()
while (performance.now() - start < 8)
return <td>{this.props.children}</td>
}
}
export default class Demo extends React.Component {
state = {val: 0}
render() {const { val} = this.state
const array = Array(3).fill()
// 结构一个 3 * 3 表格
const rows = array.map((_, row) => <tr key={row}>
{array.map((_, col) => <Col key={col}>{val}</Col>
)}
</tr>
)
return (
<table className="table">
<tbody>{rows}</tbody>
</table>
)
}
}
而后每秒对 Table 外面的值更新一次,让 val 每次 + 1,从 0 ~ 9 不停循环。
// https://codesandbox.io/embed/react-sync-demo-nlijf
export default class Demo extends React.Component {tick = () => {setTimeout(() => {this.setState({ val: next < 10 ? next : 0})
this.tick()}, 1000)
}
componentDidMount() {this.tick()
}
}
残缺代码的线上地址:https://codesandbox.io/embed/react-sync-demo-nlijf。Demo 组件每次调用 setState,React 会先判断该组件的类型有没有产生批改,如果有就整个组件进行从新渲染,如果没有会更新 state,而后向下判断 table 组件,table 组件持续向下判断 tr 组件,tr 组件再向下判断 td 组件,最初发现 td 组件下的文本节点产生了批改,通过 DOM API 更新。
通过 Performance 的函数调用堆栈也能清晰的看到这个过程,updateComponent 之后 的 updateChildren 会持续调用子组件的 updateComponent,直到递归完所有组件,示意更新实现。
递归的毛病很显著,不能暂停更新,一旦开始必须从头到尾,这与 React 16 拆分工夫片,给浏览器喘口气的理念显著不符,所以 React 必须要切换架构,将虚构 DOM 从树形构造批改为链表构造。
可循环的 Fiber
这里说的链表构造就是 Fiber 了,链表构造最大的劣势就是能够通过循环的形式来遍历,只有记住以后遍历的地位,即便中断后也能疾速还原,从新开始遍历。
咱们先看看一个 Fiber 节点的数据结构:
function FiberNode (tag, key) {
// 节点 key,次要用于了优化列表 diff
this.key = key
// 节点类型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ...
this.tag = tag
// 子节点
this.child = null
// 父节点
this.return = null
// 兄弟节点
this.sibling = null
// 更新队列,用于暂存 setState 的值
this.updateQueue = null
// 节点更新过期工夫,用于工夫分片
// react 17 改为:lanes、childLanes
this.expirationTime = NoLanes
this.childExpirationTime = NoLanes
// 对应到页面的实在 DOM 节点
this.stateNode = null
// Fiber 节点的正本,能够了解为备胎,次要用于晋升更新的性能
this.alternate = null
}
上面举个例子,咱们这里有一段一般的 HTML 文本:
<table class="table">
<tr>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
</tr>
</table>
在之前的 React 版本中,jsx 会转化为 createElement 办法,创立树形构造的虚构 DOM。
const VDOMRoot = {
type: 'table',
props: {className: 'table'},
children: [
{
type: 'tr',
props: { },
children: [
{
type: 'td',
props: { },
children: [{type: 'text', value: '1'}]
},
{
type: 'td',
props: { },
children: [{type: 'text', value: '1'}]
}
]
},
{
type: 'tr',
props: { },
children: [
{
type: 'td',
props: { },
children: [{type: 'text', value: '1'}]
}
]
}
]
}
Fiber 架构下,构造如下:
// 有所简化,并非与 React 实在的 Fiber 构造统一
const FiberRoot = {
type: 'table',
return: null,
sibling: null,
child: {
type: 'tr',
return: FiberNode, // table 的 FiberNode
sibling: {
type: 'tr',
return: FiberNode, // table 的 FiberNode
sibling: null,
child: {
type: 'td',
return: FiberNode, // tr 的 FiberNode
sibling: {
type: 'td',
return: FiberNode, // tr 的 FiberNode
sibling: null,
child: null,
text: '1' // 子节点仅有文本节点
},
child: null,
text: '1' // 子节点仅有文本节点
}
},
child: {
type: 'td',
return: FiberNode, // tr 的 FiberNode
sibling: null,
child: null,
text: '1' // 子节点仅有文本节点
}
}
}
循环更新的实现
那么,在 setState 的时候,React 是如何进行一次 Fiber 的遍历的呢?
let workInProgress = FiberRoot
// 遍历 Fiber 节点,如果工夫片工夫用完就进行遍历
function workLoopConcurrent() {
while (
workInProgress !== null &&
!shouldYield() // 用于判断以后工夫片是否到期) {performUnitOfWork(workInProgress)
}
}
function performUnitOfWork() {const next = beginWork(workInProgress) // 返回以后 Fiber 的 child
if (next) { // child 存在
// 重置 workInProgress 为 child
workInProgress = next
} else { // child 不存在
// 向上回溯节点
let completedWork = workInProgress
while (completedWork !== null) {
// 收集副作用,次要是用于标记节点是否须要操作 DOM
completeWork(completedWork)
// 获取 Fiber.sibling
let siblingFiber = workInProgress.sibling
if (siblingFiber) {
// sibling 存在,则跳出 complete 流程,持续 beginWork
workInProgress = siblingFiber
return;
}
completedWork = completedWork.return
workInProgress = completedWork
}
}
}
function beginWork(workInProgress) {
// 调用 render 办法,创立子 Fiber,进行 diff
// 操作结束后,返回以后 Fiber 的 child
return workInProgress.child
}
function completeWork(workInProgress) {// 收集节点副作用}
Fiber 的遍历实质上就是一个循环,全局有一个 workInProgress
变量,用来存储以后正在 diff 的节点,先通过 beginWork
办法对以后节点而后进行 diff 操作(diff 之前会调用 render,从新计算 state、prop),并返回以后节点的第一个子节点 (fiber.child
) 作为新的工作节点,直到不存在子节点。而后,对以后节点调用 completedWork
办法,存储 beginWork
过程中产生的副作用,如果以后节点存在兄弟节点(fiber.sibling
),则将工作节点批改为兄弟节点,从新进入 beginWork
流程。直到 completedWork
从新返回到根节点,执行 commitRoot
将所有的副作用反馈到实在 DOM 中。
在一次遍历过程中,每个节点都会经验 beginWork
、completeWork
,直到返回到根节点,最初通过 commitRoot
将所有的更新提交,对于这部分的内容能够看:《React 技术揭秘》。
工夫分片的机密
后面说过,Fiber 构造的遍历是反对中断复原,为了察看这个过程,咱们将之前的 3 * 3 的 Table 组件改成 Concurrent 模式,线上地址:https://codesandbox.io/embed/react-async-demo-h1lbz。因为每次调用 Col 组件的 render 局部须要耗时 8ms,会超出了一个工夫片,所以每个 td 局部都会暂停一次。
class Col extends React.Component {render() {
// 渲染之前暂停 8ms,给 render 制作一点点压力
const start = performance.now();
while (performance.now() - start < 8);
return <td>{this.props.children}</td>
}
}
在这个 3 * 3 组件里,一共有 9 个 Col 组件,所以会有 9 次耗时工作,扩散在 9 个工夫片进行,通过 Performance 的调用栈能够看到具体情况:
在非 Concurrent 模式下,Fiber 节点的遍历是一次性进行的,并不会切分多个工夫片,差异就是在遍历的时候调用了 workLoopSync
办法,该办法并不会判断工夫片是否用完。
// 遍历 Fiber 节点
function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress)
}
}
通过下面的剖析能够看出,shouldYield
办法决定了以后工夫片是否曾经用完,这也是决定 React 是同步渲染还是异步渲染的要害。如果去除工作优先级的概念,shouldYield
办法能够说很简略,就是判断了以后的工夫,是否曾经超过了预设的 deadline
。
function getCurrentTime() {return performance.now()
}
function shouldYield() {
// 获取以后工夫
var currentTime = getCurrentTime()
return currentTime >= deadline
}
deadline
又是如何得的呢?能够回顾上一篇文章(《React 架构的演变 – 从同步到异步》)提到的 ChannelMessage,更新开始的时候会通过 requestHostCallback
(即:port2.send
)发送异步音讯,在 performWorkUntilDeadline
(即:port1.onmessage
)中接管音讯。performWorkUntilDeadline
每次接管到音讯时,示意曾经进入了下一个工作队列,这个时候就会更新 deadline
。
var channel = new MessageChannel()
var port = channel.port2
channel.port1.onmessage = function performWorkUntilDeadline() {if (scheduledHostCallback !== null) {var currentTime = getCurrentTime()
// 重置超时工夫
deadline = currentTime + yieldInterval
var hasTimeRemaining = true
var hasMoreWork = scheduledHostCallback()
if (!hasMoreWork) {
// 曾经没有工作了,批改状态
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 还有工作,放到下个工作队列执行,给浏览器喘息的机会
port.postMessage (null);
}
} else {isMessageLoopRunning = false;}
}
requestHostCallback = function (callback) {
//callback 挂载到 scheduledHostCallback
scheduledHostCallback = callback
if (!isMessageLoopRunning) {
isMessageLoopRunning = true
// 推送音讯,下个队列队列调用 callback
port.postMessage (null)
}
}
超时工夫的设置就是在以后工夫的根底上加上了一个 yieldInterval
,这个 yieldInterval
的值,默认是 5ms。
deadline = currentTime + yieldInterval
同时 React 也提供了批改 yieldInterval
的伎俩,通过手动指定 fps,来确定一帧的具体工夫(单位:ms),fps 越高,一个工夫分片的工夫就越短,对设施的性能要求就越高。
forceFrameRate = function (fps) {if (fps < 0 || fps > 125) {
// 帧率仅反对 0~125
return
}
if (fps > 0) {
// 个别 60 fps 的设施
// 一个工夫分片的工夫为 Math.floor(1000/60) = 16
yieldInterval = Math.floor(1000 / fps)
} else {
// reset the framerate
yieldInterval = 5
}
}
总结
上面咱们将异步逻辑、循环更新、工夫分片串联起来。先回顾一下之前的文章讲过,Concurrent 模式下,setState 后的调用程序:
Component.setState()
=> enqueueSetState()
=> scheduleUpdate()
=> scheduleCallback(performConcurrentWorkOnRoot)
=> requestHostCallback()
=> postMessage()
=> performWorkUntilDeadline()
scheduleCallback
办法会将传入的回调(performConcurrentWorkOnRoot
)组装成一个工作放入 taskQueue
中,而后调用 requestHostCallback
发送一个音讯,进入异步工作。performWorkUntilDeadline
接管到异步音讯,从 taskQueue
取出工作开始执行,这里的工作就是之前传入的 performConcurrentWorkOnRoot
办法,这个办法最初会调用workLoopConcurrent
(workLoopConcurrent
后面曾经介绍过了,这个不再反复)。如果 workLoopConcurrent
是因为超时中断的,hasMoreWork
返回为 true,通过 postMessage
发送音讯,将操作提早到下一个工作队列。
到这里整个流程曾经完结,心愿大家看完文章能有所播种,下一篇文章会介绍 Fiber 架构下 Hook 的实现。