likes
comments
collection
share

一文带你领略react的调度更新机制

作者站长头像
站长
· 阅读数 41

简介

  1. 根据不同的组件更新方式产生新的reactElement对象,根据reactElement对象构建需要更新的fiber树
  2. 根据构建的fiber树配合渲染器(Renderer)创建视图实例跟渲染更新操作

根据两个工作流程的不同,前者被称为 render阶段,后者被称为commit阶段

类比我们在写代码,先进行代码编写(render),写完之后进行代码提交(commit)

调用入口

无论是初次渲染,还是调度更新,最终都会进入到scheduleUpdateOnFiber的入口逻辑,才正式开始更新/构建的流程 ,可以在ReactDom.render 上打个断点看看

function App() { 
  return <div onClick={plus}>this is app text </div>; 
} 
ReactDOM.render(<App />, document.getElementById("root")); 

一文带你领略react的调度更新机制

export function scheduleUpdateOnFiber( 
  fiber: Fiber, 
  lane: Lane, 
  eventTime: number, 
) { 
  // 省略其他的逻辑 
  const root = markUpdateLaneFromFiberToRoot(fiber, lane); 
 
  performSyncWorkOnRoot(root); 
 
} 

render阶段之前

在进入render阶段之前,也就是在调用workLoopSync之前,都会调用prepareFreshStack(root, lanes);刷新栈帧

function prepareFreshStack(root: FiberRoot, lanes: Lanes) { 
  root.finishedWork = null; 
  root.finishedLanes = NoLanes; 
  workInProgressRoot = root; 
  workInProgress = createWorkInProgress(root.current, null); 
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; 
  workInProgressRootExitStatus = RootIncomplete; 
  workInProgressRootFatalError = null; 
  workInProgressRootSkippedLanes = NoLanes; 
  workInProgressRootUpdatedLanes = NoLanes; 
  workInProgressRootPingedLanes = NoLanes; 
} 

更新workInProgressRootRenderLanes,subtreeRenderLanes,workInProgressRootIncludedLanes的车道(后面优化有用),对于ReactDOM.render使用的是syncLane的车道

调用createWorkInProgress(rootFiber,null)构建了新的HostRootFiber,拥有current rootFiber的所有属性(进行了浅克隆), 并将workInProgress指针指向该fiber

Render阶段概述

在初次渲染或者每次触发更新,都会进入render阶段 ,在这个过程,performUnitOfWork函数会通过 深度优先遍历 的方式,根据父fiber进行子fiber的派生,直到所有子孙fiber派生完毕,也就构建出整个棵fiber树了

流程概述

// 我们正在用的ReactDOM.render最终会调用workLoopSync 
function workLoopSync() { 
  // Already timed out, so perform work without checking if we need to yield. 
  while (workInProgress !== null) { 
    performUnitOfWork(workInProgress); 
  } 
} 
 
function performUnitOfWork(unitOfWork: Fiber): void { 
  // The current, flushed, state of this fiber is the alternate. Ideally 
  // nothing should rely on this, but relying on it here means that we don't 
  // need an additional field on the work in progress. 
  const current = unitOfWork.alternate; 
  // 对指针workInProgress指向的fiber 派生child fiber 
  let next = beginWork(current, unitOfWork, subtreeRenderLanes); 
 
  unitOfWork.memoizedProps = unitOfWork.pendingProps; 
   
  if (next === null) { 
    // If this doesn't spawn new work, complete the current work. 
    // 没有child fibr需要派生,则该fibr进行completeWork 
    completeUnitOfWork(unitOfWork); 
  } else { 
    // 将派生的child fiber赋值给workInProgress 指针 
    // 继续调用performUnitOfWork, 又进入beginWork派生child fiber 
    workInProgress = next; 
  } 
 
  ReactCurrentOwner.current = null; 
} 
  1. performUnitOfWork(workInProgress),调用beginWork(current,workInProgress)派生child fiber,在这个过程中可能会为fiber增加副作用,尝试更新workInProgress指针
  2. 当该fiber有child fiber派生,会将workInProgress指向该child fiber,重新进入步骤1
  3. 当该fiber没有child fiber派生,将fiber赋值给completedWork,调用completeWork函数,为该fiber创建/更新对应的dom节点/属性,可能会为fiber增加副作用
  4. 该fiber完成completeWork之后,会向上移交effectList,如果fiber本身有副作用,那么该fiber会加入effectList(副作用链表)
  5. 如果该fiber还有兄弟fiber ,那么将workInProgress指向兄弟fiber,重新进入步骤1:performUnitOfWork(workInProgress),
  6. 如果该fiber没有兄弟fiber,则fiber的父级fiber赋值给completedWork, 调用completeWork,进入completeWork逻辑,重复进行步骤3,直到根fiber,也就是HostRootFiber

副作用概念

在构建或者更新fiber过程中,可能会为fiber打上各种标记,也就是赋值fiber.flags属性,在后面的commit阶段中,要针对fiber.flags进行处理,如对于class fiber / function component fiber 是否需要调用生命周期方法|hook, 对host fiber,如div fiber,是否需要更新/删除/插入 等等。

在react里面,打标记这种行为被称为增加副作用,具备各种副作用的fiber,在构建fiber的过程中会被串成一条链表,这条链表被称为 effectList

深入理解 Fiber构建

示例代码

function Content() { 
  const [flag, setFlag] = useState(false); 
  return ( 
    <> 
      <p>1</p> 
      <p>2</p> 
      <button onClick={() => setFlag(true)}>show</button> 
      {flag ? <p>3</p> : null} 
    </> 
  ); 
} 
 
class App extends React.Component { 
  componentDidUpdate(...args) { 
    console.log("componentDidUpdate", ...args); 
  } 
 
  componentDidMount(...args) { 
    console.log("componentDidMount", ...args); 
  } 
 
  render() { 
    return ( 
      <div className="app"> 
        <header>this is header text</header> 
        <Content /> 
      </div> 
    ); 
  } 
} 
 
ReactDOM.render(<App />, document.getElementById("root")); 

mount阶段:

  1. 首次应用渲染执行ReactDOM.render创建了FiberRootNode和HostRootFiber节点【后续简称rootFiber】,fiberRootNode是整个react应用的根节点,rootFiber是fiber树的根节点,rootNode跟rootFiber通过current跟stateNode互相连接, 最终触发schedulerUpdateOnFiber,进入render阶段
  2. render阶段之前会调用prepareFreshStack(root, lanes)刷新栈帧, 创建rootFiber的alternate fiber节点,并将workInProgress指针指向rootFiber

一文带你领略react的调度更新机制

  1. render阶段会持续调用performUnitOfWork(workInProgress)派生child Fiber
  2. 当前workInProgress指针指向rootFiber,next=beginWork(hostRootFiber)调用updateHostRoot方法,根据ReactDOM.render传递的 reactElement(App), 调用reconcileChildren(current hostRootFiber, hostRootFiber, reactElement(App), syncLanes)派生 App Fiber,赋值给rootFiber.child并返回App Fiber,next为App Fiber,不为null,移动workInProgress指针指向App Fiber,由于current hostRootFiber 跟workInProgress hostRootFiber都存在,因此进入的是reconcileChildFibers的逻辑,shouldTrackSideEffects为true,最终在派生Fiber()的时候,会赋值 AppFiber.flags = Placement;
export const reconcileChildFibers = ChildReconciler(true); 
 export function reconcileChildren( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  nextChildren: any, 
  renderLanes: Lanes, 
) { 
  if (current === null) { 
    workInProgress.child = mountChildFibers( 
      workInProgress, 
      null, 
      nextChildren, 
      renderLanes, 
    ); 
  } else { 
    workInProgress.child = reconcileChildFibers( 
      workInProgress, 
      current.child, 
      nextChildren, 
      renderLanes, 
    ); 
  } 
} 
 
 function placeSingleChild(newFiber: Fiber): Fiber { 
    // This is simpler for the single child case. We only need to do a 
    // placement for inserting new children. 
    if (shouldTrackSideEffects && newFiber.alternate === null) { 
      newFiber.flags = Placement; 
    } 
    return newFiber; 
  } 

一文带你领略react的调度更新机制

  1. 当前workInProgress指针指向App Fiber,执行next=beginWork(App Fiber),由于是mount阶段执行updateClassComponent ,由于是mount阶段,调用constructClassInstance构造了App class instance, 调用instance.render()方法返回对应的reactElement(div),props结构为: {className: 'app', children: [reactElement(header), reactElement(Content)]}, 接着调用 reconcileChildren(null, App Fiber, reactElement(div), syncLanes), 然后派生出子FIber:div Fiber,赋值给App Fiber的child,并返回给next,next为div Fiber,不为null,则移动workInProgress指针指向它。

一文带你领略react的调度更新机制

  • 当前workInProgress指针指向div Fiber,next=beginWork(div Fiber),调用updateHostComponent,执行reconcileChildren(null, div Fiber, [reactElement(header), reactElement(Content)], syncLanes); 由于传递的children是数组,会调用reconcileChildrenArray,对传递的reactElement(header), reactElement(Content)依次调用createFiber构建fiber,兄弟fiber之间通过sibling链接,通过return指向父级fiber(div Fiber),并将构建好的第一个fiber:header Fiber, 赋值给workInProgress.child,并返回给next,next不为null,则移动workInProgress指针指向header Fiber

一文带你领略react的调度更新机制

当前workInProgress指针指向header Fiber,next=beginWork(header Fiber),调用了updateHostComponent, 进入isDirectTextChild优化逻辑,调用reconcileChildren(null, headerFiber, null, syncLanes); 赋值的workInProgress.child 为null,next为null

function updateHostComponent( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  renderLanes: Lanes, 
) { 
 
  const type = workInProgress.type; 
  const nextProps = workInProgress.pendingProps; 
  const prevProps = current !== null ? current.memoizedProps : null; 
 
  let nextChildren = nextProps.children; 
  const isDirectTextChild = shouldSetTextContent(type, nextProps); 
  // 优化逻辑,不生成child fiber,仅保存props.children属性 
  if (isDirectTextChild) { 
    // We special case a direct text child of a host node. This is a common 
    // case. We won't handle it as a reified child. We will instead handle 
    // this in the host environment that also has access to this prop. That 
    // avoids allocating another HostText fiber and traversing it. 
    nextChildren = null; 
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) { 
    // If we're switching from a direct text child to a normal child, or to 
    // empty, we need to schedule the text content to be reset. 
    workInProgress.flags |= ContentReset; 
  } 
 
  markRef(current, workInProgress); 
  reconcileChildren(current, workInProgress, nextChildren, renderLanes); 
  return workInProgress.child; 
} 
 
function shouldSetTextContent(type: string, props: Props): boolean { 
  return ( 
    type === 'textarea' || 
    type === 'option' || 
    type === 'noscript' || 
    typeof props.children === 'string' || 
    typeof props.children === 'number' || 
    (typeof props.dangerouslySetInnerHTML === 'object' && 
      props.dangerouslySetInnerHTML !== null && 
      props.dangerouslySetInnerHTML.__html != null) 
  ); 
} 

当前workInProgress指向的header Fiber,返回的next为null,调用了completeUnitOfWork(header Fiber)的逻辑

function completeUnitOfWork(unitOfWork: Fiber): void { 
  // Attempt to complete the current unit of work, then move to the next 
  // sibling. If there are no more siblings, return to the parent fiber. 
  let completedWork = unitOfWork; 
  do { 
 
    const current = completedWork.alternate; 
    const returnFiber = completedWork.return; 
 
    // Check if the work completed or if something threw. 
    if ((completedWork.flags & Incomplete) === NoFlags) { 
    // 根据fiber节点构建dom实例或者更新属性   
      let next = completeWork(current, completedWork, subtreeRenderLanes); 
      if (next !== null) { 
        // Completing this fiber spawned new work. Work on that next. 
        workInProgress = next; 
        return; 
      } 
 
      if ( 
        returnFiber !== null && 
        // Do not append effects to parents if a sibling failed to complete 
        (returnFiber.flags & Incomplete) === NoFlags 
      ) { 
        // Append all the effects of the subtree and this fiber onto the effect 
        // list of the parent. The completion order of the children affects the 
        // side-effect order. 
        // 每次completeUnitOfWork都将副作用链表上移给父级fiber 
        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 this fiber had side-effects, we append it AFTER the children's 
        // side-effects. We can perform certain side-effects earlier if needed, 
        // by doing multiple passes over the effect list. We don't want to 
        // schedule our own side-effect on our own list because if end up 
        // reusing children we'll schedule this effect onto itself since we're 
        // at the end. 
        const flags = completedWork.flags; 
 
        // Skip both NoWork and PerformedWork tags when creating the effect 
        // list. PerformedWork effect is read by React DevTools but shouldn't be 
        // committed. 
        // 如果当前的fiber有副作用需要处理,那么添加到当前effectList最后 
        if (flags > PerformedWork) { 
          if (returnFiber.lastEffect !== null) { 
            returnFiber.lastEffect.nextEffect = completedWork; 
          } else { 
            returnFiber.firstEffect = completedWork; 
          } 
          returnFiber.lastEffect = completedWork; 
        } 
      } 
    }  
   // 发现当前的fiber还有兄弟fiber 
   // 则移动workInProgress指针到该兄弟fiber,重新进入perforUnitOfWork继续派生子fiber 
    const siblingFiber = completedWork.sibling; 
    if (siblingFiber !== null) { 
      // If there is more work to do in this returnFiber, do that next. 
      workInProgress = siblingFiber; 
      return; 
    } 
    // 如果没有兄弟fiber需要处理,那么移动completedWork 指向 父级fiber,进入completeWork 
    // Otherwise, return to the parent 
    completedWork = returnFiber; 
    // Update the next thing we're working on in case something throws. 
    workInProgress = completedWork; 
  } while (completedWork !== null); 
 
  // We've reached the root. 
  if (workInProgressRootExitStatus === RootIncomplete) { 
    workInProgressRootExitStatus = RootCompleted; 
  } 
} 
  1. 此时completedWork指向header Fiber,调用next = completeWork(null, header Fiber, syncLanes);由于是mount阶段,则调用createInstance会创建dom,赋值给header fiber.stateNode,并返回null,赋值给next,此时判断header fiber.sibling 也就是Content Fiber存在,则将workInProgress指针指向Content Fiber,退出completeUnitOfWork流程,重新进入performUnitOfWork

一文带你领略react的调度更新机制

  1. 此时workInProgress指针指向Content Fiber, 调用next=beginWork(Content Fiber), 由于是mount阶段执行mountIndeterminateComponent ,里面又执行了renderWithHooks, 会执行Content函数,返回了[reactElement(p1),reactElement(p2)]数组,执行reconcileChildren(null, Content Fiber, [reactElement(p1), reactElement(p2)], syncLanes); 由于传递的children是数组,会调用reconcileChildrenArray,对传递的reactElement(p1), reactElement(p2)依次调用createFiber构建fiber,兄弟fiber之间通过sibling链接,通过return指向父级fiber(Content Fiber),并将构建好的第一个p1 Fiber, 赋值给workInProgress.child,并返回给next,next不为null,则移动workInProgress指针指向p1 Fiber

一文带你领略react的调度更新机制

  1. 此时workInProgress指针指向p1 Fiber, 调用next=beginWork(p1 Fiber), 进入isDirectTextChild优化逻辑,调用reconcileChildren(null, p1 fiber, null, SyncLanes); 赋值的workInProgress.child 为null,返回next为null,进入completeUnitOfWork(p1 Fiber)的逻辑
  2. 此时completedWork指向p1 Fiber,调用next = completeWork(null, p1 Fiber, SyncLanes);由于是mount阶段,则调用createInstance会创建dom,赋值给p1 fiber.stateNode,并返回null赋值给next,此时判断p1 fiber.sibling 也就是p2 Fiber存在,则将workInProgress指针指向p2 fiber,退出completeUnitOfWork流程,重新进入performUnitOfWork

一文带你领略react的调度更新机制

  1. 此时workInProgress指针指向p2 Fiber, 调用next=beginWork(p2 Fiber), 进入isDirectTextChild优化逻辑,调用reconcileChildren(null, p2 fiber, null, SyncLanes); 赋值的workInProgress.child 为null,返回next为null,进入completeUnitOfWork(p2 Fiber)的逻辑
  2. 此时completedWork指向p2 Fiber,调用next = completeWork(null, p2 Fiber, SyncLanes);由于是mount阶段,则调用createInstance会创建dom,赋值给p2 fiber.stateNode,并返回null赋值给next,此时判断p2 fiber.sibling为null,则将completedWork指针指向p2 fiber.return,也就是父级fiberContent Fiber

一文带你领略react的调度更新机制

  1. 此时completedWork指向Content Fiber,调用next=completeWork(Fiber()),属于IndeterminateComponent不做任何处理返回null, completedWork指向 Fiber().return也就是 Fiber(
    )
  2. 此时completedWork指向Fiber(
    ), 调用next=completeWork(Fiber(
    )),由于是mount阶段,则调用createInstance会创建dom,赋值给fiber(
    ).stateNode,同时将子孙fiber中的dom加入到该dom节点下,并返回null赋值给next, completedWork指向 Fiber(
    ).return也就是 Fiber()

一文带你领略react的调度更新机制

  1. 此时completedWork指向Fiber(), 调用next=completeWork(Fiber()),属于ClassComponent不做任何处理返回null, 发现有Fiber().flags 存在,则将Fiber(加入副作用链表, completedWork指向Fiber()
if (flags > PerformedWork) { 
  if (returnFiber.lastEffect !== null) { 
    returnFiber.lastEffect.nextEffect = completedWork; 
  } else { 
    returnFiber.firstEffect = completedWork; 
  } 
  returnFiber.lastEffect = completedWork; 
} 
  1. completedWork指向Fiber(), 不需要处理,退出completeUnitofWork,并标记workInProgressRootExitStatus = RootCompleted;
  2. 下一步进入commit阶段

Update阶段:

// 点击button之后,触发更新后的Content组件是这样子的 
function Content() { 
  const [flag, setFlag] = useState(false); 
  return ( 
    <> 
      <p>1</p> 
      <p>2</p> 
      <button onClick={() => setFlag(true)}>show</button> 
      {flag ? <p>3</p> : null} 
    </> 
  ); 
} 
 
class App extends React.Component { 
  componentDidUpdate(...args) { 
    console.log("componentDidUpdate", ...args); 
  } 
 
  componentDidMount(...args) { 
    console.log("componentDidMount", ...args); 
  } 
 
  render() { 
    return ( 
      <div className="app"> 
        <header>this is header text</header> 
        <Content /> 
      </div> 
    ); 
  } 
} 
 
ReactDOM.render(<App />, document.getElementById("root")); 
  1. 用户点击button ,触发react绑定的onClick方法,调用dispatchAction方法,将flag设置为true,触发scheduleUpdateOnFiber更新,在进入render阶段之前,会跟前面一样清空栈帧,然后会调用performSyncWorkOnRoot进入render阶段,主要关注这个函数,会从触发更新的fiber: Fiber() 标记lanes为syncLanes,然后向上去标记祖先fiber.childlanes为syncLanes,方便后续进行优化更新
function markUpdateLaneFromFiberToRoot( 
  sourceFiber: Fiber, 
  lane: Lane, 
): FiberRoot | null { 
  // Update the source fiber's lanes 
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); 
  let alternate = sourceFiber.alternate; 
  if (alternate !== null) { 
    alternate.lanes = mergeLanes(alternate.lanes, lane); 
  } 
  // Walk the parent path to the root and update the child expiration time. 
  let node = sourceFiber; 
  let parent = sourceFiber.return; 
  while (parent !== null) { 
    parent.childLanes = mergeLanes(parent.childLanes, lane); 
    alternate = parent.alternate; 
    if (alternate !== null) { 
      alternate.childLanes = mergeLanes(alternate.childLanes, lane); 
    } 
    node = parent; 
    parent = parent.return; 
  } 
  if (node.tag === HostRoot) { 
    const root: FiberRoot = node.stateNode; 
    return root; 
  } else { 
    return null; 
  } 
} 
  1. update阶段的递归流程与mount时类似,只是不会新建dom,对于已经存在的fiber,会去对比前后props是否相等,从而标记更新也就是fiber.flags = Update,对于需要插入的fiber(),则标记Placement

一文带你领略react的调度更新机制

beginWork 做了什么?

function beginWork( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  renderLanes: Lanes, 
): Fiber | null { 
  const updateLanes = workInProgress.lanes; 
  
 // current如果存在,证明就是update阶段,看是否能够直接复用fiber 
  if (current !== null) { 
    const oldProps = current.memoizedProps; 
    const newProps = workInProgress.pendingProps; 
 
    if ( 
      oldProps !== newProps || 
      hasLegacyContextChanged() 
    ) { 
     // props 或者context 内容发生了改变 
      didReceiveUpdate = true; 
      // 该fiber节点的更新优先级不包含在渲染优先级中,直接跳过 
    } else if (!includesSomeLane(renderLanes, updateLanes)) { 
      didReceiveUpdate = false; 
     // 当前fiber节点无需更新,调用bailoutOnAlreadyFinishedWork递归检测子节点是否需要更新 
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); 
    } else { 
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) { 
        // 例如调用了class 的this.forceUpdate() 
        didReceiveUpdate = true; 
      } else { 
        didReceiveUpdate = false; 
      } 
    } 
  } else { 
    didReceiveUpdate = false; 
  } 
 
 // 设置当前fiber优先级 
  workInProgress.lanes = NoLanes; 
 
  switch (workInProgress.tag) { 
  // 针对第一次mount时的function component 
    case IndeterminateComponent: { 
      return mountIndeterminateComponent( 
        current, 
        workInProgress, 
        workInProgress.type, 
        renderLanes, 
      ); 
    } 
    // update 阶段function component 
    case FunctionComponent: { 
      const Component = workInProgress.type; 
      const unresolvedProps = workInProgress.pendingProps; 
      const resolvedProps = 
        workInProgress.elementType === Component 
          ? unresolvedProps 
          : resolveDefaultProps(Component, unresolvedProps); 
      return updateFunctionComponent( 
        current, 
        workInProgress, 
        Component, 
        resolvedProps, 
        renderLanes, 
      ); 
    } 
    // update 阶段的 class component 
    case ClassComponent: { 
      const Component = workInProgress.type; 
      const unresolvedProps = workInProgress.pendingProps; 
      const resolvedProps = 
        workInProgress.elementType === Component 
          ? unresolvedProps 
          : resolveDefaultProps(Component, unresolvedProps); 
      return updateClassComponent( 
        current, 
        workInProgress, 
        Component, 
        resolvedProps, 
        renderLanes, 
      ); 
    } 
    // 根fiber节点 
    case HostRoot: 
      return updateHostRoot(current, workInProgress, renderLanes); 
    // 普通hostComponent类型节点: dom element类型 
    case HostComponent: 
      return updateHostComponent(current, workInProgress, renderLanes); 
    case HostText: 
    // 普通hosttext类型,dom text类型 
      return updateHostText(current, workInProgress); 
    // react.createPortal创建的节点 
    case HostPortal: 
      return updatePortalComponent(current, workInProgress, renderLanes); 
    // React.forwardRef 
    case ForwardRef: { 
      const type = workInProgress.type; 
      const unresolvedProps = workInProgress.pendingProps; 
      const resolvedProps = 
        workInProgress.elementType === type 
          ? unresolvedProps 
          : resolveDefaultProps(type, unresolvedProps); 
      return updateForwardRef( 
        current, 
        workInProgress, 
        type, 
        resolvedProps, 
        renderLanes, 
      ); 
    } 
    // <></>节点  React.Fragment 
    case Fragment: 
      return updateFragment(current, workInProgress, renderLanes); 
    case ContextProvider: 
      return updateContextProvider(current, workInProgress, renderLanes); 
    case ContextConsumer: 
      return updateContextConsumer(current, workInProgress, renderLanes); 
    // React.useMemo 
    case MemoComponent: { 
      const type = workInProgress.type; 
      const unresolvedProps = workInProgress.pendingProps; 
      // Resolve outer props first, then resolve inner props. 
      let resolvedProps = resolveDefaultProps(type, unresolvedProps); 
      resolvedProps = resolveDefaultProps(type.type, resolvedProps); 
      return updateMemoComponent( 
        current, 
        workInProgress, 
        type, 
        resolvedProps, 
        updateLanes, 
        renderLanes, 
      ); 
    } 
    case SimpleMemoComponent: { 
      return updateSimpleMemoComponent( 
        current, 
        workInProgress, 
        workInProgress.type, 
        workInProgress.pendingProps, 
        updateLanes, 
        renderLanes, 
      ); 
    } 
    // mount阶段的class component 
    case IncompleteClassComponent: { 
      const Component = workInProgress.type; 
      const unresolvedProps = workInProgress.pendingProps; 
      const resolvedProps = 
        workInProgress.elementType === Component 
          ? unresolvedProps 
          : resolveDefaultProps(Component, unresolvedProps); 
      return mountIncompleteClassComponent( 
        current, 
        workInProgress, 
        Component, 
        resolvedProps, 
        renderLanes, 
      ); 
    } 
  } 
   
function bailoutOnAlreadyFinishedWork( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  renderLanes: Lanes, 
): Fiber | null { 
  if (current !== null) { 
    // Reuse previous dependencies 
    workInProgress.dependencies = current.dependencies; 
  } 
 
  markSkippedUpdateLanes(workInProgress.lanes); 
 
  // Check if the children have any pending work. 
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { 
    // The children don't have any work either. We can skip them. 
    // TODO: Once we add back resuming, we should check if the children are 
    // a work-in-progress set. If so, we need to transfer their effects. 
    return null; 
  } else { 
    // This fiber doesn't have work, but its subtree does. Clone the child 
    // fibers and continue. 
    cloneChildFibers(current, workInProgress); 
    return workInProgress.child; 
  } 
} 
} 
 
export function cloneChildFibers( 
  current: Fiber | null, 
  workInProgress: Fiber, 
): void { 
  if (workInProgress.child === null) { 
    return; 
  } 
 
  let currentChild = workInProgress.child; 
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps); 
  workInProgress.child = newChild; 
 
  newChild.return = workInProgress; 
  while (currentChild.sibling !== null) { 
    currentChild = currentChild.sibling; 
    newChild = newChild.sibling = createWorkInProgress( 
      currentChild, 
      currentChild.pendingProps, 
    ); 
    newChild.return = workInProgress; 
  } 
  newChild.sibling = null; 
} 
  • update阶段,会通过一些优化路径来尽可能地复用原来地fiber,markUpdateLaneFromFiberToRoot 从触发更新的fiber向上标记childLanes,则其他没有标记
  • 在渲染优先级不够的情况会直接cloneFiber ,不会进入下面的流程分支
  • 正常update 跟mount 阶段 都会进入switch(workInProgress.tag)流程分支,进入各自的分支调用生成新的nextChildren,最终调用reconcileChildFibers来生成并返回子fiber
  • 在reconcileChildren中,通过current是否为null,判断是否处于mount/update, 在mount阶段会调用 mountChildFibers来新建fiber,在update阶段会调用reconcileChildFibers 通过diff算法尝试复用/新建fiber
export function reconcileChildren( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  nextChildren: any, 
  renderLanes: Lanes, 
) { 
  if (current === null) { 
    // If this is a fresh new component that hasn't been rendered yet, we 
    // won't update its child set by applying minimal side-effects. Instead, 
    // we will add them all to the child before it gets rendered. That means 
    // we can optimize this reconciliation pass by not tracking side-effects. 
    workInProgress.child = mountChildFibers( 
      workInProgress, 
      null, 
      nextChildren, 
      renderLanes, 
    ); 
  } else { 
    // If the current child is the same as the work in progress, it means that 
    // we haven't yet started any work on these children. Therefore, we use 
    // the clone algorithm to create a copy of all the current children. 
 
    // If we had any progressed work already, that is invalid at this point so 
    // let's throw it out. 
    workInProgress.child = reconcileChildFibers( 
      workInProgress, 
      current.child, 
      nextChildren, 
      renderLanes, 
    ); 
  } 
} 
  • 这里的reconcileChildFibers,mountChildFibers 都是 ChildReconciler 创建返回的函数,区别只是一开始传入的shouldTrackSideEffects不同而已
export const reconcileChildFibers = ChildReconciler(true); 
export const mountChildFibers = ChildReconciler(false); 
// 这里的true跟false,其实就是标志shouldTrackSideEffects  
  • 函数会根据一开始的闭包变量shouldTrackSideEffects是否为true,从而决定是否追踪副作用,对于Function组件来说,副作用就是useEffect useLayoutEffect这些hooks调用, 而对HostComponent 也就是dom节点来说,意味着节点的增删改。
  • 所以我们能够知道,在update阶段,reconcileChildFibers是会追踪副作用,然后更新 fiber.tags,以便在后续commit阶段,进行追踪调用。
  • 问题来了,视图渲染也好,hooks调用也好,都是通过副作用的effectList链表来追踪的,由于我们mount的判断条件就是current fiber不存在,那么mount的时候,就没有增加副作用吗? 没有副作用的话,我们怎么在commit阶段把我们生成好的dom挂载上去?
  • 虽然我们看到在mount阶段,传入workInProgress fiber的current fiber都是null, 才进入mountChildFibers, 但是其实第一个workInProgress,也就是当前rootFiber.alternate, 是rootFiber经过prepareFreshStack创建并赋值的, 所以rootFiber.alternate第一次作为workInProgress 的时候并不为null
function prepareFreshStack(root, lanes) { 
     // 如果有,重置并取消上一次没有完成的更新 
      root.finishedWork = null; 
      root.finishedLanes = NoLanes; 
      var timeoutHandle = root.timeoutHandle; 
 
      if (timeoutHandle !== noTimeout) { 
        // The root previous suspended and scheduled a timeout to commit a fallback 
        // state. Now that we have additional work, cancel the timeout. 
        root.timeoutHandle = noTimeout; // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above 
 
        cancelTimeout(timeoutHandle); 
      } 
 
      if (workInProgress !== null) { 
        var interruptedWork = workInProgress.return; 
 
        while (interruptedWork !== null) { 
          unwindInterruptedWork(interruptedWork); 
          interruptedWork = interruptedWork.return; 
        } 
      } 
 
      workInProgressRoot = root; 
      workInProgress = createWorkInProgress(root.current, null); 
      workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; 
      workInProgressRootExitStatus = RootIncomplete; 
      workInProgressRootFatalError = null; 
      workInProgressRootSkippedLanes = NoLanes; 
      workInProgressRootUpdatedLanes = NoLanes; 
      workInProgressRootPingedLanes = NoLanes; 
    } 
     
// performSyncWorkOnRoot会调用该方法 
function workLoopSync() { 
  while (workInProgress !== null) { 
    performUnitOfWork(workInProgress); 
  } 
} 
  • 因此在第一次会进入reconcileChildFibers流程,rootFiber的tag为HostRoot, 会进入updateHostRoot(current, workInProgress, renderLanes);
case HostRoot: 
      return updateHostRoot(current, workInProgress, renderLanes); 
  • updateHostRoot会处理当前fiber的处理传入的Update, 也就是updateQueue.pending属性,内容是一个Update 节点 ,payload: App 的reactElement对象
  processUpdateQueue(workInProgress, nextProps, null, renderLanes); 
  var nextState = workInProgress.memoizedState; // Caution: React DevTools currently depends on this property 
  var nextChildren = nextState.element; 

调用了reconcileChildren(current, workInProgress, nextChildren, renderLanes);

currentFiber也存在,会进入 workInProgress.child = reconcileChildFibers(workInProgress, null, nextChildren, renderLanes)(注意,这里会追踪副作用

function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) { 
        var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; 
 
        if (isUnkeyedTopLevelFragment) { 
          newChild = newChild.props.children; 
        }  
        var isObject = typeof newChild === 'object' && newChild !== null; 
 
        if (isObject) { 
          switch (newChild.$$typeof) { 
            case REACT_ELEMENT_TYPE: 
              return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes)); 
 
            case REACT_PORTAL_TYPE: 
              return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)); 
          } 
        } 
 
        if (typeof newChild === 'string' || typeof newChild === 'number') { 
          return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes)); 
        } 
 
        if (isArray$1(newChild)) { 
          return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes); 
        } 
         
        // 省略其他兼容处理 
  
        return deleteRemainingChildren(returnFiber, currentFirstChild); 
      } 
 
      return reconcileChildFibers; 
    } 

正常情况,nextChildren 会是单个App组件的reactElement对象,因此会进入REACT_ELEMENT_TYPE分支

 case REACT_ELEMENT_TYPE: 
              return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));


function placeSingleChild(newFiber) { 
    // This is simpler for the single child case. We only need to do a 
    // placement for inserting new children. 
    if (shouldTrackSideEffects && newFiber.alternate === null) { 
      newFiber.flags = Placement; 
    } 
    return newFiber; 
} 

根据 ReactElemen对象 新建对应的App fiber后, shouldTrackSideEffects为true,currentFiber存在,App fiber.current 不存在,满足条件, App Fiber 追加了Placement的副作用,

一文带你领略react的调度更新机制

这里很重要,在后面的completeWork中,负责收集effectList链表的时候, 判断这个fiber被增加了Placement的flags,所以该fiber会被追加到effectList

var flags = completedWork.flags;  
// Skip both NoWork and PerformedWork tags when creating the effect list.  
// PerformedWork effect is read by React DevTools but shouldn't be committed. 
 
if (flags > PerformedWork) { 
  if (returnFiber.lastEffect !== null) { 
    returnFiber.lastEffect.nextEffect = completedWork; 
  } else { 
    returnFiber.firstEffect = completedWork; 
  } 
 
  returnFiber.lastEffect = completedWork; 
 } 

completework做了什么

function completeWork( 
  current: Fiber | null, 
  workInProgress: Fiber, 
  renderLanes: Lanes, 
): Fiber | null { 
  const newProps = workInProgress.pendingProps; 
 
  switch (workInProgress.tag) { 
    case IndeterminateComponent: 
    case LazyComponent: 
    case SimpleMemoComponent: 
    case FunctionComponent: 
    case ForwardRef: 
    case Fragment: 
    case Mode: 
    case Profiler: 
    case ContextConsumer: 
    case MemoComponent: 
      return null; 
    case ClassComponent: { 
      const Component = workInProgress.type; 
      if (isLegacyContextProvider(Component)) { 
        popLegacyContext(workInProgress); 
      } 
      return null; 
    } 
    case HostRoot: { 
      popHostContainer(workInProgress); 
      popTopLevelLegacyContextObject(workInProgress); 
      resetMutableSourceWorkInProgressVersions(); 
      const fiberRoot = (workInProgress.stateNode: FiberRoot); 
      if (fiberRoot.pendingContext) { 
        fiberRoot.context = fiberRoot.pendingContext; 
        fiberRoot.pendingContext = null; 
      } 
      if (current === null || current.child === null) { 
         if (!fiberRoot.hydrate) { 
          // Schedule an effect to clear this container at the start of the next commit. 
          // This handles the case of React rendering into a container with previous children. 
          // It's also safe to do for updates too, because current.child would only be null 
          // if the previous render was null (so the the container would already be empty). 
          workInProgress.flags |= Snapshot; 
        } 
      } 
      updateHostContainer(workInProgress); 
      return null; 
    } 
    // 以web为例,如div span  等html 标签元素 
    case HostComponent: { 
      popHostContext(workInProgress); 
      const rootContainerInstance = getRootHostContainer(); 
      const type = workInProgress.type; 
      // update 阶段 
      if (current !== null && workInProgress.stateNode != null) { 
        updateHostComponent( 
          current, 
          workInProgress, 
          type, 
          newProps, 
          rootContainerInstance, 
        ); 
 
        if (current.ref !== workInProgress.ref) { 
          markRef(workInProgress); 
        } 
      } else { 
      // mount 阶段 
        if (!newProps) { 
          // This can happen when we abort work. 
          return null; 
        } 
 
        const currentHostContext = getHostContext(); 
 
        const instance = createInstance( 
            type, 
            newProps, 
            rootContainerInstance, 
            currentHostContext, 
            workInProgress, 
         ); 
 
         appendAllChildren(instance, workInProgress, false, false); 
 
         workInProgress.stateNode = instance; 
 
          if ( 
            finalizeInitialChildren( 
              instance, 
              type, 
              newProps, 
              rootContainerInstance, 
              currentHostContext, 
            ) 
          ) { 
            markUpdate(workInProgress); 
         } 
 
        if (workInProgress.ref !== null) { 
          // If there is a ref on a host node we need to schedule a callback 
          markRef(workInProgress); 
        } 
      } 
      return null; 
    } 
    // 省略后面的case 
  } 
} 

我们重点关注与视图渲染有关的case,主要是HostComponent,在web平台代表了我们平时使用div,p,span等html元素生成的fiber类型,

  • 与处理beginWork一样,也是通过current !== null && workInProgress.stateNode != null判断当前处于update/mount
  • 当处于mount阶段, 会调用createInstance为Fiber节点生成对应的DOM节点,调用appendAllChildren将子孙DOM节点插入刚生成的DOM节点中,并调用finalizeInitialChildren 根据newProps的属性,初始化当前dom属性,如果有需要更新,则markUpdate当前的fiber,workInProgress.flags |= Update;
  • 当处于update阶段,由于之前mount创建dom已经渲染到页面,此时并不需要创建dom,会调用 updateHostComponent,处理传入的props,将其属性经过转换后的updatePayload赋值给workInProgress.updateQueue,最终会在commit阶段被渲染在页面上。其中updatePayload为数组形式,他的偶数索引的值为变化的key,奇数索引的值为变化的value
import { 
  createInstance, 
  createTextInstance, 
  appendInitialChild, 
  finalizeInitialChildren, 
  prepareUpdate, 
  supportsMutation, 
  supportsPersistence, 
  cloneInstance, 
  cloneHiddenInstance, 
  cloneHiddenTextInstance, 
  createContainerChildSet, 
  appendChildToContainerChildSet, 
  finalizeContainerChildren, 
  getFundamentalComponentInstance, 
  mountFundamentalComponent, 
  cloneFundamentalInstance, 
  shouldUpdateFundamentalComponent, 
  preparePortalMount, 
  prepareScopeUpdate, 
} from './ReactFiberHostConfig'; 
  • 这里需要说明的是,无论是createInstance还是updateHostComponent,是根据不同的平台而不同,在web平台就是创建dom节点,在其他平台如RN,就是创建View节点之类的,这里的react代码是通用的,引入的创建与更新方法,在不同平台,会通过不同的映射打包不同的文件,例如在web平台,ReactFiberHostConfig 会引入ReactFiberHostConfig.dom.js, 在RN平台,会引入ReactFiberHostConfig.native.js, 在测试平台,会引入ReactFiberHostConfig.test.js
  • 我们会注意到, 在completeWork时,mount 阶段 生成的 dom,会调用appendAllChildren ,递归到更节点的时候,就构成一棵dom树(离屏),在commit 阶段会渲染到视图上,那么在commit阶段,我们是否需要重新遍历一遍新构建的fiber树,做dom的插入吗 ? 并不是的,还记得我们前面beginWork里面提到的追加副作用吗, 当完成每一次completeWork的时候,会去判断当前的fiber节点flags是否存在副作用, 如果有,会添加到effectList 链表中, effectList 是从rootFiber.firstEffect开始的链表,rootFiber.firstEffect -> App Fiber -> ....(假如后面的子fiber有副作用的话,通过next指针依次链接)
  • 这样在commit阶段,去遍历effectList执行副作用即可,例如我们的App组件,增加了Placement的副作用,并且此时构建的dom已经是一棵离屏的dom树,最后只需要处理这个副作用,就可以将整棵dom树加到根节点下 div#root

Commit阶段

调用逻辑

 function performSyncWorkOnRoot() { 
      // ... 
      exitStatus = renderRootSync(root, lanes); // render阶段 
      // ... 
      const finishedWork: Fiber = (root.current.alternate: any); 
      root.finishedWork = finishedWork; 
      root.finishedLanes = lanes; 
      commitRoot(root); // commit阶段 
 } 
 
 // 以最高优先级,调度commitRootImpl执行 
function commitRoot(root) { 
  const renderPriorityLevel = getCurrentPriorityLevel(); 
  runWithPriority( 
    ImmediateSchedulerPriority, 
    commitRootImpl.bind(null, root, renderPriorityLevel), 
  ); 
  return null; 
} 

在render阶段结束后,会将当前的workInProgress跟 renderLanes 树赋值给FiberRootNode的finishedWork 跟 finishedLanes,调用commitRoot(rootFiberNode)进入 Renderer(渲染器)介入的工作流程,也就是commit阶段,细分为三个阶段:

beforeMutation(渲染视图前,dom突变前)

mutation(渲染视图中,dom突变)

layout(渲染视图后,dom突变后)

beforeMutation

beforemutation代码删除了dev环境跟一些兼容代码,剩下的主要是这些

// 在beforeMutation之前,我们还有一些准备工作 
// 如清理之前存在的useEffect回调以及其他同步任务,因为这些任务可能触发新的渲染 
// 所以这里要一直遍历执行 直到没有任务 
  do { 
    flushPassiveEffects(); 
  } while (rootWithPendingPassiveEffects !== null); 
  // ... 
  const finishedWork = root.finishedWork; 
  const lanes = root.finishedLanes; 
  // ... 
  root.finishedWork = null; 
  root.finishedLanes = NoLanes; 
  // 对于之前提到的effectList,之前的处理逻辑只能递归到根节点rootFiber的子fiber节点 
  // 而rootFiber是没有加入判断的,如果workInProgress树的rootFiber有副作用 
  // 那么要把这个fiber加入到effectList链表中,所以下面多了这一步的判断 
  let firstEffect; 
  if (finishedWork.flags > PerformedWork) { 
    // A fiber's effect list consists only of its children, not itself. So if 
    // the root has an effect, we need to add it to the end of the list. The 
    // resulting list is the set that would belong to the root's parent, if it 
    // had one; that is, all the effects in the tree including the root. 
    if (finishedWork.lastEffect !== null) { 
      finishedWork.lastEffect.nextEffect = finishedWork; 
      firstEffect = finishedWork.firstEffect; 
    } else { 
      firstEffect = finishedWork; 
    } 
  } else { 
    // There is no effect on the root. 
    firstEffect = finishedWork.firstEffect; 
  } 
   
// 将之前的上下文保存,后面commit结束后恢复, 并将当前的上下文标记为commit阶段 
const prevExecutionContext = executionContext; 
executionContext |= CommitContext; 
const prevInteractions = pushInteractions(root); 
 
// 正式进入我们的commitBeforeMutation 流程 
do { 
    try { 
      commitBeforeMutationEffects(); 
    } catch (error) { 
      invariant(nextEffect !== null, 'Should be working on an effect.'); 
      captureCommitPhaseError(nextEffect, error); 
      nextEffect = nextEffect.nextEffect; 
    } 
} while (nextEffect !== null); 


function commitBeforeMutationEffects() { 
  while (nextEffect !== null) { 
    const current = nextEffect.alternate; 
    // ... 
    const flags = nextEffect.flags; 
    // 如果当前是class组件,并且有getSnapshotBeforeUpdate方法 
    // 则该class组件对应的fiber会有Snapshot的副作用 
    if ((flags & Snapshot) !== NoFlags) { 
      commitBeforeMutationEffectOnFiber(current, nextEffect); 
    } 
    // 如果当前fiber有 Passive 的副作用,证明存在useEffect的调用 
    // 以NormalSchedulerPriority的优先级调度 flushPassiveEffects的执行 
    // 这里的调度是在beforeMutation阶段产生的,但是实际上是异步的,会直到layout之后才执行 
    // 为了避免useEffect执行的副作用会阻塞浏览器的渲染 
    if ((flags & Passive) !== NoFlags) { 
      // If there are passive effects, schedule a callback to flush at 
      // the earliest opportunity. 
      if (!rootDoesHavePassiveEffects) { 
       // rootDoesHavePassiveEffects赋值为true之后,只会调度一次 
        rootDoesHavePassiveEffects = true; 
        scheduleCallback(NormalSchedulerPriority, () => { 
          flushPassiveEffects(); 
          return null; 
        }); 
      } 
    } 
    nextEffect = nextEffect.nextEffect; 
  } 
} 

接下来我们去繁从简,简述commitBeforeMutationEffects做的内容

function commitBeforeMutationLifeCycles( 
  current: Fiber | null, 
  finishedWork: Fiber, 
): void { 
  switch (finishedWork.tag) { 
    case FunctionComponent: 
    case ForwardRef: 
    case SimpleMemoComponent: 
    case Block: { 
      return; 
    } 
    case ClassComponent: { 
      if (finishedWork.flags & Snapshot) { 
        if (current !== null) { 
          const prevProps = current.memoizedProps; 
          const prevState = current.memoizedState; 
          const instance = finishedWork.stateNode; // 对应class的实例 
          // 调用了class实例上面的getSnapshotBeforeUpdate方法 
          const snapshot = instance.getSnapshotBeforeUpdate( 
            finishedWork.elementType === finishedWork.type 
              ? prevProps 
              : resolveDefaultProps(finishedWork.type, prevProps), 
            prevState, 
          ); 
          instance.__reactInternalSnapshotBeforeUpdate = snapshot; 
        } 
      } 
      return; 
    } 
    case HostRoot: { 
      if (supportsMutation) { 
        if (finishedWork.flags & Snapshot) { 
          const root = finishedWork.stateNode; 
          clearContainer(root.containerInfo); 
        } 
      } 
      return; 
    } 
    case HostComponent: 
    case HostText: 
    case HostPortal: 
    case IncompleteClassComponent: 
      // Nothing to do for these component types 
      return; 
  } 
} 

commitBeforeMutationEffectOnFiber是 commitBeforeMutationLifeCycles 方法的别名, 里面调用了getSnapshotBeforeUpdate, 用于代替React16之前的 componentWillXXX 生命周期方法。

从React16开始,钩子前增加了UNSAFE_前缀。究其原因,是因为Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。这种行为和Reactv15不一致,所以标记为UNSAFE_,但在目前的Legacy模式是不会发生fiber构建中断恢复的情况,因此之前的声明周期方法还是安全的,但是后面的Concurrent模式则是不安全的。

这里是关于新版本react 的生命周期方法:

projects.wojtekmaj.pl/react-lifec…

if ((flags & Passive) !== NoFlags) { 
  // If there are passive effects, schedule a callback to flush at 
  // the earliest opportunity. 
  if (!rootDoesHavePassiveEffects) { 
   // rootDoesHavePassiveEffects赋值为true之后,只会调度一次 
    rootDoesHavePassiveEffects = true; 
    scheduleCallback(NormalSchedulerPriority, () => { 
      flushPassiveEffects(); 
      return null; 
    }); 
  } 
} 

rootDoesHavePassiveEffects的赋值为true,能让我们在后面的处理阶段中,知道我们已经调度了flushPassiveEffects的执行,在layout之后,effectList要赋值给rootWithPendingPassiveEffects,以便于在 flushPassiveEffects执行中,根据Passive副作用遍历effectList,调用useEffect

在effectList上,存在带有各种副作用的fiber,从rootFiber.firstEffect开始,通过nextEffect串连在一起。

如:

对于fiber.tag 为HostComponent,如存在增删改dom,则会有Placement, Update, Deletion 副作用

对于fiber.tag为FunctionComponent, 如有useEffect,会有Passive副作用

对于fiber.tag为ClassComponent, 如有getSnapshotBeforeUpdate,会有Snapshot副作用

使用 scheduler调度器模块,以NormalSchedulerPriority的优先级调度了会执行useEffect相关逻辑的flushPassiveEffects。

注意这里是调度,是flushPassiveEffects是被异步执行的,目的为了避免副作用阻塞渲染,我们什么时候需要异步处理跟同步处理参考下面链接

zh-hans.reactjs.org/docs/hooks-…

useEffect 从被调度到最终执行经历了三个流程

  1. scheduler调度了flushPassiveEffect
  2. layout阶段之后将effectList赋值给rootWithPendingPassiveEffects
  3. scheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects

总结,beforeMutation阶段: 处理class组件的getSnapshotBeforeUpdate生命周期方法,以NormalSchedulerPriority优先级调度了flushPassiveEffects函数

mutation

跟beforemutation阶段一样,都是遍历effectList, 这里执行的是 commitMutationEffects

nextEffect = firstEffect; 
do { 
    try { 
      commitMutationEffects(root, renderPriorityLevel); 
    } catch (error) { 
      invariant(nextEffect !== null, 'Should be working on an effect.'); 
      captureCommitPhaseError(nextEffect, error); 
      nextEffect = nextEffect.nextEffect; 
    } 
} while (nextEffect !== null); 
// commitMutationEffects 源码 
function commitMutationEffects( 
  root: FiberRoot, 
  renderPriorityLevel: ReactPriorityLevel, 
) { 
  while (nextEffect !== null) { 
    const flags = nextEffect.flags; 
     
    // 重置文本 
    if (flags & ContentReset) { 
      commitResetTextContent(nextEffect); 
    } 
 
    // 如果有使用ref,重置ref的值 为null 
    if (flags & Ref) { 
      const current = nextEffect.alternate; 
      if (current !== null) { 
        commitDetachRef(current); 
      } 
    } 
 
    // The following switch statement is only concerned about placement, 
    // updates, and deletions. To avoid needing to add a case for every possible 
    // bitmap value, we remove the secondary effects from the effect tag and 
    // switch on that value. 
    const primaryFlags = flags & (Placement | Update | Deletion | Hydrating); 
    switch (primaryFlags) { 
      case Placement: { 
      // 插入dom节点 
        commitPlacement(nextEffect); 
        nextEffect.flags &= ~Placement; 
        break; 
      } 
      case PlacementAndUpdate: { 
        // Placement 先插入dom节点 
        commitPlacement(nextEffect); 
        // Clear the "placement" from effect tag so that we know that this is 
        // inserted, before any life-cycles like componentDidMount gets called. 
        nextEffect.flags &= ~Placement; 
 
        // Update 再更新dom节点的属性 
        const current = nextEffect.alternate; 
        commitWork(current, nextEffect); 
        break; 
      } 
      // Hydrat 开头 都是 SSR相关的 
      case Hydrating: { 
        nextEffect.flags &= ~Hydrating; 
        break; 
      } 
      // 顾名思义,就是Hydrate 然后还有触发属性更新 
      case HydratingAndUpdate: { 
        nextEffect.flags &= ~Hydrating; 
 
        // Update 
        const current = nextEffect.alternate; 
        commitWork(current, nextEffect); 
        break; 
      } 
      // 更新dom节点 
      case Update: { 
        const current = nextEffect.alternate; 
        commitWork(current, nextEffect); 
        break; 
      } 
      // 删除dom节点 
      case Deletion: { 
        commitDeletion(root, nextEffect, renderPriorityLevel); 
        break; 
      } 
    } 
    nextEffect = nextEffect.nextEffect; 
  } 
} 

commitMutationEffects按照顺序,分别执行了 重置文本节点,重置ref引用为null,执行插入/更新/删除 dom节点的操作

Placement Effect

当有placement的effect时候,会去调用commitPlacement

function commitPlacement(finishedWork: Fiber): void { 
  if (!supportsMutation) { 
    return; 
  } 
 
  // Recursively insert all host nodes into the parent. 
 // 向上递归寻找tag为HostComponent|HostRoot|HostPortal 的父级fiber节点 
 // 只有这些节点才会存在dom相关的信息 
  const parentFiber = getHostParentFiber(finishedWork); 
 
  // Note: these two variables *must* always be updated together. 
  let parent; 
  let isContainer; 
  const parentStateNode = parentFiber.stateNode; 
  switch (parentFiber.tag) { 
    case HostComponent: 
      parent = parentStateNode; 
      isContainer = false; 
      break; 
    case HostRoot: 
      parent = parentStateNode.containerInfo; 
      isContainer = true; 
      break; 
    case HostPortal: 
      parent = parentStateNode.containerInfo; 
      isContainer = true; 
      break; 
    default: 
      invariant( 
        false, 
        'Invalid host parent fiber. This error is likely caused by a bug ' + 
          'in React. Please file an issue.', 
      ); 
  } 
  if (parentFiber.flags & ContentReset) { 
    // Reset the text content of the parent before doing any insertions 
    resetTextContent(parent); 
    // Clear ContentReset from the effect tag 
    parentFiber.flags &= ~ContentReset; 
  } 
 
  const before = getHostSibling(finishedWork); 
  // We only have the top Fiber that was inserted but we need to recurse down its 
  // children to find all the terminal nodes. 
  if (isContainer) { 
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); 
  } else { 
    insertOrAppendPlacementNode(finishedWork, before, parent); 
  } 
} 

这个过程通过 getHostParentFiber 向上递归寻找tag为Host相关的父级fiber节点,如 HostComponent(普通html标签创建的),HostRoot(React.render创建的),HostPortal(React.createPortal创建),他们都有对应的dom节点。

通过getHostSibling获取相邻下一个的host节点, 根据before节点是否存在,最终会调用parentNode.appendChild(newNode) 或者 parentNode.insertBefore(newNode, referenceNode)

Update Effect

有update effect 副作用的fiber,会调用 commitWork

function commitWork(current: Fiber | null, finishedWork: Fiber): void { 
  switch (finishedWork.tag) { 
    case FunctionComponent: 
    case ForwardRef: 
    case MemoComponent: 
    case SimpleMemoComponent: 
    case Block: { 
      // Layout effects are destroyed during the mutation phase so that all 
      // destroy functions for all fibers are called before any create functions. 
      // This prevents sibling component effects from interfering with each other, 
      // e.g. a destroy function in one component should never override a ref set 
      // by a create function in another component during the same commit. 
 
      commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); 
      
      return; 
    } 
    case ClassComponent: { 
      return; 
    } 
    case HostComponent: { 
      const instance: Instance = finishedWork.stateNode; 
      if (instance != null) { 
        // Commit the work prepared earlier. 
        const newProps = finishedWork.memoizedProps; 
        // For hydration we reuse the update path but we treat the oldProps 
        // as the newProps. The updatePayload will contain the real change in 
        // this case. 
        const oldProps = current !== null ? current.memoizedProps : newProps; 
        const type = finishedWork.type; 
        // TODO: Type the updateQueue to be specific to host components. 
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any); 
        finishedWork.updateQueue = null; 
        if (updatePayload !== null) { 
          commitUpdate( 
            instance, 
            updatePayload, 
            type, 
            oldProps, 
            newProps, 
            finishedWork, 
          ); 
        } 
      } 
      return; 
    } 
    case HostText: { 
      const textInstance: TextInstance = finishedWork.stateNode; 
      const newText: string = finishedWork.memoizedProps; 
      // For hydration we reuse the update path but we treat the oldProps 
      // as the newProps. The updatePayload will contain the real change in 
      // this case. 
      const oldText: string = 
        current !== null ? current.memoizedProps : newText; 
      commitTextUpdate(textInstance, oldText, newText); 
      return; 
    } 
    case HostRoot: { 
      if (supportsHydration) { 
        const root: FiberRoot = finishedWork.stateNode; 
        if (root.hydrate) { 
          // We've just hydrated. No need to hydrate again. 
          root.hydrate = false; 
          commitHydratedContainer(root.containerInfo); 
        } 
      } 
      return; 
    } 
  } 
} 

我们主要对 Function fiber 跟HostComponent fiber 来分析:

对于Function fiber,会调用commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);HookLayout表示使用了useLayoutEffect,HookHasEffect 其实是HasEffect,表示这个hooks是否应该触发。

以下是关于ReactHooksEffectTags的类型

export const NoFlags = /*  */ 0b000; 
 
// Represents whether effect should fire. 
export const HasEffect = /* */ 0b001; 
 
// Represents the phase in which the effect (not the clean-up) fires. 
export const Layout = /*    */ 0b010; // useLayoutEffect 
export const Passive = /*   */ 0b100; // useEffect 
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { 
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); 
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; 
  if (lastEffect !== null) { 
    const firstEffect = lastEffect.next; 
    let effect = firstEffect; 
    do { 
      if ((effect.tag & tag) === tag) { 
        // Unmount 
        const destroy = effect.destroy; 
        effect.destroy = undefined; 
        if (destroy !== undefined) { 
          destroy(); 
        } 
      } 
      effect = effect.next; 
    } while (effect !== firstEffect); 
  } 
} 

遍历effectList,找出执行了useLayoutEffect 函数的销毁函数,并执行它

useLayoutEffect 分为创建函数 跟 销毁函数

useLayoutEffect(() => { 
// 创建函数 
return () => { 
// 销毁函数 
} 
},[]) 

对于HostComponent Fiber

主要是拿到该fiber上stateNode(dom节点), 新的属性memoizedProps与 更新队列updateQueue(updateQueue我们在completeWork的时候就已经处理过了,会赋值一个数组,第i项是更新的key,第i+1项是更新的value)

然后调用commitUpdate(instance,updatePayload,type,oldProps,newProps,finishedWork)

export function commitUpdate( 
  domElement: Instance, 
  updatePayload: Array<mixed>, 
  type: string, 
  oldProps: Props, 
  newProps: Props, 
  internalInstanceHandle: Object, 
): void { 
   // ... 
  // Apply the diff to the DOM node. 
  updateProperties(domElement, updatePayload, type, oldProps, newProps); 
} 
 
// Apply the diff. 
export function updateProperties( 
  domElement: Element, 
  updatePayload: Array<any>, 
  tag: string, 
  lastRawProps: Object, 
  nextRawProps: Object, 
): void { 
  // ... 
  const wasCustomComponentTag = isCustomComponent(tag, lastRawProps); 
  const isCustomComponentTag = isCustomComponent(tag, nextRawProps); 
  // Apply the diff. 
  updateDOMProperties( 
    domElement, 
    updatePayload, 
    wasCustomComponentTag, 
    isCustomComponentTag, 
  ); 
 // ... 
} 
 
function updateDOMProperties( 
  domElement: Element, 
  updatePayload: Array<any>, 
  wasCustomComponentTag: boolean, 
  isCustomComponentTag: boolean, 
): void { 
  // TODO: Handle wasCustomComponentTag 
  for (let i = 0; i < updatePayload.length; i += 2) { 
    const propKey = updatePayload[i]; 
    const propValue = updatePayload[i + 1]; 
    // 更新style 
    if (propKey === STYLE) { 
      setValueForStyles(domElement, propValue); 
    // 更新DANGEROUSLY_SET_INNER_HTML 
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { 
      setInnerHTML(domElement, propValue); 
    // 设置textContent <span>1111</span> 类似这种fiber ,tag 为HostComponent, children为 1111, 不会单独为1111创建fiber 
    } else if (propKey === CHILDREN) {  
      setTextContent(domElement, propValue); 
    } else { 
    // 其他属性调用: node.setAttribute(attributeName, attributeValue);  
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag); 
    } 
  } 
} 

Deletion effect

function commitDeletion( 
  finishedRoot: FiberRoot, 
  current: Fiber, 
  renderPriorityLevel: ReactPriorityLevel, 
): void { 
  // Recursively delete all host nodes from the parent. 
  // Detach refs and call componentWillUnmount() on the whole subtree. 
  unmountHostComponents(finishedRoot, current, renderPriorityLevel); 
  const alternate = current.alternate; 
  detachFiberMutation(current); 
  if (alternate !== null) { 
    detachFiberMutation(alternate); 
  } 
} 

对于有Deletion Effect的 Fiber节点,

主要是 调用 unmountHostComponents 执行副作用,并且调用detachFiberMutation 重置fiber节点属性引用,释放内存。

unmountHostComponents 主要做了两件事:

  1. 对于HostComponent或者HostText类型的Fiber节点,找到离该 Fiber最近的父级 Host类型 Fiber(因为他们在dom层次的关系是 父子关系),调用parentNode.removeChild(node),将fiber.stateNode, 也就是dom节点从视图移除。
  2. 调用commitNestedUnmounts方法, 对当前fiber以及其子孙fiber递归执行commitUnmount

这些调用了commitUnmountfiber的fiber,针对不同tag类型进行处理

  • 对于Function component,会将useEffect 描述节点 加入 pendingPassiveHookEffectsUnmount,之后调度flushPassiveEffects执行useEffect的销毁函数
  • 对于ClassComponent,调用实例的componentWillUnmount 函数, 并且重置ref引用为null
  • 对于HostComponent,重置ref引用为null

layout

该阶段在mutation节点之后,此时dom已经都渲染好了,可以开始执行一些不会阻塞浏览器的操作了,如 对于Function component调度useEffect函数,执行useLayoutEffect函数,对于 classCompnent则会执行componentDidMount/componentDidUpdate

commitMutationEffects(root, renderPriorityLevel); 
    // The work-in-progress tree is now the current tree. This must come after 
    // the mutation phase, so that the previous tree is still current during 
    // componentWillUnmount, but before the layout phase, so that the finished 
    // work is current during componentDidMount/Update. 
    root.current = finishedWork; 
    // The next phase is the layout phase, where we call effects that read 
    // the host tree after it's been mutated. The idiomatic use case for this is 
    // layout, but class component lifecycles also fire here for legacy reasons. 
    nextEffect = firstEffect; 
 
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { 
 
  while (nextEffect !== null) { 
  
    const flags = nextEffect.flags; 
 
    if (flags & (Update | Callback)) { 
      const current = nextEffect.alternate; 
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); 
    } 
 
 
    if (flags & Ref) { 
        commitAttachRef(nextEffect); 
    } 
     
    nextEffect = nextEffect.nextEffect; 
  } 
} 
  1. root.current = finishedWork; 前面提到的react fiber双缓存机制在这里体现出来了,由于此时workInProgress fiber 树已经渲染到视图了,那么此时要更改FiberRootNode的current指向,workinProgress Fiber树 变成了current Fiber树
  2. commitLayoutEffectOnFiber 其实是 commitLifeCycles的别名,针对不同tag类型,调用不同的处理方法,针对FunctionComponent(包含FunctionComponent,ForwardRef,SimpleMemoComponent)等类型,会调用commitHookEffectListMount 执行 useLayoutEffect函数
function commitHookEffectListMount(tag: number, finishedWork: Fiber) { 
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); 
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; 
  if (lastEffect !== null) { 
    const firstEffect = lastEffect.next; 
    let effect = firstEffect; 
    do { 
      if ((effect.tag & tag) === tag) { 
        // Mount 
        const create = effect.create; 
        effect.destroy = create(); 
      } 
      effect = effect.next; 
    } while (effect !== firstEffect); 
  } 
} 

调用 schedulePassiveEffects , 将useEffect的执行函数,跟销毁函数入栈,调度 flushPassiveEffects 的执行,最终会批量执行useEffect

function schedulePassiveEffects(finishedWork: Fiber) { 
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); 
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; 
  if (lastEffect !== null) { 
    const firstEffect = lastEffect.next; 
    let effect = firstEffect; 
    do { 
      const {next, tag} = effect; 
      if ( 
        (tag & HookPassive) !== NoHookEffect && 
        (tag & HookHasEffect) !== NoHookEffect 
      ) { 
        enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); 
        enqueuePendingPassiveHookEffectMount(finishedWork, effect); 
      } 
      effect = next; 
    } while (effect !== firstEffect); 
  } 
} 

对于class Component,会根据current fiber是否存在,调用实例的componentDidMount/componentDidUpdate(prevProps,prevState), 调用commitAttachRef更新拥有ref引用

转载自:https://juejin.cn/post/7206958960770613304
评论
请登录