本文主要将的是Fiber架构三核心中渲染器Renderer,在Reconciler调度器中“归”过程回到rootfiber节点并执行完之后会调用commitroot并传入fiberRootNode来进入到Renderer阶段(commit阶段),在commit阶段会遍历effectList来进行DOM操作,在该阶段我将其细分为三个小阶段:
EffectList是一个单向链表,在Reconciler递归中执行completeWork时构建,最终形成一个以rootfiber.firstEffect为起点的单向链表,其他fiber.updateQueue以key,vakue的数组形式保存了需要更新的props
这个阶段主要是遍历effectList并执行commitBeforeMutationEffects(旧版)/commitBeforeMutationEffectsImpl(新版), 主要做了一下三件事:
function commitBeforeMutationEffects() { while (nextEffect !== null) { const current = nextEffect.alternate; if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { // ...focus blur相关 } const effectTag = nextEffect.effectTag; // 调用getSnapshotBeforeUpdate if ((effectTag & Snapshot) !== NoEffect) { commitBeforeMutationEffectOnFiber(current, nextEffect); } // 调度useEffect if ((effectTag & Passive) !== NoEffect) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); return null; }); } } nextEffect = nextEffect.nextEffect; } }
commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名,主要是生命周期的调用。
从Reactv16开始,componentWillXXX钩子前增加了UNSAFE_前缀。原因是16用FIber架构重写,将原来的Stack Reconciler改为Fiber Reconciler后,render阶段可能会执行多次,而componentWillXXX钩子都存在于render阶段,这就会导致重复执行,设想如果我们在支付的时候,执行了多次,那就玩大了,所以进行了标记。为了解决这个问题,React提供了一个新的钩子getSnapshotBeforeUpdate,它可以保存DOM更新前的信息快照,然后给到componentDidUpdate. 并且它是在commit阶段执行的,不会重复执行。
// 调度useEffect if ((effectTag & Passive) !== NoEffect) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); return null; }); } }
代码中可见,scheduleCallback方法由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。而flushPassiveEffects就是用来调度useEffect的。
该阶段主要是来执行DOM的一些操作,主要也是遍历EffectList根据fiber的tag来处理不同的逻辑并更新对应的ref,主要做了以下事情:
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { // 遍历effectList while (nextEffect !== null) { const effectTag = nextEffect.effectTag; // 根据 ContentReset effectTag重置文字节点 if (effectTag & ContentReset) { commitResetTextContent(nextEffect); } // 更新ref if (effectTag & Ref) { const current = nextEffect.alternate; if (current !== null) { commitDetachRef(current); } } // 根据 effectTag 分别处理 const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating); switch (primaryEffectTag) { // 插入DOM case Placement: { commitPlacement (nextEffect); nextEffect.effectTag &= ~Placement; break; } // 插入DOM 并 更新DOM case PlacementAndUpdate: { // 插入 commitPlacement(nextEffect); nextEffect.effectTag &= ~Placement; // 更新 const current = nextEffect.alternate; commitWork(current, nextEffect); break; } // SSR case Hydrating: { nextEffect.effectTag &= ~Hydrating; break; } // SSR case HydratingAndUpdate: { nextEffect.effectTag &= ~Hydrating; 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;`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, ); } }` } } nextEffect = nextEffect.nextEffect; } }
当fiber.effectTag为Placement时,则通过commitPlacement函数,进行DOM的插入操作。
当Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。
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, ); } }
当fiber.tag为HostComponent,会调用commitUpdate。最终会在updateDOMProperties 中将render阶段 completeWork 中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。
function updateDOMProperties( domElement: Element, updatePayload: Array, 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]; if (propKey === STYLE) { setValueForStyles(domElement, propValue); } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { setInnerHTML(domElement, propValue); } else if (propKey === CHILDREN) { setTextContent(domElement, propValue); } else { setValueForProperty(domElement, propKey, propValue, isCustomComponentTag); } } }
当Fiber节点含有Deletion effectTag,意味着该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为commitDeletion。
该阶段之所以称为layout,因为该阶段的代码都是在DOM修改完成(mutation阶段完成)后执行的。该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段是可以参与DOM layout的阶段。
注意:由于 JS 的同步执行阻塞了主线程,所以此时 JS 已经可以获取到新的DOM,但是浏览器对新的DOM并没有完成渲染。
同before mutation、mutation阶段一样,都是遍历effectList来,并执行对应函数来进行不同的处理,这里调用的是commitLayoutEffects,主要做了两件事:
commitLayoutEffectOnFiber是commitLifeCycles的别名
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; // 调用生命周期钩子和hook if (effectTag & (Update | Callback)) { const current = nextEffect.alternate; commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); } // 赋值ref if (effectTag & Ref) { commitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; } }
在commitLifeCycles函数中,当fiber.tag为ClassComponent时,会通过current === null?区分是mount还是update,调用componentDidMount 或componentDidUpdate 。
case ClassComponent: { const instance = finishedWork.stateNode; if (finishedWork.flags & Update) { if (current === null) { if ( enableProfilerTimer && enableProfilerCommitHooks && finishedWork.mode & ProfileMode ) { try { startLayoutEffectTimer(); instance.componentDidMount(); } finally { recordLayoutEffectDuration(finishedWork); } } else { instance.componentDidMount(); } }
以下函数也会在commitLifeCycles中调用:
commitAttachRef函数就比较简单,就是获取DOM实例,并更新ref
function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } // Moved outside to ensure DCE works with this flag if (enableScopeAPI && finishedWork.tag === ScopeComponent) { instanceToUse = instance; } if (typeof ref === 'function') { ref(instanceToUse); } else { ref.current = instanceToUse; } } }
至此,commit阶段也完成了,这时候再将双缓存树的切换fiberRootNode的current指向current Fiber树,就可以更新试图。
Q: useEffect和useLayoutEffect的区别?
A:useLayoutEffect从上一次更新的销毁函数调用(mutation阶段)到本次更新的回调函数调用(layout阶段)是同步执行的。
而useEffect则需要先在before mutation调度,在Layout阶段完成后再异步执行。
React技术揭秘