React 架构的演变 - 更新机制

开发 架构
前面的文章分析了 Concurrent 模式下异步更新的逻辑,以及 Fiber 架构是如何进行时间分片的,更新过程中的很多内容都省略了,评论区也收到了一些同学对更新过程的疑惑,今天的文章就来讲解下 React Fiber 架构的更新机制。

[[345839]]

前面的文章分析了 Concurrent 模式下异步更新的逻辑,以及 Fiber 架构是如何进行时间分片的,更新过程中的很多内容都省略了,评论区也收到了一些同学对更新过程的疑惑,今天的文章就来讲解下 React Fiber 架构的更新机制。

Fiber 数据结构

我们先回顾一下 Fiber 节点的数据结构(之前文章省略了一部分属性,所以和之前文章略有不同):

  1. function FiberNode (tag, key) { 
  2.   // 节点 key,主要用于了优化列表 diff 
  3.   this.key = key 
  4.   // 节点类型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ... 
  5.   this.tag = tag 
  6.  
  7.  // 子节点 
  8.   this.child = null 
  9.   // 父节点 
  10.   this.return = null  
  11.   // 兄弟节点 
  12.   this.sibling = null 
  13.    
  14.   // 更新队列,用于暂存 setState 的值 
  15.   this.updateQueue = null 
  16.   // 新传入的 props 
  17.   this.pendingProps = pendingProps; 
  18.   // 之前的 props 
  19.   this.memoizedProps = null
  20.   // 之前的 state 
  21.   this.memoizedState = null
  22.  
  23.   // 节点更新过期时间,用于时间分片 
  24.   // react 17 改为:lanes、childLanes 
  25.   this.expirationTime = NoLanes 
  26.   this.childExpirationTime = NoLanes 
  27.  
  28.   // 对应到页面的真实 DOM 节点 
  29.   this.stateNode = null 
  30.   // Fiber 节点的副本,可以理解为备胎,主要用于提升更新的性能 
  31.   this.alternate = null 
  32.  
  33.   // 副作用相关,用于标记节点是否需要更新 
  34.   // 以及更新的类型:替换成新节点、更新属性、更新文本、删除…… 
  35.   this.effectTag = NoEffect 
  36.   // 指向下一个需要更新的节点 
  37.   this.nextEffect = null 
  38.   this.firstEffect = null 
  39.   this.lastEffect = null 

缓存机制

可以注意到 Fiber 节点有个 alternate 属性,该属性在节点初始化的时候默认为空(this.alternate = null)。这个节点的作用就是用来缓存之前的 Fiber 节点,更新的时候会判断 fiber.alternate 是否为空来确定当前是首次渲染还是更新。下面我们上代码:

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3.  
  4. class App extends React.Component { 
  5.   state = { val: 0 } 
  6.   render() { 
  7.     return <div>val: { this.state.val }</div> 
  8.   } 
  9.  
  10. ReactDOM.unstable_createRoot( 
  11.   document.getElementById('root'
  12. ).render(<App />) 

 

在调用 createRoot 的时候,会先生成一个FiberRootNode,在 FiberRootNode 下会有个 current 属性,current 指向 RootFiber 可以理解为一个空 Fiber。后续调用的 render 方法,就是将传入的组件挂载到 FiberRootNode.current(即 RootFiber) 的空 Fiber 节点上。

  1. // 实验版本对外暴露的 createRoot 需要加上 `unstable_` 前缀 
  2. exports.unstable_createRoot = createRoot 
  3.  
  4. function createRoot(container) { 
  5.   return new ReactDOMRoot(container) 
  6. function ReactDOMRoot(container) { 
  7.   var root = new FiberRootNode() 
  8.   // createRootFiber => createFiber => return new FiberNode(tag); 
  9.   root.current = createRootFiber() // 挂载一个空的 fiber 节点 
  10.   this._internalRoot = root 
  11. ReactDOMRoot.prototype.render = function render(children) { 
  12.   var root = this._internalRoot 
  13.   var update = createUpdate() 
  14.   update.payload = { element: children } 
  15.   const rootFiber = root.current 
  16.   // update对象放到 rootFiber 的 updateQueue 中 
  17.   enqueueUpdate(rootFiber, update
  18.   // 开始更新流程 
  19.   scheduleUpdateOnFiber(rootFiber) 

render 最后调用 scheduleUpdateOnFiber 进入更新任务,该方法之前有说明,最后会通过 scheduleCallback 走 MessageChannel 消息进入下个任务队列,最后调用 performConcurrentWorkOnRoot 方法。

  1. // scheduleUpdateOnFiber 
  2. // => ensureRootIsScheduled 
  3. // => scheduleCallback(performConcurrentWorkOnRoot) 
  4. function performConcurrentWorkOnRoot(root) { 
  5.   renderRootConcurrent(root) 
  6. function renderRootConcurrent(root) { 
  7.   // workInProgressRoot 为空,则创建 workInProgress 
  8.   if (workInProgressRoot !== root) { 
  9.     createWorkInProgress() 
  10.   } 
  11. function createWorkInProgress() { 
  12.   workInProgressRoot = root 
  13.   var current = root.current 
  14.   var workInProgress = current.alternate; 
  15.   if (workInProgress === null) { 
  16.     // 第一次构建,需要创建副本 
  17.     workInProgress = createFiber(current.tag) 
  18.     workInProgress.alternate = current 
  19.     current.alternate = workInProgress 
  20.   } else { 
  21.     // 更新过程可以复用 
  22.     workInProgress.nextEffect = null 
  23.     workInProgress.firstEffect = null 
  24.     workInProgress.lastEffect = null 
  25.   } 

开始更新时,如果 workInProgress 为空会指向一个新的空 Fiber 节点,表示正在进行工作的 Fiber 节点。

  1. workInProgress.alternate = current 
  2. current.alternate = workInProgress 

 


 

fiber tree

 

构造好 workInProgress 之后,就会开始在新的 RootFiber 下生成新的子 Fiber 节点了。

  1. function renderRootConcurrent(root) { 
  2.   // 构造 workInProgress... 
  3.   // workInProgress.alternate = current 
  4.  // current.alternate = workInProgress 
  5.  
  6.   // 进入遍历 fiber 树的流程 
  7.   workLoopConcurrent() 
  8.  
  9. function workLoopConcurrent() { 
  10.   while (workInProgress !== null && !shouldYield()) { 
  11.     performUnitOfWork() 
  12.   } 
  13.  
  14. function performUnitOfWork() { 
  15.   var current = workInProgress.alternate 
  16.   // 返回当前 Fiber 的 child 
  17.   const next = beginWork(current, workInProgress) 
  18.   // 省略后续代码... 

按照我们前面的案例, workLoopConcurrent 调用完成后,最后得到的 fiber 树如下:

  1. class App extends React.Component { 
  2.   state = { val: 0 } 
  3.   render() { 
  4.     return <div>val: { this.state.val }</div> 
  5.   } 

 

 

 


 

fiber tree

 

最后进入 Commit 阶段的时候,会切换 FiberRootNode 的 current 属性:

  1. function performConcurrentWorkOnRoot() { 
  2.   renderRootConcurrent() // 结束遍历流程,fiber tree 已经构造完毕 
  3.  
  4.   var finishedWork = root.current.alternate 
  5.   root.finishedWork = finishedWork 
  6.   commitRoot(root) 
  7. function commitRoot() { 
  8.   var finishedWork = root.finishedWork 
  9.   root.finishedWork = null 
  10.   root.current = finishedWork // 切换到新的 fiber 树 

fiber tree

 

上面的流程为第一次渲染,通过 setState({ val: 1 }) 更新时,workInProgress 会切换到 root.current.alternate。

  1. function createWorkInProgress() { 
  2.   workInProgressRoot = root 
  3.   var current = root.current 
  4.   var workInProgress = current.alternate; 
  5.   if (workInProgress === null) { 
  6.     // 第一次构建,需要创建副本 
  7.     workInProgress = createFiber(current.tag) 
  8.     workInProgress.alternate = current 
  9.     current.alternate = workInProgress 
  10.   } else { 
  11.     // 更新过程可以复用 
  12.     workInProgress.nextEffect = null 
  13.     workInProgress.firstEffect = null 
  14.     workInProgress.lastEffect = null 
  15.   } 

fiber tree

 

在后续的遍历过程中(workLoopConcurrent()),会在旧的 RootFiber 下构建一个新的 fiber tree,并且每个 fiber 节点的 alternate 都会指向 current fiber tree 下的节点。

fiber tree

 

这样 FiberRootNode 的 current 属性就会轮流在两棵 fiber tree 不停的切换,即达到了缓存的目的,也不会过分的占用内存。

更新队列

在 React 15 里,多次 setState 会被放到一个队列中,等待一次更新。

  1. // setState 方法挂载到原型链上 
  2. ReactComponent.prototype.setState = function (partialState, callback) { 
  3.   // 调用 setState 后,会调用内部的 updater.enqueueSetState 
  4.   this.updater.enqueueSetState(this, partialState) 
  5. }; 
  6.  
  7. var ReactUpdateQueue = { 
  8.   enqueueSetState(component, partialState) { 
  9.     // 在组件的 _pendingStateQueue 上暂存新的 state 
  10.     if (!component._pendingStateQueue) { 
  11.       component._pendingStateQueue = [] 
  12.     } 
  13.     // 将 setState 的值放入队列中 
  14.     var queue = component._pendingStateQueue 
  15.     queue.push(partialState) 
  16.     enqueueUpdate(component) 
  17.   } 

同样在 Fiber 架构中,也会有一个队列用来存放 setState 的值。每个 Fiber 节点都有一个 updateQueue 属性,这个属性就是用来缓存 setState 值的,只是结构从 React 15 的数组变成了链表结构。

无论是首次 Render 的 Mount 阶段,还是 setState 的 Update 阶段,内部都会调用 enqueueUpdate 方法。

  1. // --- Render 阶段 --- 
  2. function initializeUpdateQueue(fiber) { 
  3.   var queue = { 
  4.     baseState: fiber.memoizedState, 
  5.     firstBaseUpdate: null
  6.     lastBaseUpdate: null
  7.     shared: { 
  8.       pending: null 
  9.     }, 
  10.     effects: null 
  11.   } 
  12.   fiber.updateQueue = queue 
  13. ReactDOMRoot.prototype.render = function render(children) { 
  14.   var root = this._internalRoot 
  15.   var update = createUpdate() 
  16.   update.payload = { element: children } 
  17.   const rootFiber = root.current 
  18.   // 初始化 rootFiber 的 updateQueue 
  19.   initializeUpdateQueue(rootFiber) 
  20.   // update 对象放到 rootFiber 的 updateQueue 中 
  21.   enqueueUpdate(rootFiber, update
  22.   // 开始更新流程 
  23.   scheduleUpdateOnFiber(rootFiber) 
  24.  
  25. // --- Update 阶段 --- 
  26. Component.prototype.setState = function (partialState, callback) { 
  27.   this.updater.enqueueSetState(this, partialState) 
  28. var classComponentUpdater = { 
  29.   enqueueSetState: function (inst, payload) { 
  30.     // 获取实例对应的fiber 
  31.     var fiber = get(inst) 
  32.     var update = createUpdate() 
  33.     update.payload = payload 
  34.  
  35.     // update 对象放到 rootFiber 的 updateQueue 中 
  36.     enqueueUpdate(fiber, update
  37.     scheduleUpdateOnFiber(fiber) 
  38.   } 

enqueueUpdate 方法的主要作用就是将 setState 的值挂载到 Fiber 节点上。

  1. function enqueueUpdate(fiber, update) { 
  2.   var updateQueue = fiber.updateQueue; 
  3.  
  4.   if (updateQueue === null) { 
  5.     // updateQueue 为空则跳过 
  6.     return
  7.   } 
  8.   var sharedQueue = updateQueue.shared; 
  9.   var pending = sharedQueue.pending; 
  10.  
  11.   if (pending === null) { 
  12.     update.next = update
  13.   } else { 
  14.     update.next = pending.next
  15.     pending.next = update
  16.   } 
  17.  
  18.   sharedQueue.pending = update

多次 setState 会在 sharedQueue.pending 上形成一个单向循环链表,具体例子更形象的展示下这个链表结构。

  1. class App extends React.Component { 
  2.   state = { val: 0 } 
  3.   click () { 
  4.     for (let i = 0; i < 3; i++) { 
  5.       this.setState({ val: this.state.val + 1 }) 
  6.     } 
  7.   } 
  8.   render() { 
  9.     return <div onClick={() => { 
  10.       this.click() 
  11.     }}>val: { this.state.val }</div> 
  12.   } 

 

点击 div 之后,会连续进行三次 setState,每次 setState 都会更新 updateQueue。

第一次 setState

第二次 setState

第三次 setState

 

更新过程中,我们遍历下 updateQueue 链表,可以看到结果与预期的一致。

  1. let $pending = sharedQueue.pending 
  2. // 遍历链表,在控制台输出 payload 
  3. while($pending) { 
  4.   console.log('update.payload', $pending.payload) 
  5.   $pending = $pending.next 

 

 


 

链表数据

 

递归 Fiber 节点

Fiber 架构下每个节点都会经历递(beginWork)和归(completeWork)两个过程:

  • beginWork:生成新的 state,调用 render 创建子节点,连接当前节点与子节点;
  • completeWork:依据 EffectTag 收集 Effect,构造 Effect List;

先回顾下这个流程:

  1. function workLoopConcurrent() { 
  2.   while (workInProgress !== null && !shouldYield()) { 
  3.     performUnitOfWork() 
  4.   } 
  5.  
  6. function performUnitOfWork() { 
  7.   var current = workInProgress.alternate 
  8.   // 返回当前 Fiber 的 child 
  9.   const next = beginWork(current, workInProgress) 
  10.   if (next === null) { // child 不存在 
  11.     completeUnitOfWork() 
  12.   } else { // child 存在 
  13.     // 重置 workInProgress 为 child 
  14.     workInProgress = next 
  15.   } 
  16. function completeUnitOfWork() { 
  17.   // 向上回溯节点 
  18.   let completedWork = workInProgress 
  19.   while (completedWork !== null) { 
  20.     // 收集副作用,主要是用于标记节点是否需要操作 DOM 
  21.     var current = completedWork.alternate 
  22.     completeWork(current, completedWork) 
  23.  
  24.     // 省略构造 Effect List 过程 
  25.  
  26.     // 获取 Fiber.sibling 
  27.     let siblingFiber = workInProgress.sibling 
  28.     if (siblingFiber) { 
  29.       // sibling 存在,则跳出 complete 流程,继续 beginWork 
  30.       workInProgress = siblingFiber 
  31.       return 
  32.     } 
  33.  
  34.     completedWork = completedWork.return 
  35.     workInProgress = completedWork 
  36.   } 

递(beginWork)

先看看 beginWork 进行了哪些操作:

  1. function beginWork(current, workInProgress) { 
  2.   if (current !== null) { // current 不为空,表示需要进行 update 
  3.     var oldProps = current.memoizedProps // 原先传入的 props 
  4.     var newProps = workInProgress.pendingProps // 更新过程中新的 props 
  5.     // 组件的 props 发生变化,或者 type 发生变化 
  6.     if (oldProps !== newProps || workInProgress.type !== current.type) { 
  7.       // 设置更新标志位为 true 
  8.       didReceiveUpdate = true 
  9.     } 
  10.   } else { // current 为空表示首次加载,需要进行 mount 
  11.     didReceiveUpdate = false 
  12.   } 
  13.    
  14.   // tag 表示组件类型,不用类型的组件调用不同方法获取 child 
  15.   switch(workInProgress.tag) { 
  16.     // 函数组件 
  17.     case FunctionComponent: 
  18.       return updateFunctionComponent(current, workInProgress, newProps) 
  19.     // Class组件 
  20.     case ClassComponent: 
  21.       return updateClassComponent(current, workInProgress, newProps) 
  22.     // DOM 原生组件(div、span、button……) 
  23.     case HostComponent: 
  24.       return updateHostComponent(current, workInProgress) 
  25.     // DOM 文本组件 
  26.     case HostText: 
  27.       return updateHostText(current, workInProgress) 
  28.   } 

首先判断 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,主要是用来区分每个节点各自的类型,下面是常用的几个:

  1. var FunctionComponent = 0; // 函数组件 
  2. var ClassComponent = 1; // Class组件 
  3. var HostComponent = 5; // 原生组件 
  4. var HostText = 6; // 文本组件 

调用的方法不一一展开讲解,我们只看看 updateClassComponent:

  1. // 更新 class 组件 
  2. function updateClassComponent(current, workInProgress, newProps) { 
  3.   // 更新 state,省略了一万行代码,只保留了核心逻辑,看看就好 
  4.   var oldState = workInProgress.memoizedState 
  5.   var newState = oldState 
  6.  
  7.   var queue = workInProgress.updateQueue 
  8.   var pendingQueue = queue.shared.pending 
  9.   var firstUpdate = pendingQueue 
  10.   var update = pendingQueue 
  11.  
  12.   do { 
  13.     // 合并 state 
  14.     var partialState = update.payload 
  15.     newState = Object.assign({}, newState, partialState) 
  16.  
  17.     // 链表遍历完毕 
  18.     update = update.next 
  19.     if (update === firstUpdate) { 
  20.      // 链表遍历完毕 
  21.       queue.shared.pending = null 
  22.       break 
  23.     } 
  24.   } while (true
  25.  
  26.  workInProgress.memoizedState = newState // state 更新完毕 
  27.    
  28.   // 检测 oldState 和 newState 是否一致,如果一致,跳过更新 
  29.   // 调用 componentWillUpdate 判断是否需要更新 
  30.    
  31.  
  32.   var instance = workInProgress.stateNode 
  33.   instance.props = newProps 
  34.   instance.state = newState 
  35.  
  36.   // 调用 Component 实例的 render 
  37.   var nextChildren = instance.render() 
  38.   reconcileChildren(current, workInProgress, nextChildren) 
  39.   return workInProgress.child 

首先遍历了之前提到的 updateQueue 更新 state,然后就是判断 state 是否更新,以此来推到组件是否需要更新(这部分代码省略了),最后调用的组件 render 方法生成子组件的虚拟 DOM。最后的 reconcileChildren 就是依据 render 的返回值来生成 fiber 节点并挂载到 workInProgress.child 上。

  1. // 构造子节点 
  2. function reconcileChildren(current, workInProgress, nextChildren) { 
  3.   if (current === null) { 
  4.     workInProgress.child = mountChildFibers( 
  5.       workInProgress, null, nextChildren 
  6.     ) 
  7.   } else { 
  8.     workInProgress.child = reconcileChildFibers( 
  9.       workInProgress, current.child, nextChildren 
  10.     ) 
  11.   } 
  12.  
  13. // 两个方法本质上一样,只是一个需要生成新的 fiber,一个复用之前的 
  14. var reconcileChildFibers = ChildReconciler(true
  15. var mountChildFibers = ChildReconciler(false
  16.  
  17. function ChildReconciler(shouldTrackSideEffects) { 
  18.   return function (returnFiber, currentChild, nextChildren) { 
  19.     // 不同类型进行不同的处理 
  20.     // 返回对象 
  21.     if (typeof newChild === 'object' && newChild !== null) { 
  22.    return placeSingleChild( 
  23.         reconcileSingleElement( 
  24.           returnFiber, currentChild, newChild 
  25.         ) 
  26.       ) 
  27.     } 
  28.     // 返回数组 
  29.     if (Array.isArray(newChild)) { 
  30.       // ... 
  31.     } 
  32.     // 返回字符串或数字,表明是文本节点 
  33.     if ( 
  34.       typeof newChild === 'string' || 
  35.       typeof newChild === 'number' 
  36.     ) { 
  37.       // ... 
  38.     } 
  39.     // 返回 null,直接删除节点 
  40.     return deleteRemainingChildren(returnFiber, currentChild) 
  41.   } 

篇幅有限,看看 render 返回值为对象的情况(通常情况下,render 方法 return 的如果是 jsx 都会被转化为虚拟 DOM,而虚拟 DOM 必定是对象或数组):

  1. if (typeof newChild === 'object' && newChild !== null) { 
  2.   return placeSingleChild( 
  3.     // 构造 fiber,或者是复用 fiber 
  4.     reconcileSingleElement( 
  5.       returnFiber, currentChild, newChild 
  6.     ) 
  7.   ) 
  8.  
  9. function placeSingleChild(newFiber) { 
  10.   // 更新操作,需要设置 effectTag 
  11.   if (shouldTrackSideEffects && newFiber.alternate === null) { 
  12.     newFiber.effectTag = Placement 
  13.   } 
  14.   return newFiber 

归(completeWork)

当 fiber.child 为空时,就会进入 completeWork 流程。而 completeWork 主要就是收集 beginWork 阶段设置的 effectTag,如果有设置 effectTag 就表明该节点发生了变更, effectTag 的主要类型如下(默认为 NoEffect ,表示节点无需进行操作,完整的定义可以参考 react/packages/shared/ReactSideEffectTags.js):

  1. export const NoEffect = /*                     */ 0b000000000000000; 
  2. export const PerformedWork = /*                */ 0b000000000000001; 
  3.  
  4. // You can change the rest (and add more). 
  5. export const Placement = /*                    */ 0b000000000000010; 
  6. export const Update = /*                       */ 0b000000000000100; 
  7. export const PlacementAndUpdate = /*           */ 0b000000000000110; 
  8. export const Deletion = /*                     */ 0b000000000001000; 
  9. export const ContentReset = /*                 */ 0b000000000010000; 
  10. export const Callback = /*                     */ 0b000000000100000; 
  11. export const DidCapture = /*                   */ 0b000000001000000; 

我们看看 completeWork 过程中,具体进行了哪些操作:

  1. function completeWork(current, workInProgress) { 
  2.   switch (workInProgress.tag) { 
  3.     // 这些组件没有反应到 DOM 的 effect,跳过处理 
  4.     case Fragment: 
  5.     case MemoComponent: 
  6.     case LazyComponent: 
  7.     case ContextConsumer: 
  8.     case FunctionComponent: 
  9.       return null 
  10.     // class 组件 
  11.     case ClassComponent: { 
  12.       // 处理 context 
  13.       var Component = workInProgress.type 
  14.       if (isContextProvider(Component)) { 
  15.         popContext(workInProgress) 
  16.       } 
  17.       return null 
  18.     } 
  19.     case HostComponent: { 
  20.       // 这里 Fiber 的 props 对应的就是 DOM 节点的 props 
  21.       // 例如:id、src、className …… 
  22.     var newProps = workInProgress.pendingProps // props 
  23.       if ( 
  24.         current !== null && 
  25.         workInProgress.stateNode != null 
  26.       ) { // current 不为空,表示是更新操作 
  27.         var type = workInProgress.type 
  28.         updateHostComponent(current, workInProgress, type, newProps) 
  29.       } else { // current 为空,表示需要渲染 DOM 节点 
  30.         // 实例化 DOM,挂载到 fiber.stateNode 
  31.         var instance = createInstance(type, newProps) 
  32.         appendAllChildren(instance, workInProgress, falsefalse); 
  33.         workInProgress.stateNode = instance 
  34.       } 
  35.       return null 
  36.     } 
  37.     case HostText: { 
  38.       var newText = workInProgress.pendingProps // props 
  39.       if (current && workInProgress.stateNode != null) { 
  40.         var oldText = current.memoizedProps 
  41.         // 更新文本节点 
  42.         updateHostText(current, workInProgress, oldText, newText) 
  43.       } else { 
  44.         // 实例文本节点 
  45.         workInProgress.stateNode = createTextInstance(newText) 
  46.       } 
  47.       return null 
  48.     } 
  49.   } 

与 beginWork 一样,completeWork 过程中也会依据 workInProgress.tag 来进行不同的处理,其他类型的组件基本可以略过,只用关注下 HostComponent、HostText,这两种类型的节点会反应到真实 DOM 中,所以会有所处理。

  1. updateHostComponent = function ( 
  2.  current, workInProgress, type, newProps 
  3. ) { 
  4.   var oldProps = current.memoizedProps 
  5.  
  6.   if (oldProps === newProps) { 
  7.     // 新旧 props 无变化 
  8.     return 
  9.   } 
  10.  
  11.   var instance = workInProgress.stateNode // DOM 实例 
  12.   // 对比新旧 props 
  13.  var updatePayload = diffProperties(instance, type, oldProps, newProps) 
  14.   // 将发生变化的属性放入 updateQueue 
  15.   // 注意这里的 updateQueue 不同于 Class 组件对应的 fiber.updateQueue 
  16.   workInProgress.updateQueue = updatePayload 
  17. }; 

updateHostComponent 方法最后会通过 diffProperties 方法获取一个更新队列,挂载到 fiber.updateQueue 上,这里的 updateQueue 不同于 Class 组件对应的 fiber.updateQueue,不是一个链表结构,而是一个数组结构,用于更新真实 DOM。

下面举一个例子,修改 App 组件的 state 后,下面的 span 标签对应的 data-val、style、children 都会相应的发生修改,同时,在控制台打印出 updatePayload 的结果。

  1. import React from 'react' 
  2.  
  3. class App extends React.Component { 
  4.   state = { val: 1 } 
  5.   clickBtn = () => { 
  6.     this.setState({ val: this.state.val + 1 }) 
  7.   } 
  8.   render() { 
  9.     return (<div> 
  10.       <button onClick={this.clickBtn}>add</button> 
  11.       <span 
  12.         data-val={this.state.val} 
  13.         style={{ fontSize: this.state.val * 15 }} 
  14.       > 
  15.         { this.state.val } 
  16.       </span> 
  17.     </div>) 
  18.   } 
  19.  
  20. export default App 

 

 

 


 

console

 

副作用链表

在最后的更新阶段,为了不用遍历所有的节点,在 completeWork 过程结束后,会构造一个 effectList 连接所有 effectTag 不为 NoEffect 的节点,在 commit 阶段能够更高效的遍历节点。

  1. function completeUnitOfWork() { 
  2.   let completedWork = workInProgress 
  3.   while (completedWork !== null) { 
  4.     // 调用 completeWork()... 
  5.  
  6.     // 构造 Effect List 过程 
  7.     var returnFiber = completedWork.return 
  8.     if (returnFiber !== null) { 
  9.       if (returnFiber.firstEffect === null) { 
  10.         returnFiber.firstEffect = completedWork.firstEffect; 
  11.       } 
  12.       if (completedWork.lastEffect !== null) { 
  13.         if (returnFiber.lastEffect !== null) { 
  14.           returnFiber.lastEffect.nextEffect = completedWork.firstEffect; 
  15.         } 
  16.         returnFiber.lastEffect = completedWork.lastEffect; 
  17.       } 
  18.  
  19.       if (completedWork.effectTag > PerformedWork) { 
  20.         if (returnFiber.lastEffect !== null) { 
  21.           returnFiber.lastEffect.nextEffect = completedWork 
  22.         } else { 
  23.           returnFiber.firstEffect = completedWork 
  24.         } 
  25.         returnFiber.lastEffect = completedWork 
  26.       } 
  27.     } 
  28.  
  29.     // 判断 completedWork.sibling 是否存在... 
  30.   } 

上面的代码就是构造 effectList 的过程,光看代码还是比较难理解的,我们还是通过实际的代码来解释一下。

  1. import React from 'react' 
  2.  
  3. export default class App extends React.Component { 
  4.   state = { val: 0 } 
  5.   click = () => { 
  6.     this.setState({ val: this.state.val + 1 }) 
  7.   } 
  8.   render() { 
  9.     const { val } = this.state 
  10.     const array = Array(2).fill() 
  11.     const rows = array.map( 
  12.       (_, row) => <tr key={row}> 
  13.         {array.map( 
  14.           (_, col) => <td key={col}>{val}</td> 
  15.         )} 
  16.       </tr> 
  17.     ) 
  18.     return <table onClick={() => this.click()}> 
  19.       {rows
  20.     </table
  21.   } 

 

 


 

App

 

我们构造一个 2 * 2 的 Table,每次点击组件,td 的 children 都会发生修改,下面看看这个过程中的 effectList 是如何变化的。

第一个 td 完成 completeWork 后,EffectList 结果如下:

1

 

第二个 td 完成 completeWork 后,EffectList 结果如下:

2

 

两个 td 结束了 completeWork 流程,会回溯到 tr 进行 completeWork ,tr 结束流程后 ,table 会直接复用 tr 的 firstEffect 和 lastEffect,EffectList 结果如下:

3

 

后面两个 td 结束 completeWork 流程后,EffectList 结果如下:

4

 

回溯到第二个 tr 进行 completeWork ,由于 table 已经存在 firstEffect 和 lastEffect,这里会直接修改 table 的 firstEffect 的 nextEffect,以及重新指定 lastEffect,EffectList 结果如下:

5

 

最后回溯到 App 组件时,就会直接复用 table 的 firstEffect 和 lastEffect,最后 的EffectList 结果如下:

6

 

提交更新这一阶段的主要作用就是遍历 effectList 里面的节点,将更新反应到真实 DOM 中,当然还涉及一些生命周期钩子的调用,我们这里只展示最简单的逻辑。

  1. function commitRoot(root) { 
  2.   var finishedWork = root.finishedWork 
  3.   var firstEffect = finishedWork 
  4.   var nextEffect = firstEffect 
  5.   // 遍历effectList 
  6.   while (nextEffect !== null) { 
  7.     const effectTag = nextEffect.effectTag 
  8.     // 根据 effectTag 进行不同的处理 
  9.     switch (effectTag) { 
  10.       // 插入 DOM 节点 
  11.       case Placement: { 
  12.         commitPlacement(nextEffect) 
  13.         nextEffect.effectTag &= ~Placement 
  14.         break 
  15.       } 
  16.       // 更新 DOM 节点 
  17.       case Update: { 
  18.         const current = nextEffect.alternate 
  19.         commitWork(current, nextEffect) 
  20.         break 
  21.       } 
  22.       // 删除 DOM 节点 
  23.       case Deletion: { 
  24.         commitDeletion(root, nextEffect) 
  25.         break 
  26.       } 
  27.     } 
  28.     nextEffect = nextEffect.nextEffect 
  29.   } 

这里不再展开讲解每个 effect 下具体的操作,在遍历完 effectList 之后,就是将当前的 fiber 树进行切换。

  1. function commitRoot() { 
  2.   var finishedWork = root.finishedWork 
  3.  
  4.   // 遍历 effectList …… 
  5.  
  6.   root.finishedWork = null 
  7.   root.current = finishedWork // 切换到新的 fiber 树 

总结到这里整个更新流程就结束了,可以看到 Fiber 架构下,所有数据结构都是链表形式,链表的遍历都是通过循环的方式来实现的,看代码的过程中经常会被突然出现的 return、break 扰乱思路,所以要完全理解这个流程还是很不容易的。

本文转载自微信公众号「更了不起的前端」,可以通过以下二维码关注。转载本文请联系更了不起的前端公众号。

 

责任编辑:武晓燕 来源: 更了不起的前端
相关推荐

2011-07-15 09:57:03

MongoDB缓存刷新

2021-12-08 06:53:28

Choreograph屏幕机制

2020-09-21 14:35:20

VuenextTick前端

2024-06-17 08:55:52

2010-09-06 08:43:13

.NET 4

2020-10-28 09:12:48

React架构Hooks

2016-10-21 09:29:53

嵌入式Linux更新机制

2020-09-30 09:15:24

React架构递归

2020-09-24 08:45:10

React架构源码

2018-08-10 04:40:56

2021-04-21 07:53:13

Android屏幕刷新

2010-03-10 11:55:30

Mocha BSM运维管理摩卡软件

2023-05-29 13:56:00

JSReact

2024-07-16 23:07:30

2018-09-03 15:45:48

Windows 10Windows驱动程序

2021-02-25 11:12:31

人工智能生物技术肺纤维化药物

2022-11-15 17:31:35

边缘计算架构人工智能

2024-10-31 08:35:53

2024-05-10 09:36:36

架构消息队列

2024-07-03 08:28:44

HWKafkaLEO
点赞
收藏

51CTO技术栈公众号