React技术揭密学习(二)

react

学习React技术揭秘

Render阶段 - 协调器 - Reconciler工作

  • render阶段开始于performSyncWorkOnRoot或者performConcurrentWorkOnRoot.

  • 取决于同步还是异步更新

// performSyncWorkOnRoot会调用该方法

function workLoopSync() {

while (workInProgress !== null) {

performUnitOfWork(workInProgress);

}

}

// performConcurrentWorkOnRoot会调用该方法

function workLoopConcurrent() {

while (workInProgress !== null && !shouldYield()) {

performUnitOfWork(workInProgress);

}

}

  • shouldYield: 判断浏览器当前是否存在剩余时间.
  • workInProcess: 代表当前已经创建的workInProcess fiber

  • performUnitOfWork:

    • 创建下一个Fiber节点, 并赋值给workInProcess

    • 并将workInProcess与已经创建的Fiber节点链接起来, 构成一个fiber树

  • Fiber Reconciler重构了Stack Reconciler, 通过遍历实现可中断的递归

  • performUnitOfWork. 工作分为"递"和"归"
  • "递"阶段

    • rootFiber开始向下深度优先遍历. 为每个fiber节点调用beginWork方法
    • 该方法会根据传入的Fiber节点创建子Fiber节点, 并将两个Fiber节点链接起来.
    • 如果有兄弟节点, 并根据节点, 依次创建兄弟节点
    • 遍历到叶子节点时, 会进入归阶段.

  • "归"阶段

    • 归节点调用completeWork阶段, 处理fiber节点

    • 某个fiber节点完成执行完completeWork

      • 如果其存在兄弟Fiber节点, 会进入兄弟节点的递阶段
      • 如果不存在兄弟fiber节点, 会进入父级Fiber的归阶段

  • "递"和"归"会交替执行, 直到"归"到rootFiber. render阶段的工作完成

单一文本的叶子节点不会执行beginWork/completeWork

beginWork

function beginWork(

current: Fiber | null,

workInProgress: Fiber,

renderLanes: Lanes,

): Fiber | null {

// ...省略函数体

}

  • current: 当前组件对应的Fiber节点在上一次更新时的Fiber节点. workInProcess.alternate

  • workInProcess: 当前组件对应的fiber节点

  • renderLanes: 优先级相关

  • 区分mount还是update

    • mount时: current === null

    • update时: current !== null

  • begin工作分为两部分:

    • update时: current节点存在, 满足一定条件, 进行复用. 克隆current.childworkInProcess.child

    • mount时: 除了fiberRootNode以外, current === null. 根据fiber.tag不同, 创建不同类型的子fiber节点

  • Update: 满足如下情况. -> didReceiveUpdate = false;直接复用上一次的fiber节点

    • oldProps === newProps && workInProcess.type === current.type不变

    • includesSomeLane(renderLanes, updateLanes), 当前节点的优先级不够

  • Mount:

    • 根据不同类型的tag, 进入不同类型的Fiber的创建逻辑
    • 最终都会执行reconcileChildren: 根据fiber节点创建子fiber节点

function beginWork(

current: Fiber | null,

workInProgress: Fiber,

renderLanes: Lanes

): Fiber | null {

// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)

if (current !== null) {

const oldProps = current.memoizedProps;

const newProps = workInProgress.pendingProps;

if (

oldProps !== newProps ||

hasLegacyContextChanged() ||

(__DEV__ ? workInProgress.type !== current.type : false)

) {

didReceiveUpdate = true;

} else if (!includesSomeLane(renderLanes, updateLanes)) {

didReceiveUpdate = false;

switch (workInProgress.tag) {

// 省略处理

}

// 复用current

return bailoutOnAlreadyFinishedWork(

current,

workInProgress,

renderLanes,

);

} else {

didReceiveUpdate = false;

}

} else {

didReceiveUpdate = false;

}

// mount时:根据tag不同,创建不同的子Fiber节点

switch (workInProgress.tag) {

case IndeterminateComponent:

// ...省略

case LazyComponent:

// ...省略

case FunctionComponent:

// ...省略

case ClassComponent:

// ...省略

case HostRoot:

// ...省略

case HostComponent:

// ...省略

case HostText:

// ...省略

// ...省略其他类型

}

}

  • reconcileChildren工作:

    • mount: 创建新的子fiber节点

    • update: 会将当前fiber节点和上次更新的fiber节点对比(diff), 产生结果比较新的fiber节点

    • 最终都会生成新的子fiber节点, 赋值给workInProcess.child, 作为本次beginWork的返回值
    • 并作为下一次: performUnitOfWork执行时workInProcess的传参
    • reconcileChildFibers会为生成的Fiber节点带上effectTag属性

export function reconcileChildren(

current: Fiber | null,

workInProgress: Fiber,

nextChildren: any,

renderLanes: Lanes

) {

if (current === null) {

// 对于mount的组件

workInProgress.child = mountChildFibers(

workInProgress,

null,

nextChildren,

renderLanes,

);

} else {

// 对于update的组件

workInProgress.child = reconcileChildFibers(

workInProgress,

current.child,

nextChildren,

renderLanes,

);

}

}

  • effectTag

    • render(Reconciler)阶段时在内存中工作的. 工作接受后, 通知Renderer进行dom操作
    • dom的操作保存在effectTag

  • 通知RendererFiber节点对应的dom插入到页面中需要满足两个条件

    • fiber.stateNode存在, 即fiber节点保存了对应的dom节点

    • Fiber节点上存在Placement effectTag

  • fiber.stateNode会在completeWork中完成

  • mount时只用rootFiber会被赋值Placement effectTag. commit阶段一次性插入完成

commitWork

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: {

// ...省略

return null;

}

case HostRoot: {

// ...省略

updateHostContainer(workInProgress);

return null;

}

case HostComponent: {

// ...省略

return null;

}

// ...省略

  • HostComponent: 原生组件对应的fiber节点

  • Update时:

    • Fiber节点对应的dom节点已经存在了, 不需要生成dom节点, 主要处理props

      • onClick, onChange等回调函数的注册

      • 处理style prop

      • 处理DANGEROUSLY_SET_INNER_HTML prop

      • 处理children prop

    • 主要调用updateHostComponent

      • 处理完的props赋值给workInProcess.updateQueue

      • 并最终会在commit阶段(Renderer)渲染在页面上
      • workInProgress.updateQueue = (updatePayload: any);
      • updatePayload为数组, 偶数为变化的prop key, 奇数为prop value

  • Mount时

    • 主要逻辑:

      • fiber节点增加对应的dom节点
      • 将子孙dom节点插入到刚生成的dom节点上
      • 与update时中的updateHostComponent类似的props处理过程

  • completeWork阶段属于归阶段函数. 每次调用appendAllChildren时, 都会将已生成的子孙dom节点插入到当前生成的dom节点下.
  • "归"到rootFiber时, 已经有一颗已经构建好的内存中的dom树

case HostComponent: {

popHostContext(workInProgress);

const rootContainerInstance = getRootHostContainer();

const type = workInProgress.type;

if (current !== null && workInProgress.stateNode != null) {

// update的情况

updateHostComponent(

current,

workInProgress,

type,

newProps,

rootContainerInstance,

);

} else {

// mount的情况

// ...省略服务端渲染相关逻辑

const currentHostContext = getHostContext();

// 为fiber创建对应DOM节点

const instance = createInstance(

type,

newProps,

rootContainerInstance,

currentHostContext,

workInProgress,

);

// 将子孙DOM节点插入刚生成的DOM节点中

appendAllChildren(instance, workInProgress, false, false);

// DOM节点赋值给fiber.stateNode

workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程

if (

finalizeInitialChildren(

instance,

type,

newProps,

rootContainerInstance,

currentHostContext,

)

) {

markUpdate(workInProgress);

}

}

return null;

}

  • effectList
  • 解决需要所有fiber节点, 找到标记effectTagfiber

  • completeWork的上层函数completeUnitOfWork中:

  • 每次执行完completeWork, 且存在effectTagfiber节点, 会保存在effectList单向链表中
  • 第一个节点保存在fiber.firstEffect

  • 最后一个节点保存在fiber.lastEffect

  • 在"归"阶段, 所有effectTag的Fiber节点, 都会追加到这条链表中
  • 最终形成一条以rootFiber.firstEffect为起点的effectList

  • commit阶段只需要遍历effectList即可

                       nextEffect         nextEffect

rootFiber.firstEffect -----------> fiber -----------> fiber

  • render阶段全部完成工作后.

    • performSyncWorkRoot函数中, fiberNode传递给commitRoot.

    • 开启commit阶段的工作

Commit阶段 - 渲染器 - Renderer器工作

  • fiberRootNode作为传参

  • fiberRootNode.firstEffect上保存了一条需要执行副作用Fiber节点单向链表

  • 这些fiber节点的updateQueue上保存了变化的props
  • 这些副作用对应的dom操作, 在commit阶段执行
  • 一些声明周期钩子函数(componentDidXXX), hook(useEffect)在commit阶段执行.
  • commit阶段的主要工作

    • before mutation阶段: 执行dom操作前

    • mutation阶段: 执行dom操作

    • layout阶段: 执行dom操作后

  • before mutation之前layout之后还有一些工作

    • useEffect的触发

    • 优先级相关重置

    • ref的绑定的解绑

  • before mutation之前

    • 变量赋值, 状态重置
    • 最后会赋值一个firstEffect, commit三个子阶段都会用到

  • layout之后

    • useEffect相关处理

    • 性能追踪相关
    • commit出发一些生命周期函数, hook

before mutation

  • 遍历effectList并调用commitBeforeMutationEffects函数处理

// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级

const previousLanePriority = getCurrentUpdateLanePriority();

setCurrentUpdateLanePriority(SyncLanePriority);

// 将当前上下文标记为CommitContext,作为commit阶段的标志

const prevExecutionContext = executionContext;

executionContext |= CommitContext;

// 处理focus状态

focusedInstanceHandle = prepareForCommit(root.containerInfo);

shouldFireAfterActiveInstanceBlur = false;

// beforeMutation阶段的主函数

commitBeforeMutationEffects(finishedWork);

focusedInstanceHandle = null;

  • commitBeforeMutationEffects

    • 处理DOM节点渲染/删除后的autoFocus, blur的逻辑
    • 调用getSnapshotBeforeUpdate生命周期钩子
    • 调度useEffect

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, () => {

// 调用useEffect

flushPassiveEffects();

return null;

});

}

}

nextEffect = nextEffect.nextEffect;

}

}

  • 调用getSnapshotBeforeUpdate

    • render阶段的任务可能被中断重新开始, componentWillXXX钩子可能被多次触发. 变为不安全的

    • 使用新的getSnapshotBeforeUpdate, 在commit阶段内同步执行, 所以只会触发一次

  • 异步调度

    • flushPassiveEffects方法从全局变量rootWithPendingPassiveEffects获取effectList

    • effectList包含了需要执行副作用的Fiber节点

      • 插入dom节点(PLACEMENT)
      • 更新dom节点(UPDATE)
      • 删除dom节点(DELETION)
      • 当一个FunctionComponent含有useEffect或者useLayoutEffect时, 它的fiber节点也会含有Passive effectTag

    • flushPassiveEffect内部遍历effectList, 执行effect回调函数

    • useEffect分成三步:

      1. before mutation阶段scheduleCallback中调度flushPassiveEffects

      2. layout阶段之后, 将effectList赋值给rootWidthPendingPassiveEffects

      3. scheduleCallBack触发flushPassiveEffects, flushPassiveEffects内部遍历rootWidthPendingPassiveEffects

    • 原因:

      • 在浏览器完成渲染和绘制以后, 传给useEffect的函数会延迟调用.
      • 使得它试用于许多浏览器常见的副作用场景, 比如设置订阅和事件处理等情况
      • 因此不应该在函数中执行阻塞浏览器更新屏幕的操作
      • 防止同步执行时阻塞浏览器渲染

mutation阶段

  • 同样遍历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

    • 根据ContentRest effectTag重置文本节点
    • 更新ref

    • 根据effectTag分别处理, 其中effectTag分别包括(Placement|Update|Deletion|Hydrating)

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;

}

}

nextEffect = nextEffect.nextEffect;

}

}

  • Placement effect: fiber节点对应的dom节点需要插入到页面中

    1. 获取父级dom节点
    2. 获取fiber的兄弟节点, getHostSibling非常耗时, 因为同一个fiber节点不止包含HostComponent*
    3. 根据dom节点是否存在, 判断执行parentNode.insertBefore或者parentNode.appendChild

  • Update effect: fiber节点需要更新, 调用commitWork

    • FunctionComponent mutation

      • fiber.tagFunctionComponent, 会调用commitHookEffectListUnmount.
      • 遍历effectList, 执行所有useLayoutEffect hook的销毁函数

    • HostComponent mutation

      • 调用commitUpdate

      • 最终在updateDOMProperties中将render阶段 completeWork中为fiber节点赋值的updateQueue对应的内容, 渲染在页面上

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);

// 处理 children

} else if (propKey === CHILDREN) {

setTextContent(domElement, propValue);

} else {

// 处理剩余 props

setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);

}

}

  • Deletion Effect: 当fiber节点含有effectTag时, 调用commitDeletion

    • 递归调用fiber节点以及子孙fiber节点. 调用fiber.tagClassComponentcomponentWillUnComponent.
    • 从页面中移除fiber节点对应的dom节点

    • 解绑ref

    • 调度useEffect的销毁函数

layout

  • 代码是在dom渲染完成后执行的
  • 该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的dom

  • 遍历effectList, 执行函数

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;

}

}

  • commitLayoutEffects

    1. commitLayoutEffectOnFiber调用生命周期钩子和hook相关操作

      • ClassComponent调用ComponentDidMount/ComponentDidUpdate

      • 调用this.state()第二个参数函数
      • FunctionComponent调用useLayoutEffect回调函数, 调度useEffect的销毁和回调函数

      • useLayoutEffect(): 从上一次更新的销毁到这次更新执行, 是同步执行的

      • useEffect(): 需要先调度, 在Layout阶段执行完成后, 再异步执行

    2. commitAttachRef: 赋值ref: 获取dom实例, 更新dom

  • mutation阶段执行后, layout开始前, 切换root.current = finishWork

  • 因为layout执行的生命周期函数和hook需要获取新的dom信息

以上是 React技术揭密学习(二) 的全部内容, 来源链接: utcz.com/z/383352.html

回到顶部