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节点
渲染/删除后的 autoFocus
、blur
逻辑。
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
触发flushPassiveEffects
,flushPassiveEffects
内部遍历`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阶段的主要内容,如果未能解决你的问题,请参考以下文章