React核心原理以及部分源码解读

笔者于今年开始接触并接手React项目的研发,为了进一步学习React并了解其原理源码,笔者开始学习React的原理以及源码,主要的学习途径为一些网站(主要是卡颂大佬的React技术揭秘),书籍,以及自己看源码等等,本文是笔者在学习过程记录的笔记,以及本人对于React的一些理解。阅读本文前请确保你已经非常熟悉React,并且有一定的踩坑经验。

React核心原理以及部分源码

React哲学

官网上对于React哲学的简介是:

"我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。"

因此对于React来说,如何构建快速响应的大型Web应用程序是核心。

React核心架构

React15架构

  • Reconciler(协调器)

    • 找出变化的组件
  • Renderer

    • 负责把变化的组件渲染到页面上

Reconciler(协调器)

先说说React渲染原理:

在React中触发组件更新的操作有(不包括React Hook):

this.setState,this.forceUpdate,ReactDOM.render

当有更新要发生时,Reconciler将开始工作,进行以下操作:

  • 1.执行函数组件或执行Class Components的render方法,将返回的JSX通过React.createElement转换成虚拟DOM。

  • 2.通过Diff算法将新旧虚拟DOM进行对比

  • 3.通过对比得到最小更新范围

  • 4.通知Renderer将本次的变更更新到真是DOM即页面上

Renderer(渲染器)

React官方文档对于渲染器的解释是:

渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。

Renderer做的事情非常简单,就是:在每次更新发生时,Renderer接到Reconciler通知,将变化的组件渲染在当前宿主环境。

PS:不同平台有不同的Renderer,浏览器平台是React.DOM,RN上是ReactNative,还有ReactArt用于渲染到Canvas,SVG等

缺点

对于Reconciler,mount的组件会调用mountComponent,update的组件会调用updateComponent。这两个方法都会递归更新子组件。可参考源码:mountComponent updateComponent

总所周知,递归无法被中断,所以当子组件很多,递归层级很深时,会耗时非常长,而对于用户来讲,每一帧的刷新时间差不多是16.6ms(1s/60FPS),所以当递归更新的时间超过这个时间时,视觉上就会卡顿。

其实要解决这个问题就需要异步更新,但是React15当时并不支持异步更新,因此React16更新了架构。

React16架构

React16架构中新增了Scheduler(调度器),所以新的架构分为:

  • Scheduler(调度器)

    • 调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)

    • 找出变化的组件
  • Renderer(渲染器)

    • 负责将变化的组件渲染到页面上

Scheduler(调度器)

调度器的作用就是当浏览器有剩余时间时通知我们,因此作为任务中断的标准,除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

ps: Scheduler是独立于React的库

Reconciler(协调器)

在React16中不在是用递归去更新,而是通过可中断的循环过程,每次循环都会调用shouldYield判断当前是否有剩余时间。部分源码如下:


/** @noinline */

function workLoopConcurrent() {

  // Perform work until Scheduler asks us to yield

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

    workInProgress = performUnitOfWork(workInProgress);

  }

}

但是仅此不作处理会导致,DOM渲染不完全。因此在React16中,Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记:


export type SideEffectTag = number;

// Don't change these two values. They're used by React Dev Tools.

export const NoEffect = /*                    */ 0b000000000000000;

export const PerformedWork = /*                */ 0b000000000000001;

// You can change the rest (and add more).

export const Placement = /*                    */ 0b000000000000010;

export const Update = /*                      */ 0b000000000000100;

export const PlacementAndUpdate = /*          */ 0b000000000000110;

export const Deletion = /*                    */ 0b000000000001000;

export const ContentReset = /*                */ 0b000000000010000;

export const Callback = /*                    */ 0b000000000100000;

export const DidCapture = /*                  */ 0b000000001000000;

export const Ref = /*                          */ 0b000000010000000;

export const Snapshot = /*                    */ 0b000000100000000;

export const Passive = /*                      */ 0b000001000000000;

export const PassiveUnmountPendingDev = /*    */ 0b010000000000000;

export const Hydrating = /*                    */ 0b000010000000000;

export const HydratingAndUpdate = /*          */ 0b000010000000100;

// Passive & Update & Callback & Ref & Snapshot

export const LifecycleEffectMask = /*          */ 0b000001110100100;

// Union of all host effects

export const HostEffectMask = /*              */ 0b000011111111111;

// These are not really side effects, but we still reuse this field.

export const Incomplete = /*                  */ 0b000100000000000;

export const ShouldCapture = /*                */ 0b001000000000000;

export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000;

// Union of side effect groupings as pertains to subtreeTag

export const BeforeMutationMask = /*          */ 0b000001100001010;

export const MutationMask = /*                */ 0b000010010011110;

export const LayoutMask = /*                  */ 0b000000010100100;

只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。

因此,并中断的只有Scheduler通知Reconciler以及Reconciler产生虚拟DOM以及打标记的过程,并不会中断Renderer的渲染,因此不会造成不完全的DOM。

React Fiber

从React16开始采用了新的Reconciler,新的Reconciler采用了Fiber架构。

如何理解Fiber?先简单做一个定义:

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

其中每个任务更新单元为React Element对应的Fiber节点。

Fiber含义:

  • 1.作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。

  • 2.作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。

  • 3.作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

Fiber结构:

先来看看Fiber的源码:


function FiberNode(

  tag: WorkTag,

  pendingProps: mixed,

  key: null | string,

  mode: TypeOfMode,

) {

  // Instance 当前节点的一些静态属性

  // Fiber对应组件的类型 Function/Class/Host...

  this.tag = tag;

  this.key = key;

  this.elementType = null;

  // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName

  this.type = null;

  // Fiber对应的真实DOM节点

  this.stateNode = null;

  // Fiber  连接其他节点,形成Fiber树

  // 指向父级Fiber节点

  this.return = null;

  // 指向子Fiber节点

  this.child = null;

  // 指向右边第一个兄弟Fiber节点

  this.sibling = null;

  this.index = 0;

  this.ref = null;

  // 作为调度任务的信息属性

  // 保存本次更新造成的状态改变相关信息

  this.pendingProps = pendingProps;

  this.memoizedProps = null;

  this.updateQueue = null;

  this.memoizedState = null;

  this.dependencies = null;

  this.mode = mode;

  // Effects

  // 保存本次更新会造成的DOM操作

  this.effectTag = NoEffect;

  this.subtreeTag = NoSubtreeEffect;

  this.deletions = null;

  this.nextEffect = null;

  this.firstEffect = null;

  this.lastEffect = null;

  // 调度优先级相关

  this.lanes = NoLanes;

  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber

  this.alternate = null;

}

Fiber执行机制

双缓存Fiber树:

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

并且二者通过一下方式链接:


currentFiber.alternate === workInProgressFiber;

workInProgressFiber.alternate === currentFiber;

Fiber在渲染更新中的切换机制是:

React应用的根节点通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。

即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。

每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。

这部细节可以看卡颂大佬的文章。

JSX本质

在React中,JSX在编译时会被Babel编译为React.createElement。当然也可以指定编译为其他函数,因此并不是只有React才能使用JSX,可以通过@babel/plugin-transform-react-jsx (opens new window)插件显式告诉Babel编译时需要将JSX编译为什么函数的调用。

React.createElement

先来看看代码:


export function createElement(type, config, children) {

  let propName;

  const props = {};

  let key = null;

  let ref = null;

  let self = null;

  let source = null;

  if (config != null) {

    // 将 config 处理后赋值给 props

    // ...省略

  }

  const childrenLength = arguments.length - 2;

  // 处理 children,会被赋值给props.children

  // ...省略

  // 处理 defaultProps

  // ...省略

  return ReactElement(

    type,

    key,

    ref,

    self,

    source,

    ReactCurrentOwner.current,

    props,

  );

}

const ReactElement = function(type, key, ref, self, source, owner, props) {

  const element = {

    // 标记这是个 React Element

    $$typeof: REACT_ELEMENT_TYPE,

    type: type,

    key: key,

    ref: ref,

    props: props,

    _owner: owner,

  };

  return element;

};

可以看到React.createElement接受三个参数,第一个是type,一般指标签的类型,自定义组件等等,第二个是config指标签和组件上的props属性,第三个是children代表子节点,子组件。最终React.createElement返回的也就是ReactElement。

可以看到ReactElement上是没有之前提到的Fiber的一些信息:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer的标记

因此,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。

在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。

Render阶段

在render阶,如果是同步更新会调用performSyncWorkOnRoot,如果是异步更新则会调用performConcurrentWorkOnRoot


// performSyncWorkOnRoot会调用该方法

function workLoopSync() {

  while (workInProgress !== null) {

    performUnitOfWork(workInProgress);

  }

}

// performConcurrentWorkOnRoot会调用该方法

function workLoopConcurrent() {

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

    performUnitOfWork(workInProgress);

  }

}

二者的区别只有如果是异步更新,会在其中加中断条件shouldYield,如果当前浏览器帧没有剩余时间,会终止循环,等到浏览器空闲时继续遍历。

workInProgress代表当前已创建的workInProgress fiber。

performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。

Render流程

  • 1.首先会从rootFiber开始进行深度优先遍历,对于每一个遍历的Fiber节点,都会调用beginWork方法。

  • 2.beginWork方法根据传入的Fiber节点,创建子Fiber节点,并且将两个节点连接起来。

  • 3.当遍历到叶子节点,会开始执行completeWork处理节点。

    • 当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),则对其兄弟节点进行beginWork

    • 如果不存在兄弟Fiber,对父节点调用completeWork

    • 最终会回到rootFiber节点,至此整个render阶段就结束了

beginWork

根据 beginWork 源码。

首先,beginWork 接三个参数:

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

  • workInProgress:当前组件对应的Fiber节点

  • renderLanes:和渲染优先级相关


function beginWork(

  current: Fiber | null,

  workInProgress: Fiber,

  renderLanes: Lanes,

): Fiber | null {

  // ...省略函数体

}

根据之前讲得双缓存Fiber树,假如组件是第一次mount那么beginWorkcurrent参数就是null,因此beginWork通过判断current===null来区组件是mount还是update

代码逻辑如下:


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:

      // ...省略

    // ...省略其他类型

  }

}

所以可以把beginWork的工作分为两个部分:

update时:如果current存在,且满足一定条件时,复用current节点,克隆current.child作为workInProgress.child。

mount时:除fiberRootNode以外,根据fiber.tag不同,创建不同类型的子Fiber节点。

update

update部分代码逻辑如下:


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;

  }

可以看到,当满足:

  • 1.oldProps === newProps && workInProgress.type === current.type 即props与fiber.type都不变

  • 2.!includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够。

如果满足上述条件,didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)。

mount

当不满足update条件时会走mount,其代码逻辑如下:


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

switch (workInProgress.tag) {

  case IndeterminateComponent:

    // ...省略

  case LazyComponent:

    // ...省略

  case FunctionComponent:

    // ...省略

  case ClassComponent:

    // ...省略

  case HostRoot:

    // ...省略

  case HostComponent:

    // ...省略

  case HostText:

    // ...省略

  // ...省略其他类型

}

对于常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren方法。

reconcileChildren

reconcileChildrenReconciler(协调器)最核心的方法,它主要做了以下工作:

  • 1.对于mount的组件,创建新的子Fiber节点

  • 2.对于update的组件,会使用Diff算法对比当前的Fiber和上次更新的Fiber节点,通过比较的结果生成新的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,

    );

  }

}

跟beginWork一样,reconcileChildren也是通过current === null ?区分mount与update。

不论走哪个逻辑,最终他会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值 (opens new window),并作为下次performUnitOfWork执行时workInProgress的传参 (opens new window)。

reconcileChildFibers会为生成的Fiber节点带上effectTag属性。

effectTag

上面Reconcilerrender阶段是在内存中进行的,当这个工作完成之后,会通知Renderer去执行真正的DOM操作,而执行DOM操作的具体类型就保存在fiber.effectTag中。exp:


// DOM需要插入到页面中

export const Placement = /*                */ 0b00000000000010;

// DOM需要更新

export const Update = /*                  */ 0b00000000000100;

// DOM需要插入到页面中并更新

export const PlacementAndUpdate = /*      */ 0b00000000000110;

// DOM需要删除

export const Deletion = /*                */ 0b00000000001000;

那么,如果要通知Renderer将Fiber节点对应的DOM节点插入页面中,需要满足两个条件:

1.fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点

2.(fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag

我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?

针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。

第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。

为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。

beginWork流程图

beginWork.png

completeWork

上面讲了,render阶段会经历beginWork与completeWork这两部分工作,组件执行beginWork后会创建子Fiber节点,节点上可能存在effectTag,接下来看看completeWork做什么工作吧。

可以先看看completeWork 源码

部分代码如下:


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是原生DOM组件对应的Fiber节点。

处理HostComponent

和beginWork一样,还是根据current === null 判断是mount还是update。

但是如果是update还需要考虑workInProgress.stateNode != null(即该Fiber节点是否存在对应的DOM节点)

update

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

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

  • 处理style prop

  • 处理DANGEROUSLY_SET_INNER_HTML prop

  • 处理children prop

最主要的逻辑是调用updateHostComponent方法。


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

  // update的情况

  updateHostComponent(

    current,

    workInProgress,

    type,

    newProps,

    rootContainerInstance,

  );

}

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。


workInProgress.updateQueue = (updatePayload: any);

mount

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

}

至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

effectList

effectTag是commit阶段操作DOM的依据,因此在render的递归阶段,会记录这些effect,保存在effectList当中,避免做重复的操作。

在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。

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

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


                      nextEffect        nextEffect

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

在commit阶段只需要遍历effectList就能执行所有effect了。

completeWork流程图

completeWork.png

Commit阶段

Renderer工作的阶段被称为commit阶段。commit阶段可以分为三个子阶段:

before mutation阶段(执行DOM操作前)

mutation阶段(执行DOM操作)

layout阶段(执行DOM操作后)

before mutation阶段

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,先看看代码:


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

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

为此,React提供了替代的生命周期钩子getSnapshotBeforeUpdate。

getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题。

调度useEffect

scheduleCallback方法由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。


// 调度useEffect

if ((effectTag & Passive) !== NoEffect) {

  if (!rootDoesHavePassiveEffects) {

    rootDoesHavePassiveEffects = true;

    scheduleCallback(NormalSchedulerPriority, () => {

      // 触发useEffect

      flushPassiveEffects();

      return null;

    });

  }

}

被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects。

如何异步调度?

在flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList。

effectList中保存了需要执行副作用的Fiber节点。其中副作用包括

  • 插入DOM节点(Placement)

  • 更新DOM节点(Update)

  • 删除DOM节点(Deletion)

当一个FunctionComponent含有useEffect或useLayoutEffect,他对应的Fiber节点也会被赋值effectTag。

useEffect异步调用分为三步:

  • 1.before mutation阶段在scheduleCallback中调度flushPassiveEffects

  • 2.layout阶段之后将effectList赋值给rootWithPendingPassiveEffects

  • 3.scheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects

useEffect异步执行的原因主要是防止同步执行时阻塞浏览器渲染。

mutation阶段

类似before mutation阶段,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


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;

  }

}

从代码中路由看出,commitMutationEffects会遍历effectList,对每个Fiber节点执行如下三个操作:

  • 1.根据ContentReset effectTag重置文字节点

  • 2.更新ref

  • 3.根据effectTag分别处理,其中effectTag包括(Placement(插入) | Update(更新) | Deletion(删除) | Hydrating(服务端渲染))

Placement effect

当Fiber节点含有Placement effectTag,调用commitPlacement该Fiber节点对应的DOM节点需要插入到页面中。

commitPlacement的方法主要分为三步:

  • 1.获取父级DOM节点

    
    const parentFiber = getHostParentFiber(finishedWork);  // 父级DOM节点  const parentStateNode = parentFiber.stateNode;
    
    

    finishedWork为传入的Fiber节点

  • 2.获取Fiber节点的DOM兄弟节点

    
    const before = getHostSibling(finishedWork);
    
    
  • 3.根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作。

    
    // parentStateNode是否是rootFiber
    
    if (isContainer) {
    
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
    
    } else {
    
    insertOrAppendPlacementNode(finishedWork, before, parent);
    
    }
    
    

Update effect

当Fiber节点含有Update effectTag,调用commitWork根据Fiber.tag分别处理。

对于FunctionComponent,也就是当fiber.tag为FunctionComponent,会调用commitHookEffectListUnmount。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数。

对于HostComponent,fiber.tag为HostComponent(原生html标签),会调用commitUpdate。

最终会在updateDOMProperties (opens new window)中将render阶段 completeWork (opens new window)中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。

Deletion effect

当Fiber节点含有Deletion effectTag,会调用commitDeletion,把对应的DOM删除。

commitDeletion主要执行以下操作:

  • 递归调用Fiber节点及其子孙Fiber节点中fiber.tag为ClassComponent的componentWillUnmount (opens new window)生命周期钩子,从页面移除Fiber节点对应DOM节点

  • 解绑ref

  • 调度useEffect的销毁函数

layout 阶段

layout阶段也是遍历effectList,执行commitLayoutEffects函数。


root.current = finishedWork;

nextEffect = firstEffect;

do {

  try {

    commitLayoutEffects(root, lanes);

  } catch (error) {

    invariant(nextEffect !== null, "Should be working on an effect.");

    captureCommitPhaseError(nextEffect, error);

    nextEffect = nextEffect.nextEffect;

  }

} while (nextEffect !== null);

nextEffect = null;

commitLayoutEffects


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;

  }

}

从代码中可以看到就做了两件事:

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

  • 2.commitAttachRef(赋值 ref)

commitLayoutEffectOnFiber

commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理。

  • 对于ClassComponent,他会通过current === null?区分是mount还是update,调用componentDidMount 或componentDidUpdate。

  • 触发状态更新的this.setState如果赋值了第二个参数回调函数,也会在此时调用。

    
    this.setState({ xxx: 1 }, () => {
    
    console.log("i am update~");
    
      });// 立马打印i am update~
    
    
  • 对于FunctionComponent及相关类型,他会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数。


  switch (finishedWork.tag) {

    // 以下都是FunctionComponent及相关类型

    case FunctionComponent:

    case ForwardRef:

    case SimpleMemoComponent:

    case Block: {

      // 执行useLayoutEffect的回调函数

      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

      // 调度useEffect的销毁函数与回调函数

      schedulePassiveEffects(finishedWork);

      return;

    }

mutation阶段会执行useLayoutEffect hook的销毁函数。

结合这里我们可以发现,useLayoutEffect hook从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行的。

而useEffect则需要先调度,在Layout阶段完成后再异步执行。

以上useLayoutEffect与useEffect的区别。

对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。

即ReactDom.render函数的第三个参数


ReactDOM.render(<App />, document.querySelector("#root"), function() {

  console.log("i am mount~");

});

commitAttachRef


function commitAttachRef(finishedWork: Fiber) {

  const ref = finishedWork.ref;

  if (ref !== null) {

    const instance = finishedWork.stateNode;

    // 获取DOM实例

    let instanceToUse;

    switch (finishedWork.tag) {

      case HostComponent:

        instanceToUse = getPublicInstance(instance);

        break;

      default:

        instanceToUse = instance;

    }

    if (typeof ref === "function") {

      // 如果ref是函数形式,调用回调函数

      ref(instanceToUse);

    } else {

      // 如果ref是ref实例形式,赋值ref.current

      ref.current = instanceToUse;

    }

  }

}

很显然,就是做了获取DOM实例,更新ref的操作。

current Fiber树切换

在layout之前会执行root.current = finishedWork

componentWillUnmount会在mutation阶段执行。此时current Fiber树还指向前一次更新的Fiber树,在生命周期钩子内获取的DOM还是更新前的。

componentDidMount和componentDidUpdate会在layout阶段执行。此时current Fiber树已经指向更新后的Fiber树,在生命周期钩子内获取的DOM就是更新后的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容