这篇文章是 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-nlijfclass 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-nlijfexport 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.port2channel.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 的实现。