react源码学习-架构篇-commit阶段

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码学习-架构篇-commit阶段相关的知识,希望对你有一定的参考价值。

commit阶段

在rootFiber.firstEffect上保存了一条需要执行副作用Fiber节点的单向链表effectList,这些Fiber节点的updateQueue中保存了变化的props。

这些副作用对应的dom操作在commit阶段执行,除此之外,一些生命周期钩子如componentDidXXX,useEffect需要在commit阶段执行。

commie阶段开始于commitRoot函数

commit阶段的工作主要分为三个部分:

before mutation阶段(执行DOM操作前)
mutation阶段(执行DOM操作)
layout阶段(执行DOM操作后)

在beform mutation阶段之前和layout阶段之后,有一些额外操作,如useEffect的触发,优先级相关的重置, ref的绑定和解绑。

before mutation之前主要做一些变量赋值,状态重置的工作,以及调度useEffect。

layout阶段之后主要做三个事情:

// layout阶段之后做的事情
  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
  
   // useEffect相关
  if (rootDoesHavePassiveEffects) 
    // This commit has passive effects. Stash a reference to them. But don't
    // schedule a callback until after flushing layout work.
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsLanes = lanes;
   else 
    // There were no passive effects, so we can immediately release the cache
    // pool for this render.
    releaseRootPooledCache(root, remainingLanes);
  

  // 性能优化相关
  if (remainingLanes === NoLanes) 
    // If there's no remaining work, we can clear the set of already failed
    // error boundaries.
    legacyErrorBoundariesThatAlreadyFailed = null;
  


  // 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
  ensureRootIsScheduled(root, now());

  // ...处理未捕获错误及老版本遗留的边界问题


  // 执行同步任务,这样同步任务不需要等到下次事件循环再执行
   // 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
   // 或useLayoutEffect
  flushSyncCallbacks();

1 useEffect相关的处理(给全局变量rootWithPendingPassiveEffects赋值)

2 性能追踪相关

3 在commit阶段会触发一些生命周期钩子,这些钩子中可能触发新的更新,会开启新的render-commit阶段。

beform mutation阶段(执行dom操作前)

整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。

主要看commitBeforeMutationEffects做了什么事情。调用实例的getSnapShotBeforeUpdate。

除了一些focus和blur相关的,调用commitBeforeMutationEffects_begin。

commitBeforeMutationEffects_begin会调用commitBeforeMutationEffects_complete,而commitBeforeMutationEffects_complete会调用commitBeforeMutationEffectsOnFiber,在commitBeforeMutationEffectsOnFiber里面会调用实例的getSnapShotBeforeUpdate函数。

所以before-mutation的主要工作就是:

1遍历effectList, 处理DOM节点渲染/删除后的 autoFocusblur 逻辑。

2 调用getSnapshotBeforeUpdate生命周期钩子。

调用getSnapshotBeforeUpdate生命周期钩子。

从React16开始,componentWillXX加上了UNSAFE_前缀,因为Reconciler重构为Fiber Reconciler后,render的任务可能因为某些特殊原因(有优先级更高任务)中断或者是重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXX)可能触发多次,

(当优先级低的任务被跳过之后,比如u1,执行到一半的时候u2优先级更高了,所以中断u1的render,开始u2的render-commit阶段,此时会执行u2的render阶段的一些生命周期钩子,比如不安全的co mponentWillUnMount,u1因为任务优先级更低,所以在等u2commit阶段之后,会开启新的调度,出于依赖性问题,因为跳过了u1,所以u1后面的fiber即u2,也会保存起来,在下一次更新的时候继续调度,所以第二次更新,u2还是会执行render阶段,就导致了componentWillMount可能会执行两次,)。

React提供了getSnapShotBeforeUpdate,他是在commit阶段内的before mutation阶段调用,由于commit阶段是同步的,所以不会遇到多次调用的情况。

调度useEffect

ScheduleCallback由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。

在此处,被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects,这个回调函数会在调度后执行,相当于在这里注册了这个回调函数。

为何需要异步调度而不是同步调度。

effectList会在某个阶段存放在全局变量rootWithPendingPassiveEffects中,并且当一个functionComponent含有useEffect或者useLayoutEffect的时候,他对应的fiber节点也会赋值effectTag.

flushPassiveEffects方法内部会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。

这里的rootWithPendingPassiveEffects还没存放effectLists呢,如果在这里不注册,而是直接执行flushPassiveEffects(),

那么rootWithPendingPassiveEffects === null。那么rootWithPendingPassiveEffects何时被赋值呢?在layout阶段之后有这段代码

 // useEffect相关
  if (rootDoesHavePassiveEffects) 
    // This commit has passive effects. Stash a reference to them. But don't
    // schedule a callback until after flushing layout work.
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsLanes = lanes;
   

rootWithPendingPassiveEffects = root;就是在这里,给全局变量赋值的。所以useEffect异步调用分为三步:

1 before mutation阶段之前scheduleCallback中调度flushPassiveEffects 注册回调函数

2 layout阶段之后将effectList赋值给rootWithPendingPassiveEffects. 赋值全局变量,回调函数执行的时候才有值

3 scheduleCallback触发flushPassiveEffectsflushPassiveEffects内部遍历`rootWithPendingPassiveEffects. 执行回调函数, 做对应的事情。

与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

异步执行的原因主要是防止同步执行时阻塞浏览器渲染。

总结: before-mutation阶段之前,会调度useEffect,before-mutation阶段,会遍历effectList,依此执行处理dom节点渲染/删除后的foucs, blue等逻辑,调用getSnapshotBeforeUpdate生命周期钩子,layout阶段之后,会做一系列事情,包括给全局变量赋值。useEffect异步调度的逻辑就是在before-mutation阶段之前调度flushPassiveEffects,然乎lyaoyt阶段之后赋值rootWithPendingPassiveEffects ,最后等待scheduleCallback触发回调函数,执行flushPassiveEffects。

执行dom的mutation阶段

类似before mutation阶段mutation阶段也是遍历effectList,执行函数。这里执行的是commitMutationEffects

他与before mutation阶段是在同一个函数.

主要调用了commitMutationEffects_begin,里面处理了需要删除的fiber,从fiber.deletetions中获取,然后再调用commitMutatinEffects_complete()方法,

重点就是这个commitMutationEffectsOnFiber方法

commitMutationeffects会遍历effectList,并且继续调用一些函数做一些事情:

1 除了删除的fiber节点。

2 根据ContentReset effectTag重置文子节点

3 更新ref

4 根据fiber.effectTag分别处理 Placement | Update 等等

Placement effect

Fiber节点含有Placement effectTag,意味着该Fiber节点对应的DOM节点需要插入到页面中。

获取dom兄弟节点是为了根据dom的兄弟节点事哦赋存在决定调用insertBefore还是appendChild执行dom插入操作。

Update effect

Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。

如果是functionComponent,会遍历effectList然后调用useLayoutEffect的销毁函数。

对于HostComponent,会调用commitUpdate

updatePayload就是render阶段的时候,completeWork在更新的时候,只处理了fiber的props,生成updatePayload赋值在fiber.updateQueue上,

deletioon Effect

调用commitDeletion,该方法会执行如下操作:

1 递归调用fiber节点及子孙节点中finer.tag为classComponent的componentWillUnmount钩子,从页面删除fiber节点对应的do m。

2 解绑ref

3 调度useLayoutEffect的销毁函数。

总结:mutation阶段为创建dom的阶段,遍历effectList,然后处理ref,根据effectTag做不同的处理,如新增,就插入dom,update,对于不同的fiber.tag,比如函数组件,执行useLayoutEffect的销毁函数,原生组件,更新props等等。对于deletion effectTag,就调用commitDeletion,执行对应的生命周期钩子,解绑ref,移除dom,调度useEffect, useLayoutEffect的销毁函数,调用componentWillUnMount。

layout阶段

该阶段的代码是在dom渲染完成后运行的。该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段可以参与DOM layout的阶段。

与前面两个阶段类似,layout阶段,也是遍历effectList.

commitLayoutEffects做了两件事情:

遍历effectList,调用commmitLayoutEffectOnFiber,通过不同的tag,调用对应的生命周期钩子如componentDidMount,componentDidUpdate,和hooks的一些调度(调度useEffect和useLayoutEffect)。

然后赋值ref。(commiAttachRef)

commmitLayoutEffectOnFiber

根据fiber的tag不同,对不同类型的节点处理。

对于classComponent,根据current === null ? 区分是mount还是update,调用componentDidMount或者componentDidUpdate.如果this.setState赋值了第二个参数回调函数,也会在此调用。

对于functionCOmponent,调用useLayoutEffect hook的回调函数,调度useEffect和销毁和回调函数。

注意,useLayoutEffect的销毁函数是在mutation阶段完成的,而本次更新的回调函数是在layout阶段完成的,他们是同步的,而useEffect则需要在before-mutation阶段前调度,然后等layout阶段完成后,再异步执行。这就是useEffect和useLayoutEffect的区别。useEffect的销毁函数也是在layout阶段执行的。

  • useEffect的调度是在before-mutation阶段之前完成的,在la yout阶段之后赋值完毕才异步调用的,他的销毁函数是在layout阶段调度的。
  • useLayoutEffect是在layout阶段直接调用的,而他的销毁函数是在mutation阶段调用的,他们是同步执行的,

这也是useLayoutEffect会在dom创建渲染之后,浏览器绘制布局(浏览器layout阶段)之前执行的,而useEffect是在浏览器绘制布局(浏览器layout阶段)之后执行的。执行跟调度不同,调度相当于注册,通过ScheduleCallback根据不同优先级注册回调函数,

commitLayoutEffects会做的第二件事是commitAttachRef

获取do m实例,更新ref。

至此layout阶段结束。

current Fiber树切换

workInProgress Fiber树commit阶段完成渲染后会变为current Fiber树。这行代码的作用就是切换fiberRootNode指向的current Fiber树

为什么在mutation阶段之后,layout阶段之前,英文注释已经解释了,因为componentWillUnmount是在mutation阶段执行(commiteDeletion),所以此时的current fiber树还指向前一次更新的fiber树,componentWillUnMount获取到的还是更新前的Dom。

而componnetDidMount/componentDidUpdate是在layout阶段执行,此时的current fiber树必须指向新的fiber树,在函数里面获取到的dom才是新的。所以fiber树的切换就应该在mutation阶段之后,layout阶段之前完成。

总结: layout阶段之前会替换fiber树,layout阶段会遍历effectList,调度useEffect的执行和销毁函数,调度useLayoutEffect的执行函数,对于类组件,调用对应的componentDidMount/componentDidUpdate等等,并且赋值ref。

总结:

  • render阶段分为递归两个阶段,

    • 递阶段执行beginWork,在mount的时候为儿子创建fiber节点并且连在一起,但不会创建dom,并且不会打上effectTag标记。在update的时候,为fiber节点打上变化的effectTag。

    • 到了归阶段的时候,遵循儿子完成即自己完成,儿子没完成,就从大儿子逐步通过sibling循环完成递归阶段,所有儿子完成递归阶段后,自己再执行归阶段。归阶段执行函数completeWork,该函数在update的时候,会处理props,存放在fiber.updateQueue上。在mount的时候,创建do m节点,关键的是他还会将创建完的子dom,插入到自己下面。那么当rootfiber执行完completeWork的时候,已经有一颗完整的dom树了。

    • render阶段的变化会存放在effectList中,每一个完成completeWork的fiber,会根据有无effectTag,而判断是否加入effectList中,然后在commit阶段只需遍历effectList即可,不用再遍历整颗fiber树。

  • commit阶段对应Renderer模块,他是同步的,不可中断的。commit阶段分为三个阶段,before-mutation,mutation,layout,分别对应创建dom之前,创建dom,创建dom之后,他们都是遍历effectList,做对应的事情。,其次,before-mutatiion之前和layout阶段之后也会处理一些事情。

    • before-mutation阶段之前:变量赋值,状态重置,还有调度useEffect!通过ScheduleCallback注册了回调函数。

    • before-mutatioin阶段,dom创建之前, 会遍历effectList,依此执行处理dom节点渲染/删除后的foucs, blue等逻辑,调用getSnapshotBeforeUpdate生命周期钩子

    • mutation阶段:dom创建的时候。主要做了一些事情: 1 通过fiber.deletion删除一次fiber; 2 重置文字节点; 3 更新ref ; 4 根据fiber.effectTag分别处理Placement|Update等等。Placment的时候,获取父级的dom和兄弟dom,判断调用appendCHild或者insertBefore方法。Update的时候,会调用useLayoutEffect的销毁函数(mutation阶段执行useLayoutEffect的销毁函数),处理fiber上的props,fiber.updateQueue。

      对于fiber.deletion需要删除的fiber,会调用类组件的componentWillUnMount,调度函数组件useLayout,useEffect的销毁函数,删除dom,解绑ref。

    • mutation阶段之后,layout阶段之前,会进行current fiber树的切换,原因是:因为componentWillUnmount是在mutation阶段执行(commiteDeletion),所以此时的current fiber树还指向前一次更新的fiber树,componentWillUnMount获取到的还是更新前的Dom。

      而componnetDidMount/componentDidUpdate是在layout阶段执行,此时的current fiber树必须指向新的fiber树,在函数里面获取到的dom才是新的。所以fiber树的切换就应该在mutation阶段之后,layout阶段之前完成。

    • layout阶段:dom创建后的阶段,该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段可以参与DOM layout的阶段。该阶段主要做两个事情:遍历effectList,调用commitLayoutEffectObFiber,通过不同的tag,判断是mounth还是update,执行componentDidMount或者是componentDidUpdate,对于函数组件,调度useEffect的执行和销毁函数,调度useLayoutEffect的执行函数。然后赋值ref

useEffect与useLayoutEffect的区别:

useLayoutEffect的销毁函数是在mutation阶段完成的,而本次更新的回调函数是在layout阶段完成的,他们是同步的,而useEffect则需要在before-mutation阶段前调度,然后等layout阶段完成后,再异步执行。这就是useEffect和useLayoutEffect的区别。useEffect的销毁函数也是在layout阶段执行的。

  • useEffect的调度是在before-mutation阶段之前完成的,在la yout阶段之后赋值完毕才异步调用的,他的销毁函数是在layout阶段调度的。
  • useLayoutEffect是在layout阶段直接调用的,而他的销毁函数是在mutation阶段调用的,他们是同步执行的,

这也是useLayoutEffect会在dom创建渲染之后,浏览器绘制布局(浏览器layout阶段)之前执行的

为什么hooks里面调用hooks会报错。

react通过给ReactCurrentDispatcher.current赋值,里面存放着我们使用的hooks。他会在不同的上下文为其赋值不同的dispatcher。比如

根据当前是mount还是update赋值不同的dispatcher

export const åContextOnlyDispatcher: Dispatcher = 
  readContext,

  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useInsertionEffect: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  useDebugValue: throwInvalidHookError,
  useDeferredValue: throwInvalidHookError,
  useTransition: throwInvalidHookError,
  useMutableSource: throwInvalidHookError,
  useSyncExternalStore: throwInvalidHookError,
  useId: throwInvalidHookError,

  unstable_isNewReconciler: enableNewReconciler,
;
const HooksDispatcherOnMount: Dispatcher = 
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,

  unstable_isNewReconciler: enableNewReconciler,
;
const HooksDispatcherOnUpdate: Dispatcher = 
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,

  unstable_isNewReconciler: enableNewReconciler,
;

在useEffect内部,ReactCurrentDispatcher.current已经赋值了ContextOnlyDispatcher,当再调用useState的时候实际上是调用

throwInvalidHookError抛出错误。

以上是关于react源码学习-架构篇-commit阶段的主要内容,如果未能解决你的问题,请参考以下文章

react源码学习-架构篇-render阶段

自顶而下学习react源码 架构篇 render阶段

react源码学习-实习篇-状态更新

react源码解析8.render阶段

React源码Part7——Commit(Mutation阶段)

React源码Part6——Commit阶段(beforeMutation)