React18之fiber

发布于 2024-03-11  117 次阅读


一、什么是Fiber

Fiber定义:React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。

功能:

  1. 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler;
  2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息;
  3. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...);

二、Fiber的结构

constructor(tag: WorkTag, pendingProps: Props, key: Key) {
		// 实例
		this.tag = tag;
		this.key = key || null;
		// HostComponent <div> div DOM
		this.stateNode = null;
		// FunctionComponent () => {}
		this.type = null;

		// 构成树状结构
		// 表示节点之间的关系
		// 指向父FiberNode
		this.return = null;
		// 指向兄弟FiberNode
		this.sibling = null;
		// 指向子FiberNode
		this.children = null;
		// 针对同级FiberNode 的index
		this.index = 0;

		this.ref = null;

		// 作为工作单元
		// 开始工作时的props
		this.pendingProps = pendingProps;
		// 结束工作时将pendingProps 赋值给 memoizedProps
		this.memoizedProps = null;
		this.memoizedState = null;
		this.updateQueue = null;

		this.alternate = null;
		// 副作用
		this.flags = NoFlags;
		this.subtreeFlags = NoFlags;
		this.deletions = null;
	}
}

上述代码就是Fiber的基本结构(非源码)源码地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiber.new.js#L117

可以看到Fiber架构描述了一个节点的不同作用

描述节点的静态数据

this.tag = tag;
		this.key = key || null;
		// HostComponent <div> div DOM
		this.stateNode = null;
		// FunctionComponent () => {}
		this.type = null;

描述节点之间的关系

                // 构成树状结构
		// 表示节点之间的关系
		// 指向父FiberNode
		this.return = null;
		// 指向兄弟FiberNode
		this.sibling = null;
		// 指向子FiberNode
		this.children = null;
		// 针对同级FiberNode 的index
		this.index = 0;

Q:为什么指向的父节点是return而不是parent?

因为作为一个工作单元,return指节点执行完completeWork后会返回的下一个节点。子Fiber节点及其兄弟节点完成工作后会返回其父级节点,所以用return指代父级节点

作为工作单元

                // 作为工作单元
		// 开始工作时的props
		this.pendingProps = pendingProps;
		// 结束工作时将pendingProps 赋值给 memoizedProps
		this.memoizedProps = null;
		this.memoizedState = null;
		this.updateQueue = null;

三、Fiber结构树

Fiber树是一个运行时生成的结构树,看下面的例子,现在有这样一个结构

function App() {
	return (
		<div>
			<Child />
                        <p>react</p>
		</div>
	);
}
function Child() {
	return <span>hello</span>;
}
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

这段代码在运行后会生成一个Fiber树,我们先来看一下这棵树的结构

首先Fiber树中存在两个特殊的节点,分别为FiberRootNode和HostRootFiber,这是Fiber树的根节点他们的关系为 FiberRootNode通过current指针指向HostRootFiber,HostRootFiber通过stateNode指针指向FiberRootNode结构如下

接着HostRootFiber会指向组件 App,这里为什么是指向App而不是指向App组件返回的div呢?

这是因为React会针对组件单独创建一个Fiber节点

注意:React有一条优化路径是不针对但文本节点创建Fiber

综上所述 该例子的最终Fiber树结构为

使用“双缓存”

在内存中绘制当前的fiber dom,绘制完毕后直接替换上一帧的fiber dom,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber,正在内存中构建的Fiber树称为workInProgress Fiber,两者通过alternate连接

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过current指针指向不同的fiber dom切换,当update时,workInProgressFiber rende完成后会跟currentFiber 替换,下一次更新会将当前currentFiber(上一次的workInProgressFiber)替换

工作流程

首屏渲染(mount)

首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点;

  1. 区分fiberRootNode与rootFiber:因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode;
  2. fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber;
  3. 因为是首次渲染,此时页面还没有挂在所有的DOM,所以rootFiber还没有子fiber dom

render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber;

  1. 在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性(后续的diff),在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)

update阶段

  1. 假设元素更新,这会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树,且会尽可能复用现有的current Fiber
  2. 在这个阶段React会从当前触发更新的节点向上寻找到根节点FiberRootNode然后生成新的Fiber树,并且此过程遵循第一点,React会尽可能复用现有的节点
  3. 在workInProgress构建过程中会为每个Fiber节点打上标记比如Placement、Update、ChildDeletion

这就是大致的过程 这个过程中主要又两个阶段 分别是render和commit

  • render阶段:从根节点开始创建wip Fiber树,计算出每个fiberNode发生的更新,标记在flagssubtreeFlags
  • commit阶段:把每个fiberNode的改变提交在宿主环境中,比如在浏览器上就是更新dom

render阶段

内容:Fiber节点是如何被创建并构建成render树的

流程概览

在render的阶段中,根据是同步还是异步,执行performSyncWorkOnRoot 和 performConcurrentWorkOnRoot

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

区别:是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历

说明:

  • workInProgress代表当前已创建的workInProgress fiber
  • performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树;

虽然fiber reconciler是从stack reconciler重构而来,但都是通过遍历的方式实现可中断的异步递归

首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork(下面详细讲);

  • 该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来;
  • 当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段;

在“归”阶段会调用completeWork(下面详细讲)处理Fiber节点。

  • 当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段;
  • 如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段;

“递”和“归”阶段会交错执行直到“归”到rootFiber

还是上面的例子,他的流程大体是这样子的

function App() {
  return (
    <div>
      i am
      <span>text</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

// 没有叶子节点是因为React针对只有单一文本子节点的Fiber节点做了性能优化

// 递归的格式
function performUnitOfWork(fiber) {
  // 执行beginWork

  if (fiber.child) {
    performUnitOfWork(fiber.child);
  }

  // 执行completeWork

  if (fiber.sibling) {
    performUnitOfWork(fiber.sibling);
  }
}

beginWork

地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3075

beginWork的工作是传入当前Fiber节点,创建子Fiber节点

入参

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函数体
}
  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级相关,后面讲文章会讲

可以根据current!==null,判断组件时mount还是update

  • mount:首次渲染,当前组件的fiber节点为null;
  • update:之前已经mount,fiber节点不为null;

基于此原因,beginWork的工作可以分为两部分:

  • update时:如果current存在,在满足一定条件时可以复用current节点,(diff)这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child;
  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点;
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略

    // 复用current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他类型
  }
}

update时

  • didReceiveUpdate = false:不需要新建fiber,可以直接复用
  • !includesSomeLane(renderLanes, updateLanes),即当前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) {
        // 省略处理
      }
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  }

mount时

// 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节点比较(Diff),将比较的结果生成新Fiber节点;
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,
    );
  }
}

mountChildFibers & reconcileChildFibers

  • 都会生成新的fiber节点返回给workInProgress.child,作为本次beginWork的返回值,在下次performUnitOfWork执行时workInProgress的入参

effectTag

  • render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中

completeWork

作用:针对不同的fiber.tag调用不同的处理逻辑

地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js#L673

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;
    }
  // ...省略

判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)

case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update的情况
    // ...省略
  } else {
    // mount的情况
    // ...省略
  }
  return null;
}

update

当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:

  • onClick、onChange等回调函数的注册(合成事件,后面会写文章讲)
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop
if (current !== null && workInProgress.stateNode != null) {
  // update的情况
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );
}

updateHostComponent git地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js#L225

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上,其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value

workInProgress.updateQueue = (updatePayload: any);

mount时

  • 为Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点中
  • 与update逻辑中的updateHostComponent类似的处理props的过程
// 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);
}

effectList

Q:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?

completeWork在上层函数completeUnitOfWork上维护了一个单向链表

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。

类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。

地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L1744

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

最后

在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

commitRoot(root);

commit阶段

流程概览

commitRoot(root);

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

这些副作用对应的DOM操作在commit阶段执行。

源码地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L2001

除此之外,一些生命周期钩子(比如componentDidXXX)、hook(比如useEffect)需要在commit阶段执行。

commit阶段的主要工作(即Renderer的工作流程)分为三部分:

  1. before mutation阶段(执行DOM操作前)
  2. mutation阶段(执行DOM操作)
  3. layout阶段(执行DOM操作后)
before mutation
do {
    // 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  // root指 fiberRootNode
  // root.finishedWork指当前应用的rootFiber
  const finishedWork = root.finishedWork;

  // 凡是变量名带lane的都是优先级相关
  const lanes = root.finishedLanes;
  if (finishedWork === null) {
    return null;
  }
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  // 重置Scheduler绑定的回调函数
  root.callbackNode = null;
  root.callbackId = NoLanes;

  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  // 重置优先级相关变量
  markRootFinished(root, remainingLanes);

  // 清除已完成的discrete updates,例如:用户鼠标点击触发的更新。
  if (rootsWithPendingDiscreteUpdates !== null) {
    if (
      !hasDiscreteLanes(remainingLanes) &&
      rootsWithPendingDiscreteUpdates.has(root)
    ) {
      rootsWithPendingDiscreteUpdates.delete(root);
    }
  }

  // 重置全局变量
  if (root === workInProgressRoot) {
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  } else {
  }

  // 将effectList赋值给firstEffect
  // 由于每个fiber的effectList只包含他的子孙节点
  // 所以根节点如果有effectTag则不会被包含进来
  // 所以这里将有effectTag的根节点插入到effectList尾部
  // 这样才能保证有effect的fiber都在effectList中
  let firstEffect;
  if (finishedWork.effectTag > PerformedWork) {
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // 根节点没有effectTag
    firstEffect = finishedWork.firstEffect;
  }

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

layout

主要包括三点内容:

  1. useEffect相关的处理:后面详细讲
  2. 性能追踪相关:代码里有很多和interaction相关的变量。他们都和追踪React渲染时间、性能相关,在Profiler APIDevTool中使用,你可以在这里看到interaction的定义
  3. 在commit阶段会触发一些生命周期钩子(如 componentDidXXX)和hook(如useLayoutEffect、useEffect)。

在这些回调方法中可能触发新的更新,新的更新会开启新的render-commit流程。

  1. before mutation(执行DOM前)

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

地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L2104-L2127

// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
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

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;
  }
}
  1. 处理DOM节点渲染、删除后的autoFocus、blur等操作;
  2. 调用getSnapshotBeforeUpdate
  3. 调度 useEffect

调用getSnapshotBeforeUpdate

commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名,在该方法内会调用getSnapshotBeforeUpdate。

地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberCommitWork.old.js#L222

因为在V16版本后,componentWillXXX钩子为UNSAFE_,所以,React提供了替代的生命周期钩子getSnapshotBeforeUpdate,getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题

Q:为什么从Reactv16开始,componentWillXXX钩子前增加了UNSAFE_前缀?

从React15升级为React16后,源码改动如此之大,说React被重构可能更贴切些。

正是由于变动如此之大,使得一些特性在新旧版本React中表现不一致

为了让开发者能平稳从旧版本迁移到新版本,React推出了三个模式:

  • legacy模式 -- 通过ReactDOM.render创建的应用会开启该模式。这是当前React使用的方式。这个模式可能不支持一些新功能。
  • blocking模式 -- 通过ReactDOM.createBlockingRoot创建的应用会开启该模式。开启部分concurrent模式特性,作为迁移到concurrent模式的第一步。
  • concurrent模式 -- 通过ReactDOM.createRoot创建的应用会开启该模式。面向未来的开发模式。

但是在从legacy迁移到concurrent模式时,可中断的异步更新还替代了同步更新

在Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。

这种行为和Reactv15不一致,所以标记为UNSAFE_。

  • componentWillMount -- componentDidMount
  • componentWillRecieveProps -- getDerivedStateFromProps
  • componentWillUpdate -- getDerivedStateFromProps

在React更新里,每次发起更新都会创建一个Update对象,同一组件的多个Update,会以链表的形式保存在updateQueue中。

update

const update: Update<*> = {
  // ...省略当前不需要关注的字段
  lane, // 表示调度优先级
  payload: null, // 更新挂载的数据,对于this.setState创建的更新,payload为this.setState的传参
  next: null // 与其他update形成链表
};

updateQueue

const queue: UpdateQueue = {
    baseState: fiber.memoizedState, // 更新基于哪个state开始
    firstBaseUpdate: null,  // 更新开始和结束的update
    lastBaseUpdate: null,
    shared: { 
      pending: null, // 更新的单个或多个update形成的链表
    },
    // 其他参数省略...
};

// baseUpdate + shared.pending会作为本次更新需要执行的Update

假设,某个组件updateQueue 存在4个update,数字代表优先级

baseState = '';

A1 - B2 - C1 - D2

// 为了保证更新的连贯性,第一个被跳过的update(B)和后面的update会作为第二次渲染的baseUpdate
// 为BCD
// 首次渲染后
baseState: ''
Updates: [A1, C1]
Result state: 'AC'

// 第二次渲染,B在第一次渲染时被跳过,所以在他之后的C造成的渲
// 染结果不会体现在第二次渲染的baseState中。所以baseState为A而不是上次渲染的Result state AC
// 。这也是为了保证更新的连贯性
baseState: 'A'  // 为了保证一致性,C不在        
Updates: [B2, C1, D2]  
Result state: 'ABCD'

// Updates里出现了两次C

调度useEffect

// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    scheduleCallback(NormalSchedulerPriority, () => { // scheduler提供,调度优先级的回调
      // 触发useEffect
      flushPassiveEffects(); // 具体见后文hooks
      return null;
    });
  }
}

在flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList,就是会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。

Q:为什么要异步调度:

https://zh-hans.reactjs.org/docs/hooks-reference.html#timing-of-effects

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

防止同步执行时阻塞浏览器渲染

mutation(执行DOM中)

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

地址:https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L2091

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

执行内容:

  1. 根据ContentReset effectTag重置文字节点
  2. 更新ref
  3. 根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating),hydrate是SSR,不考虑
最后更新于 2024-03-11