前面的文章分析了 Concurrent 模式下异步更新的逻辑,以及 Fiber 架构是如何进行时间分片的,更新过程当中的不少内容都省略了,评论区也收到了一些同窗对更新过程的疑惑,今天的文章就来说解下 React Fiber 架构的更新机制。react
咱们先回顾一下 Fiber 节点的数据结构(以前文章省略了一部分属性,因此和以前文章略有不一样):git
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 // 新传入的 props this.pendingProps = pendingProps; // 以前的 props this.memoizedProps = null; // 以前的 state this.memoizedState = null; // 节点更新过时时间,用于时间分片 // react 17 改成:lanes、childLanes this.expirationTime = NoLanes this.childExpirationTime = NoLanes // 对应到页面的真实 DOM 节点 this.stateNode = null // Fiber 节点的副本,能够理解为备胎,主要用于提高更新的性能 this.alternate = null // 反作用相关,用于标记节点是否须要更新 // 以及更新的类型:替换成新节点、更新属性、更新文本、删除…… this.effectTag = NoEffect // 指向下一个须要更新的节点 this.nextEffect = null this.firstEffect = null this.lastEffect = null }
能够注意到 Fiber 节点有个 alternate
属性,该属性在节点初始化的时候默认为空(this.alternate = null
)。这个节点的做用就是用来缓存以前的 Fiber 节点,更新的时候会判断 fiber.alternate
是否为空来肯定当前是首次渲染仍是更新。下面咱们上代码:github
import React from 'react'; import ReactDOM from 'react-dom'; class App extends React.Component { state = { val: 0 } render() { return <div>val: { this.state.val }</div> } } ReactDOM.unstable_createRoot( document.getElementById('root') ).render(<App />)
在调用 createRoot 的时候,会先生成一个FiberRootNode
,在 FiberRootNode
下会有个 current 属性,current 指向 RootFiber
能够理解为一个空 Fiber。后续调用的 render 方法,就是将传入的组件挂载到 FiberRootNode.current
(即 RootFiber
) 的空 Fiber 节点上。数组
// 实验版本对外暴露的 createRoot 须要加上 `unstable_` 前缀 exports.unstable_createRoot = createRoot function createRoot(container) { return new ReactDOMRoot(container) } function ReactDOMRoot(container) { var root = new FiberRootNode() // createRootFiber => createFiber => return new FiberNode(tag); root.current = createRootFiber() // 挂载一个空的 fiber 节点 this._internalRoot = root } ReactDOMRoot.prototype.render = function render(children) { var root = this._internalRoot var update = createUpdate() update.payload = { element: children } const rootFiber = root.current // update对象放到 rootFiber 的 updateQueue 中 enqueueUpdate(rootFiber, update) // 开始更新流程 scheduleUpdateOnFiber(rootFiber) }
render
最后调用 scheduleUpdateOnFiber
进入更新任务,该方法以前有说明,最后会经过 scheduleCallback 走 MessageChannel 消息进入下个任务队列,最后调用 performConcurrentWorkOnRoot
方法。缓存
// scheduleUpdateOnFiber // => ensureRootIsScheduled // => scheduleCallback(performConcurrentWorkOnRoot) function performConcurrentWorkOnRoot(root) { renderRootConcurrent(root) } function renderRootConcurrent(root) { // workInProgressRoot 为空,则建立 workInProgress if (workInProgressRoot !== root) { createWorkInProgress() } } function createWorkInProgress() { workInProgressRoot = root var current = root.current var workInProgress = current.alternate; if (workInProgress === null) { // 第一次构建,须要建立副本 workInProgress = createFiber(current.tag) workInProgress.alternate = current current.alternate = workInProgress } else { // 更新过程能够复用 workInProgress.nextEffect = null workInProgress.firstEffect = null workInProgress.lastEffect = null } }
开始更新时,若是 workInProgress
为空会指向一个新的空 Fiber 节点,表示正在进行工做的 Fiber 节点。数据结构
workInProgress.alternate = current current.alternate = workInProgress
构造好 workInProgress
以后,就会开始在新的 RootFiber 下生成新的子 Fiber 节点了。架构
function renderRootConcurrent(root) { // 构造 workInProgress... // workInProgress.alternate = current // current.alternate = workInProgress // 进入遍历 fiber 树的流程 workLoopConcurrent() } function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork() } } function performUnitOfWork() { var current = workInProgress.alternate // 返回当前 Fiber 的 child const next = beginWork(current, workInProgress) // 省略后续代码... }
按照咱们前面的案例, workLoopConcurrent
调用完成后,最后获得的 fiber 树以下:app
class App extends React.Component { state = { val: 0 } render() { return <div>val: { this.state.val }</div> } }
最后进入 Commit 阶段的时候,会切换 FiberRootNode 的 current 属性:dom
function performConcurrentWorkOnRoot() { renderRootConcurrent() // 结束遍历流程,fiber tree 已经构造完毕 var finishedWork = root.current.alternate root.finishedWork = finishedWork commitRoot(root) } function commitRoot() { var finishedWork = root.finishedWork root.finishedWork = null root.current = finishedWork // 切换到新的 fiber 树 }
上面的流程为第一次渲染,经过 setState({ val: 1 })
更新时,workInProgress
会切换到 root.current.alternate
。异步
function createWorkInProgress() { workInProgressRoot = root var current = root.current var workInProgress = current.alternate; if (workInProgress === null) { // 第一次构建,须要建立副本 workInProgress = createFiber(current.tag) workInProgress.alternate = current current.alternate = workInProgress } else { // 更新过程能够复用 workInProgress.nextEffect = null workInProgress.firstEffect = null workInProgress.lastEffect = null } }
在后续的遍历过程当中(workLoopConcurrent()
),会在旧的 RootFiber 下构建一个新的 fiber tree,而且每一个 fiber 节点的 alternate 都会指向 current fiber tree 下的节点。
这样 FiberRootNode 的 current 属性就会轮流在两棵 fiber tree 不停的切换,即达到了缓存的目的,也不会过度的占用内存。
在 React 15 里,屡次 setState 会被放到一个队列中,等待一次更新。
// setState 方法挂载到原型链上 ReactComponent.prototype.setState = function (partialState, callback) { // 调用 setState 后,会调用内部的 updater.enqueueSetState this.updater.enqueueSetState(this, partialState) }; var ReactUpdateQueue = { enqueueSetState(component, partialState) { // 在组件的 _pendingStateQueue 上暂存新的 state if (!component._pendingStateQueue) { component._pendingStateQueue = [] } // 将 setState 的值放入队列中 var queue = component._pendingStateQueue queue.push(partialState) enqueueUpdate(component) } }
一样在 Fiber 架构中,也会有一个队列用来存放 setState 的值。每一个 Fiber 节点都有一个 updateQueue
属性,这个属性就是用来缓存 setState 值的,只是结构从 React 15 的数组变成了链表结构。
不管是首次 Render 的 Mount 阶段,仍是 setState 的 Update 阶段,内部都会调用 enqueueUpdate
方法。
// --- Render 阶段 --- function initializeUpdateQueue(fiber) { var queue = { baseState: fiber.memoizedState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null }, effects: null } fiber.updateQueue = queue } ReactDOMRoot.prototype.render = function render(children) { var root = this._internalRoot var update = createUpdate() update.payload = { element: children } const rootFiber = root.current // 初始化 rootFiber 的 updateQueue initializeUpdateQueue(rootFiber) // update 对象放到 rootFiber 的 updateQueue 中 enqueueUpdate(rootFiber, update) // 开始更新流程 scheduleUpdateOnFiber(rootFiber) } // --- Update 阶段 --- Component.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState) } var classComponentUpdater = { enqueueSetState: function (inst, payload) { // 获取实例对应的fiber var fiber = get(inst) var update = createUpdate() update.payload = payload // update 对象放到 rootFiber 的 updateQueue 中 enqueueUpdate(fiber, update) scheduleUpdateOnFiber(fiber) } }
enqueueUpdate
方法的主要做用就是将 setState 的值挂载到 Fiber 节点上。
function enqueueUpdate(fiber, update) { var updateQueue = fiber.updateQueue; if (updateQueue === null) { // updateQueue 为空则跳过 return; } var sharedQueue = updateQueue.shared; var pending = sharedQueue.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } sharedQueue.pending = update; }
屡次 setState 会在 sharedQueue.pending
上造成一个单向循环链表,具体例子更形象的展现下这个链表结构。
class App extends React.Component { state = { val: 0 } click () { for (let i = 0; i < 3; i++) { this.setState({ val: this.state.val + 1 }) } } render() { return <div onClick={() => { this.click() }}>val: { this.state.val }</div> } }
点击 div 以后,会连续进行三次 setState,每次 setState 都会更新 updateQueue。
更新过程当中,咱们遍历下 updateQueue 链表,能够看到结果与预期的一致。
let $pending = sharedQueue.pending // 遍历链表,在控制台输出 payload while($pending) { console.log('update.payload', $pending.payload) $pending = $pending.next }
Fiber 架构下每一个节点都会经历递(beginWork)
和归(completeWork)
两个过程:
先回顾下这个流程:
function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork() } } function performUnitOfWork() { var current = workInProgress.alternate // 返回当前 Fiber 的 child const next = beginWork(current, workInProgress) if (next === null) { // child 不存在 completeUnitOfWork() } else { // child 存在 // 重置 workInProgress 为 child workInProgress = next } } function completeUnitOfWork() { // 向上回溯节点 let completedWork = workInProgress while (completedWork !== null) { // 收集反作用,主要是用于标记节点是否须要操做 DOM var current = completedWork.alternate completeWork(current, completedWork) // 省略构造 Effect List 过程 // 获取 Fiber.sibling let siblingFiber = workInProgress.sibling if (siblingFiber) { // sibling 存在,则跳出 complete 流程,继续 beginWork workInProgress = siblingFiber return } completedWork = completedWork.return workInProgress = completedWork } }
先看看 beginWork
进行了哪些操做:
function beginWork(current, workInProgress) { if (current !== null) { // current 不为空,表示须要进行 update var oldProps = current.memoizedProps // 原先传入的 props var newProps = workInProgress.pendingProps // 更新过程当中新的 props // 组件的 props 发生变化,或者 type 发生变化 if (oldProps !== newProps || workInProgress.type !== current.type) { // 设置更新标志位为 true didReceiveUpdate = true } } else { // current 为空表示首次加载,须要进行 mount didReceiveUpdate = false } // tag 表示组件类型,不用类型的组件调用不一样方法获取 child switch(workInProgress.tag) { // 函数组件 case FunctionComponent: return updateFunctionComponent(current, workInProgress, newProps) // Class组件 case ClassComponent: return updateClassComponent(current, workInProgress, newProps) // DOM 原生组件(div、span、button……) case HostComponent: return updateHostComponent(current, workInProgress) // DOM 文本组件 case HostText: return updateHostText(current, workInProgress) } }
首先判断 current(即:workInProgress.alternate)
是否存在,若是存在表示须要更新,不存在就是首次加载,didReceiveUpdate
变量设置为 false,didReceiveUpdate
变量用于标记是否须要调用 render 新建 fiber.child
,若是为 false 就会从新构建fiber.child
,不然复用以前的 fiber.child
。
而后会依据 workInProgress.tag
调用不一样的方法构建 fiber.child
。关于 workInProgress.tag
的含义能够参考 react/packages/shared/ReactWorkTags.js,主要是用来区分每一个节点各自的类型,下面是经常使用的几个:
var FunctionComponent = 0; // 函数组件 var ClassComponent = 1; // Class组件 var HostComponent = 5; // 原生组件 var HostText = 6; // 文本组件
调用的方法不一一展开讲解,咱们只看看 updateClassComponent
:
// 更新 class 组件 function updateClassComponent(current, workInProgress, newProps) { // 更新 state,省略了一万行代码,只保留了核心逻辑,看看就好 var oldState = workInProgress.memoizedState var newState = oldState var queue = workInProgress.updateQueue var pendingQueue = queue.shared.pending var firstUpdate = pendingQueue var update = pendingQueue do { // 合并 state var partialState = update.payload newState = Object.assign({}, newState, partialState) // 链表遍历完毕 update = update.next if (update === firstUpdate) { // 链表遍历完毕 queue.shared.pending = null break } } while (true) workInProgress.memoizedState = newState // state 更新完毕 // 检测 oldState 和 newState 是否一致,若是一致,跳过更新 // 调用 componentWillUpdate 判断是否须要更新 var instance = workInProgress.stateNode instance.props = newProps instance.state = newState // 调用 Component 实例的 render var nextChildren = instance.render() reconcileChildren(current, workInProgress, nextChildren) return workInProgress.child }
首先遍历了以前提到的 updateQueue
更新 state
,而后就是判断 state
是否更新,以此来推到组件是否须要更新(这部分代码省略了),最后调用的组件 render
方法生成子组件的虚拟 DOM。最后的 reconcileChildren
就是依据 render
的返回值来生成 fiber 节点并挂载到 workInProgress.child
上。
// 构造子节点 function reconcileChildren(current, workInProgress, nextChildren) { if (current === null) { workInProgress.child = mountChildFibers( workInProgress, null, nextChildren ) } else { workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren ) } } // 两个方法本质上同样,只是一个须要生成新的 fiber,一个复用以前的 var reconcileChildFibers = ChildReconciler(true) var mountChildFibers = ChildReconciler(false) function ChildReconciler(shouldTrackSideEffects) { return function (returnFiber, currentChild, nextChildren) { // 不一样类型进行不一样的处理 // 返回对象 if (typeof newChild === 'object' && newChild !== null) { return placeSingleChild( reconcileSingleElement( returnFiber, currentChild, newChild ) ) } // 返回数组 if (Array.isArray(newChild)) { // ... } // 返回字符串或数字,代表是文本节点 if ( typeof newChild === 'string' || typeof newChild === 'number' ) { // ... } // 返回 null,直接删除节点 return deleteRemainingChildren(returnFiber, currentChild) } }
篇幅有限,看看 render 返回值为对象的状况(一般状况下,render 方法 return 的若是是 jsx 都会被转化为虚拟 DOM,而虚拟 DOM 一定是对象或数组):
if (typeof newChild === 'object' && newChild !== null) { return placeSingleChild( // 构造 fiber,或者是复用 fiber reconcileSingleElement( returnFiber, currentChild, newChild ) ) } function placeSingleChild(newFiber) { // 更新操做,须要设置 effectTag if (shouldTrackSideEffects && newFiber.alternate === null) { newFiber.effectTag = Placement } return newFiber }
当 fiber.child
为空时,就会进入 completeWork
流程。而 completeWork
主要就是收集 beginWork
阶段设置的 effectTag
,若是有设置 effectTag
就代表该节点发生了变动, effectTag
的主要类型以下(默认为 NoEffect
,表示节点无需进行操做,完整的定义能够参考 react/packages/shared/ReactSideEffectTags.js):
export const NoEffect = /* */ 0b000000000000000; export const PerformedWork = /* */ 0b000000000000001; // You can change the rest (and add more). export const Placement = /* */ 0b000000000000010; export const Update = /* */ 0b000000000000100; export const PlacementAndUpdate = /* */ 0b000000000000110; export const Deletion = /* */ 0b000000000001000; export const ContentReset = /* */ 0b000000000010000; export const Callback = /* */ 0b000000000100000; export const DidCapture = /* */ 0b000000001000000;
咱们看看 completeWork
过程当中,具体进行了哪些操做:
function completeWork(current, workInProgress) { switch (workInProgress.tag) { // 这些组件没有反应到 DOM 的 effect,跳过处理 case Fragment: case MemoComponent: case LazyComponent: case ContextConsumer: case FunctionComponent: return null // class 组件 case ClassComponent: { // 处理 context var Component = workInProgress.type if (isContextProvider(Component)) { popContext(workInProgress) } return null } case HostComponent: { // 这里 Fiber 的 props 对应的就是 DOM 节点的 props // 例如: id、src、className …… var newProps = workInProgress.pendingProps // props if ( current !== null && workInProgress.stateNode != null ) { // current 不为空,表示是更新操做 var type = workInProgress.type updateHostComponent(current, workInProgress, type, newProps) } else { // current 为空,表示须要渲染 DOM 节点 // 实例化 DOM,挂载到 fiber.stateNode var instance = createInstance(type, newProps) appendAllChildren(instance, workInProgress, false, false); workInProgress.stateNode = instance } return null } case HostText: { var newText = workInProgress.pendingProps // props if (current && workInProgress.stateNode != null) { var oldText = current.memoizedProps // 更新文本节点 updateHostText(current, workInProgress, oldText, newText) } else { // 实例文本节点 workInProgress.stateNode = createTextInstance(newText) } return null } } }
与 beginWork
同样,completeWork
过程当中也会依据 workInProgress.tag
来进行不一样的处理,其余类型的组件基本能够略过,只用关注下 HostComponent
、HostText
,这两种类型的节点会反应到真实 DOM 中,因此会有所处理。
updateHostComponent = function ( current, workInProgress, type, newProps ) { var oldProps = current.memoizedProps if (oldProps === newProps) { // 新旧 props 无变化 return } var instance = workInProgress.stateNode // DOM 实例 // 对比新旧 props var updatePayload = diffProperties(instance, type, oldProps, newProps) // 将发生变化的属性放入 updateQueue // 注意这里的 updateQueue 不一样于 Class 组件对应的 fiber.updateQueue workInProgress.updateQueue = updatePayload };
updateHostComponent
方法最后会经过 diffProperties
方法获取一个更新队列,挂载到 fiber.updateQueue
上,这里的 updateQueue 不一样于 Class 组件对应的 fiber.updateQueue
,不是一个链表结构,而是一个数组结构,用于更新真实 DOM。
下面举一个例子,修改 App 组件的 state 后,下面的 span 标签对应的 data-val
、style
、children
都会相应的发生修改,同时,在控制台打印出 updatePayload
的结果。
import React from 'react' class App extends React.Component { state = { val: 1 } clickBtn = () => { this.setState({ val: this.state.val + 1 }) } render() { return (<div> <button onClick={this.clickBtn}>add</button> <span data-val={this.state.val} style={{ fontSize: this.state.val * 15 }} > { this.state.val } </span> </div>) } } export default App
在最后的更新阶段,为了避免用遍历全部的节点,在 completeWork
过程结束后,会构造一个 effectList 链接全部 effectTag 不为 NoEffect 的节点,在 commit 阶段可以更高效的遍历节点。
function completeUnitOfWork() { let completedWork = workInProgress while (completedWork !== null) { // 调用 completeWork()... // 构造 Effect List 过程 var returnFiber = completedWork.return if (returnFiber !== null) { if (returnFiber.firstEffect === null) { returnFiber.firstEffect = completedWork.firstEffect; } if (completedWork.lastEffect !== null) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork.firstEffect; } returnFiber.lastEffect = completedWork.lastEffect; } if (completedWork.effectTag > PerformedWork) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork } else { returnFiber.firstEffect = completedWork } returnFiber.lastEffect = completedWork } } // 判断 completedWork.sibling 是否存在... } }
上面的代码就是构造 effectList 的过程,光看代码仍是比较难理解的,咱们仍是经过实际的代码来解释一下。
import React from 'react' export default class App extends React.Component { state = { val: 0 } click = () => { this.setState({ val: this.state.val + 1 }) } render() { const { val } = this.state const array = Array(2).fill() const rows = array.map( (_, row) => <tr key={row}> {array.map( (_, col) => <td key={col}>{val}</td> )} </tr> ) return <table onClick={() => this.click()}> {rows} </table> } }
咱们构造一个 2 * 2 的 Table,每次点击组件,td 的 children 都会发生修改,下面看看这个过程当中的 effectList 是如何变化的。
第一个 td 完成 completeWork
后,EffectList 结果以下:
第二个 td 完成 completeWork
后,EffectList 结果以下:
两个 td 结束了 completeWork
流程,会回溯到 tr 进行 completeWork
,tr 结束流程后 ,table 会直接复用 tr 的 firstEffect 和 lastEffect,EffectList 结果以下:
后面两个 td 结束 completeWork
流程后,EffectList 结果以下:
回溯到第二个 tr 进行 completeWork
,因为 table 已经存在 firstEffect 和 lastEffect,这里会直接修改 table 的 firstEffect 的 nextEffect,以及从新指定 lastEffect,EffectList 结果以下:
最后回溯到 App 组件时,就会直接复用 table 的 firstEffect 和 lastEffect,最后 的EffectList 结果以下:
这一阶段的主要做用就是遍历 effectList 里面的节点,将更新反应到真实 DOM 中,固然还涉及一些生命周期钩子的调用,咱们这里只展现最简单的逻辑。
function commitRoot(root) { var finishedWork = root.finishedWork var firstEffect = finishedWork var nextEffect = firstEffect // 遍历effectList while (nextEffect !== null) { const effectTag = nextEffect.effectTag // 根据 effectTag 进行不一样的处理 switch (effectTag) { // 插入 DOM 节点 case Placement: { commitPlacement(nextEffect) nextEffect.effectTag &= ~Placement break } // 更新 DOM 节点 case Update: { const current = nextEffect.alternate commitWork(current, nextEffect) break } // 删除 DOM 节点 case Deletion: { commitDeletion(root, nextEffect) break } } nextEffect = nextEffect.nextEffect } }
这里再也不展开讲解每一个 effect 下具体的操做,在遍历完 effectList 以后,就是将当前的 fiber 树进行切换。
function commitRoot() { var finishedWork = root.finishedWork // 遍历 effectList …… root.finishedWork = null root.current = finishedWork // 切换到新的 fiber 树 }
到这里整个更新流程就结束了,能够看到 Fiber 架构下,全部数据结构都是链表形式,链表的遍历都是经过循环的方式来实现的,看代码的过程当中常常会被忽然出现的 return、break 扰乱思路,因此要彻底理解这个流程仍是很不容易的。
最后,但愿你们在阅读文章的过程当中能有收获,下一篇文章会开始写 Hooks 相关的内容。