自顶而下学习react源码 (3)架构篇 commit阶段

commit阶段

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

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

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抛出错误。

学习文章来自:https://react.iamkasong.com/renderer/layout.html#commitlayouteffectonfiber

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在 react commit 阶段React 会执行以下操作: - 调用组件的 `render()` 方法来获取组件的虚拟 DOM(virtual DOM) - 对比上一次渲染的虚拟 DOM 和当前虚拟 DOM,找出它们之间的差异 - 打补丁,将差异应用到真实 DOM 上,使真实 DOM 与虚拟 DOM 保持同步 除了 commit 阶段React 中还有以下几个阶段: - render 阶段:调用组件的 `render()` 方法,获取组件的虚拟 DOM - reconcile 阶段:对比上一次渲染的虚拟 DOM 和当前虚拟 DOM,找出它们之间的差异 - commit 阶段:打补丁,将差异应用到真实 DOM 上,使真实 DOM 与虚拟 DOM 保持同步 ### 回答2: 在React中,commit阶段React更新DOM并进行最终渲染的阶段。在commit阶段React通过协调更新队列中的任务,将变化应用到实际的DOM树上。 具体来说,commit阶段主要完成以下任务: 1. 生成DOM更新:在commit阶段React会将变化转换为具体的DOM操作。这包括创建、更新和删除DOM节点等。 2. 调用生命周期方法:在更新DOM之前,React会调用组件的生命周期方法,例如`componentDidMount`或`componentDidUpdate`。这样,开发者可以在这些方法中执行一些副作用或处理逻辑,例如发送网络请求、订阅事件等。 3. 处理副作用:在commit阶段React还会处理一些副作用操作,例如更新refs、触发组件的回调函数、处理错误边界等。 除了commit阶段React还有其他几个重要的阶段: 1. 事件处理阶段:在这个阶段React会收集和处理用户的事件,并将事件传递给相应的组件。这包括事件捕获、冒泡、合成事件的处理等。 2. 虚拟DOM的构建阶段:在这个阶段React会根据组件的状态变化,生成新的虚拟DOM树。React通过对比新旧虚拟DOM树的差异,减少对实际DOM的操作。 3. 调解阶段:在这个阶段React会根据组件的更新情况,决定是否要进行组件的更新。React使用一些策略,例如shouldComponentUpdate或memo来进行性能优化,避免不必要的更新。 4. 协调阶段:在这个阶段React会协调处理多个组件的更新。它通过批处理更新任务,将相似或相关的更新合并在一起,减少不必要的计算和渲染。 总之,Reactcommit阶段是实际将变化应用到DOM的阶段,同时React还有其他阶段用于处理事件、构建虚拟DOM、调解和协调组件的更新。 ### 回答3: 在 React 中,commit 阶段是用于将虚拟 DOM 更新应用到实际的 DOM 上的阶段。在 commit 阶段React 会执行以下操作: 1. 执行必要的 DOM 操作:根据虚拟 DOM 的变化,React 将添加、更新或删除相应的 DOM 节点。 2. 触发组件的副作用操作:在 React 中,副作用操作指的是那些不直接与渲染相关,但仍然需要在组件渲染后进行的任务,例如发起网络请求、订阅事件等。在 commit 阶段React 将触发组件中声明的副作用操作。 3. 运行生命周期方法:在组件更新完成并且 DOM 更新后,React 会运行相应组件实例中的生命周期方法,例如 componentDidUpdate。 除了 commit 阶段React 还有其他几个重要的阶段: 1. 渲染阶段(render phase):在渲染阶段React 会比对组件的新旧状态并生成虚拟 DOM。这个阶段的主要目标是计算变更,而不会实际应用这些变更。 2. 调度阶段(scheduling phase):在调度阶段React 会根据当前系统的优先级决定是否中断渲染过程,以便让用户交互或其他高优先级任务得到处理。这个阶段采用了一种称为 Fiber 的机制,用于实现可中断的渲染过程。 3. 布局阶段(layout phase):在布局阶段React 会计算出虚拟 DOM 在浏览器中的位置和尺寸。这个阶段的主要目标是收集有关 DOM 树结构的信息,以便在下一阶段进行 DOM 更新。 4. 提交阶段commit phase):如前所述,提交阶段是将虚拟 DOM 更新应用到实际的 DOM 上的阶段。 总结起来,React 的渲染过程可以分为调度阶段、渲染阶段、布局阶段和提交阶段。其中,commit 阶段是将虚拟 DOM 的更新应用到实际 DOM 上的阶段,还包括执行 DOM 操作、触发副作用操作和运行生命周期方法等任务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值