likes
comments
collection
share

我对React18 Fiber架构的理解

作者站长头像
站长
· 阅读数 47

基本数据结构

React中有三种节点类型

  1. ReactElementcreateElement的返回值
// 主要的,不包含全部
type ReactElement = {
  $$typeof: any, // Symbol.for('react.element')
  type: any, // dom是标签名、函数组件是函数本身、类组件是类本身
  key: any,
  ref: any,
  props: any,
  _owner: any,
}
  1. ReactComponent:函数组件或类组件
  2. FiberNode:Fiber架构的工作单元,一般情况下与ReactElement对应(有特例下面会提到)
// 主要的,不包含全部
type FiberNode = {
  // 作为静态属性
  tag: WorkTag, // 标识FunctionComponent/HostComponent/HostText等
  key: null | string,
  type: any, // 同ReactElement
  elementType: any, // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
  stateNode: any, // 存dom节点
  index: number,
  ref: any,
  refCleanup: null | (() => void),

  // 用于组成Fiber树
  return: Fiber | null, // 父
  child: Fiber | null, // 子
  sibling: Fiber | null, // 右边第一个兄弟

  // 作为工作单元,保存本次更新造成的状态改变相关信息
  // 要更新的新props
  pendingProps: any,
  // 计算后的props
  memoizedProps: any,
  // 更新队列
  updateQueue: mixed,
  // 计算出的新状态
  memoizedState: any,
  // 保存context、事件相关内容
  dependencies: Dependencies | null,
  mode: TypeOfMode,

  // 副作用
  flags: Flags,
  // 子树的所有节点的flags
  subtreeFlags: Flags,
  // 要删除的子fiberNode
  deletions: Array<Fiber> | null,
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  // 用于调度 优先级
  lanes: Lanes,
  childLanes: Lanes,

  // 指向对应的workInProgress FiberNode或者current FiberNode
  alternate: Fiber | null,

他们三者关系如下:

// ReactComponent:函数组件
const App = () => {
  // ReactElement:jsx语法会被转化为createElement方法
  return <h1>hello! react18</h1>
}
// ReactElement
const element = <App />

// FiberNode: 
// <App /> 和 <h1>hello! react18</h1> 都有与之对应的FiberNode

Fiber树

function App() {
  return (
    <Header>
      <img />
      <span>hello! react18</span>
    </Header>
  );
}

function Header({ children }) {
  return <header>{children}</header>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

这段JSX代码运行后会生成一个如下所示的 Fiber树,其中有两个特殊的节点:

  1. fiberRootNode:整个 Fiber树 的根结点,此节点的current属性指向的就是current Fiber树,对应的还有workInprogress Fiber树,下面会解释。
  2. hostRootFiber:挂载 React 应用的 dom 对应的fiberNode
我对React18 Fiber架构的理解 有一点需要说明的是,span下面的`TextNode`没有对应的`fiberNode` ,这是 React 的一条优化路径,**只有唯一文本节点的`fiberNode`就不会再生成子`fiberNode`了。**

❓:我曾有一个疑问,在上述这段JSX结构中,Header 组件下还有img和span节点,函数内部又有header节点,那么 Header 的fiberNode.child指向的是img的fiberNode呢还是内部的header的呢?

✅:指向的是header的,img和span会存到Header组件的props.children中,如果我渲染了children,那么img和span也会生成对应的fiberNode,并且在 Fiber树中的节点位置会是渲染的位置,并不是定义时的位置;如果不渲染,那也就不会生成与之对应的fiberNode。这也说明了FiberNode是Fiber架构运行时动态的工作单元,并不像ReactElement一样只是静态结构。

两颗Fiber树(双缓存)

UI = f(state)

当下大多数前端框架的实现原理就是这个公式,状态改变引起UI改变,UI因宿主环境的不同而不同,在浏览器中就是dom。

React是一个应用级框架,意思是当状态改变时,React不知道这个改变会导致哪些fiberNode改变,所以要从根节点开始找这个改变的状态与UI的对应关系。与之对应的是组件级框架,比如Vue,Vue的实现方式可以在状态改变时就定位到与改变对应的组件,然后从组件中寻找与改变对应的UI。

对于React来说,寻找state和UI对应关系的大致流程是从根节点开始遍历Fiber树,一路上标记发生改变的fiberNode,最终再将改变映射到真实UI上。

为了不影响到当前显示使用的Fiber树(名为current Fiber树),在映射到真实UI之前React的活动都是在内存中进行的,内存中也存在一个Fiber树,名为workInProgress Fiber树,两颗Fiber树之间互相以alternate属性连接。工作完成后根节点的current属性就会指向workInProgress Fiber树workInProgress Fiber树current Fiber树位置就互换了,这种技术也被称为双缓存,简而言之就是后台工作,完成以后前台后台位置互换。

工作流程

在初始化挂载(mount)时,React会先创建根节点fiberRootNode,然后从根节点开始创建wip Fiber树(workInProgress简称);

在更新发生(update)时,React会从发生动作的fiberNode开始向上找到根节点(fiberRootNode.stateNodehostRootFiber),然后从根节点开始生成新的wip Fiber树,当然此时的生成不是从0开始创建,会根据一些条件复用或删除已存在的wip Fiber树中的某些节点。

wip Fiber树构建过程中会根据改变打上对应的标记,比如插入或删除标记。构建完成后会遍历wip,根据不同的标记进行操作,最终将对应的UI渲染到页面上。

这就是大致的工作流程,这个过程中主要有两个大的阶段:render阶段和commit阶段

render

开始工作前会先找到div#root对应的fiberNode,称为hostRootFiber,然后开始生成wip Fiber树

这个过程分为两个部分:beginWorkcompleteWork

这是一个深度优先遍历的过程:先从hostRootFiber开始向下以深度优先的方式遍历到每个fiberNode,执行beginWork方法,当遍历到叶子节点(fiberNode.child===null)时,再调用completeWork方法,然后判断该fiberNode是否存在兄弟节点(fiberNode.sibling!==null),如果存在,则从兄弟节点开始继续向下遍历执行beginWork,如果不存在,就向上遍历一个节点执行completeWork,以此往复直到整颗wip Fiber树生成完成。

用一张图展示这个流程,还是上面一样的代码

function App() {
  return (
    <Header>
      <img />
      <span>hello! react18</span>
    </Header>
  );
}

function Header({ children }) {
  return <header>{children}</header>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
我对React18 Fiber架构的理解

beginWork

该过程作用是为传入的wip fiberNode生成子fiberNode,并打上副作用标记,beginWork会打两种标记:

  1. Placement插入
  2. ChildDeletion删除

对于 <div> <span/> </div> ReactElement结构,当进入div的beginWork时,通过对比span的current fiberNode和span的reactElement,生成span对应的新的wip fiberNode

  1. 进入beginWork后,根据不同的fiberNode.tag进入不同的逻辑,这一步主要是为了获取fiberNode对应的子reactElement结构,不同类型节点的子reactElement会存到不同的地方,比如对于HostRoot来说是存到fiberNode.memorizedState,对于HostComponent是存到fiberNode.pendingProps.children,而对于FC(FunctionComponent简称)来说,来自于函数的返回值,所以说函数组件的函数就是在这个时候调的,通过renderWithHooks函数来调用FC,这个函数内除了返回函数组件的返回值,也做了其他和hooks有关的操作,这里先不谈。这里也就知道了,平时开发打断点调试时想找到函数组件调用位置时,在renderWithHooks打断点就可以。
  2. 获取到子reactElement后,将它连同wip fiberNode一起传给reconcileChildren进行reconcile的操作。这里会拿到wip fiberNode.alternate即与之对应的current fiberNode,根据update或mount进入不同的函数(current===null就是mount),reconcileChildrenFibersmountChildrenFibers,他们俩内部都调的reconcileChildrenFibers函数,区别是:是否会为生成的子fiberNode打副作用标记,根据给reconcileChildrenFibers传不同的参数(shouldTrackSideEffects)来区分。mount时不需要给子fiberNode打标记,这是 React 的一条优化路径,因为mount都是插入操作(Placement),这个时候只给根节点hostFiberNode打上标记,最终只需要处理一次Placement就可以将整个dom挂载到页面上。具体流程completeWork会提到。
  3. 进入reconcileChildrenFibers后,执行reconcile操作,根据传入的reactElement的不同类型进入不同逻辑,目的都是根据reactElement创建新的fiberNode,根据具体操作打上副作用标记,然后连接到wip Fiber树上。当然update时还要判断能不能复用current fiberNode,这里涉及到diff算法,根据单节点和多节点进入不同的逻辑,去判断更新前的哪些节点可以复用,不可复用的节点就要删除wip Fiber树上与current fiberNode对应的wip fiberNode,当然这里没有删除逻辑,只是打上ChildDeletion标记。
  4. 流程结束,给wip fiberNode生成了子 fiberNode,然后进入子 fiberNodebeginWork流程,继续生成它的子fiberNode
我对React18 Fiber架构的理解

completeWork

该过程是为了

  1. diff props 判断是否给传入的fiberNodeUpdate标记
  2. flags冒泡:将它所有的子fiberNodeflags冒泡到它身上,存到subtreeFlags中。通过subtreeFlags就可以快速得知该fiberNode所在子树上是否存在副作用需要执行。
  3. fiberNode创建dom,初始化事件监听器或其他内部属性

具体来看,根据wip fiberNode.tag的不同进入不同的逻辑,比如

  • HostRootFunctionComponent需要冒泡flags
  • HostComponentHostText则需要创建dom或者diff props来判断是否需要打Update标记

然后判断是进入sibling的beginWork还是父节点的completeWork 我对React18 Fiber架构的理解

commit

render 阶段完成后,fiberRootNode.finishedWork就存着以hostRootFiber为根节点并且带着flags的wip Fiber树

在commit阶段,会将各种副作用(flags)提交到宿主环境中。上面一直都没提React的调度功能,在Fiber架构运行过程中,render阶段是有可能会被高优先级任务打断的,但commit阶段一旦开始就会同步执行不会被打断。

整个阶段可以分为三个子阶段

  1. before mutation:执行dom操作前
  2. mutation:执行dom操作
  3. layout:执行dom操作后
我对React18 Fiber架构的理解

before mutation

深度优先遍历wip Fiber树,主要处理两种finerNode.tag

  1. ClassComponent,执行getSnapshotBeforeUpdate
  2. HostRoot,清空挂载的内容,方便mutation阶段渲染

mutation

深度优先遍历wip Fiber树,找到第一个不存在subtreeFlags的节点,类似render阶段的流程,先向下遍历,找到之后再向上遍历(执行对应的操作),然后如果存在sibling的话,继续从sibling节点开始向下遍历,如此往复。

上面所说的操作,根据flags类型的不同而不同,有三种需要处理的类型

  1. Placement:插入或移动,找到 parent 对应的dom和找到 sibling 对应的dom,再找到当前fiberNode对应的dom,然后插到parent的dom中,当前fiberNode对应的dom可能不止一个,也就是存在sibling,需要遍历插入。对于浏览器来说,会根据有没有sibling的dom来判断使用appendChild方法还是insertBefore方法。由于fiberNode不仅仅对应dom节点(HostComponent),还有可能是函数组件等,所以寻找fiberNode的parent、sibling、本身的对应的dom节点并不简单,尤其是寻找sibling,是一个比较耗时的操作。
  2. Update:对于HostComponentHostText来说可以处理style属性、innerHTML、直接文本节点还有其他属性的变化,FunctionComponent还需要调用useLayoutEffect的销毁函数。
  3. ChildDeletion:所有要删除的子fiberNode都保存在fiberNode.deletions中,遍历deletions执行操作,具体操作:
    1. 每一个fiberNode在删除逻辑中都要走深度优先遍历的过程遍历子树,因为除了卸载dom,还有其他工作要做,比如解绑ref,函数组件useEffect的销毁函数等。
    2. 找到当前要卸载fiberNode下的第一个dom,为了卸载。
我对React18 Fiber架构的理解

layout

到这个阶段dom更新已经完成,但是JS线程还未结束,页面还没有渲染。这里也是深度优先遍历wip Fiber树,根据fiberNode.tag走不同的逻辑,但是每一个节点都会更新ref:

  1. 对于ClassComponent,执行componentDidMout/UpdatesetState传入的第二个参数callback存在fiberNode.updateQueue中,这个时候调用。
  2. 对于FunctionComponent,执行useLayoutEffect
  3. 对于HostRootReactDOM.render(element, container, callback)传入的三个参数callback,这个时候调用。

这里也就可以知道,在上述所说调用的函数中可以访问到已经更新过的dom。 我对React18 Fiber架构的理解

流程结束

到现在整个Fiber架构渲染的完整流程就结束了,当某个fiberNode触发更新的时候,就会向上找到hostRootFiber,然后开始render阶段再到commit阶段。

当然这只是一个渲染流程,还没有加入调度阶段。React实现了一套基于lane模型的优先级算法,并在此基础上实现了批量更新、任务打断/恢复等特性。又基于这些特性实现了Concurrent SuspenseuseTransition等开发者可以直接使用的内容。

把整个流程放在一个流程图中

我对React18 Fiber架构的理解

立flag

这篇总结的内容是属于比较宏观的,很多细小的点都是一笔带过比如renconcile算法、多节点和单节点的Fiber子树的处理(能力也不足以支撑我写的太细😅),也由于调度系统还没有深入学习,所以本文没有提到调度相关内容。 后续有计划,着重于某些点写一下学习总结。除此之外,事件系统、Hooks架构等也都很值得总结。

转载自:https://juejin.cn/post/7211072055780573221
评论
请登录