(万字长文)React 18 源码与原理解读 —— 看这一篇就够了

news2024/11/15 17:56:13

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

  4. 本教程是基于 React v18.1.0 版本的解读,如果后续版本 React 的机制有了一定的更新迭代,这个教程可能会缺乏时效性

本一节的内容

本节是整个React 的源码与原理解读解读教程的最后一章,作者会用自己的理解带着大家梳理一下整个 React 的运行过程,因为篇幅的问题,作者会主要讲述一下一下重点的内容和要点的信息,侧重给读者一个完整的运行逻辑,对于讲解不到位的部分可以阅读之前的详细教程,教程肯定还是会有疏漏的部分请大家海涵

前置基础知识

Fiber架构

Fiber 是 React 16.x 开始新增的一个数据结构,React 将每个节点都封装到了一个 Fiber 中,使得整个 DOM 树的渲染任务被分成了一个一个小片,每个 Fiber 中通过这样的指针相互联系 ,最后形成一个链表树的结构:

return: Fiber | null,   //表述一个元素的父亲
child: Fiber | null,    //表述一个元素的第一个孩子
sibling: Fiber | null,  //表述一个元素的兄弟
index: number,          //在兄弟节点中的位置

请添加图片描述

如果你想了解 Fiber 的详细数据结构可以看这篇 React 的源码与原理解读(二):Fiber 与 Fiber 链表树

引入 Fiber 架构的好处是:

  • 在引入它之前,React 每个更新需要遍历整个数据结构,当一个页面比较复杂的时候,js 主线程就不得不停止其他工作开始进行渲染的逻辑,用户的点击输入等交互事件、页面动画等都不会得到响应当。

  • 引入 Fiber 后,React 会从根节点开始一个一个去更新每一个 Fiber ,每当处理完一个 Fiber ,在处理下一个 Fiber 之前,js 可以转而去处理优先级更高、更需要快速响应的任务,因为 Fiber 中有指向孩子和兄弟的指针,所以在处理完紧急任务后可以很容易的找到下一个需要遍历的节点,从而继续我们的任务。

双缓冲架构

我们的 React 中,存在两颗上述的 Fiber 链表树,一颗是用于渲染页面的 current Fiber 树,一颗是 workInProgress Fiber 树,我们用于渲染当前页面的是 current Fiber 树,而我们在整个更新过程中会构建一颗叫做 workInProgress Fiber 树。

使用双缓冲树的原因是:

  • 上面提到了,我们操作 Fiber 的过程是可能被中断的,所以我们不能直接更新当前的 Fiber ,这样可能会产生更新了一部分另一部分没更新的操作
  • 在遍历 Fiber 树过程中,我们大量遇到节点不需要更新的情况出现,此时我们可以直接复用另一颗树上对应的 Fiber 来提升系统的性能和效率,复用 也是我们 React Diff 算法的精髓之一

在整个架构的顶部,有一个 FiberRootNode 节点,它存储了一些运行过程中的标志和数据等,它最重要的作用是通过 current 属性指向了当前工作的 Fiber 树的根,当我们的 workInProgress Fiber 生成完毕后,我们通过切换 current 属性的指向可以直接切换我们的整个工作树,使得 current 树和 workInProgress 树实现交换:

请添加图片描述

Lanes

上面提到了,我们的 Fiber 操作过程是可以中断的,当有紧急事进入时,React 会优先处理紧急任务,而判断任务的紧急程度就需要一套指标,他就是 Lanes 系统:

lanes 使用31位二进制来表示优先级车道,共31条, 位数越小(1的位置越靠右)表示优先级越高。在实际使用中,我们会有一个 31位的二进制数来标识我们的任务,,如果他的某一位对应的是 1 ,那么他的某个优先级就有任务了,反之就是空闲的,你可以理解成我们把任务看成我一辆辆汽车,他们需要在不同的车道上行驶才不会相互影响,但是右边的车道可以向左边超车(优先级高)。这就是为什么我们称之为 车道模型

// lane使用31位二进制来表示优先级车道共31条, 位数越小(1的位置越靠右)表示优先级越高
export const TotalLanes = 31;

// 没有优先级
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

// 同步优先级,表示同步的任务一次只能执行一个,例如:用户的交互事件产生的更新任务
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;

// 连续触发优先级,例如:滚动事件,拖动事件等
export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;

// 默认优先级,例如使用setTimeout,请求数据返回等造成的更新
export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000010000;

// 过度优先级,例如: Suspense、useTransition、useDeferredValue等拥有的优先级(React18 的新机制)
const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /*                       */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /*                       */ 0b0000000001000000000000000000000;

// 重试车道
const RetryLanes: Lanes = /*                            */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /*                             */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /*                             */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /*                             */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /*                             */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /*                             */ 0b0000100000000000000000000000000;

export const SomeRetryLane: Lane = RetryLane1;
// 可选的车道
export const SelectiveHydrationLane: Lane = /*          */ 0b0001000000000000000000000000000;

//非空闲车道
const NonIdleLanes: Lanes = /*                          */ 0b0001111111111111111111111111111;
// 空闲车道
export const IdleHydrationLane: Lane = /*               */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /*                        */ 0b0100000000000000000000000000000;
// 屏幕外车道
export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

lanes 之所以这么设计,主要是方便我们使用位操作来实现一些比较、合并、重置等功能,因为我们可以使用或非和等操作方便的筛选、合并和删除对应的 lane,在 React 的每个阶段中,我们都需要通过 lane 进行一些判定操作。

如果你想详细的了解 Lanes 体系可以看这篇: React 的源码与原理解读(九):Lanes

React 的更新流程

在 React 从我们的代码生成我们的页面和更新我们的页面的过程中,分为 Schedule【调度】、Reconcile【协调】、commit【提交】三个阶段:

  • Schedule 阶段用于通过 lane 调度任务的优先级,使高优先级任务优先进入Reconcile,并且提供中断和恢复机制。

  • reconcile 阶段用于 fiber 树结构的更新,它深度优先递归了整个 React 结构树,为每一个节点创建 Fiber,通过 diff算法 和当前的结构进行对比,判断对应节点需要进行了新增,修改,删除或者直接复用,并在 Fiber 上给他们打上对应的标记

  • commit 阶段又称 render【渲染】阶段,顾名思义它做的就是把刚刚在 Fiber 上做的标记内容同步到对应的真实 DOM 中去,这个过程又分为三个阶段:before mutation 阶段 (dom 操作之前)、mutation 阶段 (dom 操作)、layout 阶段 (dom 操作之后)

从 JSX 代码开始

在了解了一些基础知识和 React 的整体运行层次后,我们来完整的梳理一次从我们的代码到最后成为 HTML 的过程中发生了什么,我们还是从我们编写的代码开始讲起:

在 React 中,对于用户而言,我们一般都编写对应的 jsx 代码来生成我们的组件

function App() {
  return <h1 class="test" key="122" >Hello World</h1>;
}

但是在 React 中 jsx 代码是不能直接被处理的, jsx 代码需要被转变为对应的 jsx 结构(也就是之前版本的 React.createElement 函数),这个过程由 Raect 和 babel 合作完成,jsx 函数的第一个参数是类型,第二个是配置项和子元素,第三个是 key

function App() {
  return jsx("h1", {
    class: "test",
    children: "Hello World"
  }, "122");
}

而 jsx 这个函数的作用是使用将传入的参数生成一个 ReactElement 结构,而这个结构可以被我们的 React 进行处理,关于这个 ReactElement 的结构的具体信息可以看这篇:React 的源码与原理解读(一):从创建React元素出发

export function jsx(type, config, maybeKey) {
  let propName;
  const props = {};

  let key = null;
  let ref = null;

  // 若设置了key,则使用该key
  if (maybeKey !== undefined) {
    if (__DEV__) {
      checkKeyStringCoercion(maybeKey);
    }
    key = '' + maybeKey;
  }

  // 若config中设置了key,则使用config中的key
  if (hasValidKey(config)) {
    if (__DEV__) {
      checkKeyStringCoercion(config.key);
    }
    key = '' + config.key;
  }

  // 提取设置的ref属性
  if (hasValidRef(config)) {
    ref = config.ref;
  }

  // 剩余属性将添加到新的props对象中
  for (propName in config) {
    if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
      props[propName] = config[propName];
    }
  }

  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  return ReactElement(type, key, ref, undefined, undefined, ReactCurrentOwner.current, props);
}

Render 挂载

之后,当我们编写完了我们的代码后,我们会把它挂载到我们的 Root 上,类似于这样:

import ReactDOM from 'react-dom/client';
import App from './App';

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

createRoot 这个函数的作用是:

  • 创建我们的 FiberRootNode 节点,FiberRootNode 我们在上文提过了它用于指向我们正在运行的 Fiber 树,由于我们提供了用于挂载的 DOM 节点,那么我们的 FiberRootNode 就可以完成初始化了,它会指向我们的提供的 DOM 所生成的 Fiber ,同时,我们用一个随机的标记来唯一标识 DOM 已经被使用了,这样当有其他的组件被挂载在这个 root 上时会被报错。
  • 获取并监听该 DOM 的所有事件,在监听的过程中,通过 getEventPriority 这个函数,将传入的事件根据类别设定了 Lane ,而这些 Lane 会在后续调度的时候发挥作用。
  • 最后返回一个 ReactDOMRoot 对象,它提供一个 render 和一个 unmount 函数,用于我们挂载和卸载我们的组件, render 中有一个 updateContainer 函数,这个函数中完成我们整个组件的解析和生成 DOM 树的逻辑

想要了解这部分的详细逻辑可以看这篇:React 的源码与原理解读(三):从 Render 开始,理解元素的挂载和解析

更新列表 updateQueue

因为后面需要进入我们的 Schedule 调度阶段,我们先来讲讲一些它需要的前置内容:

因为 Schedule 主要负责任务的调度,我们需要将我们所有的更新任务保存起来,这些任务被称为 update,它的数据结构如下:

const update: Update<*> = {
  eventTime, // 当前操作的时间
  lane, // 优先级
  tag: UpdateState, // 执行的操作
  payload: null,   //更新挂载的数据
  callback: null,   //更新的回调函数
  next: null, // next指针
};

因为在进入 Schedule 的时候,由他来决定我们的谁会被调度,还会决定任务中断和继续,所以我们就需要一个数据结构来保存这些任务,,他就是 updateQueue 更新队列,每一个 fiber 节点都有一个 updateQueue 的更新队列,它的数据结构如下:

export type UpdateQueue<State> = {|
  baseState: State,                       // 当前 state (props)
  firstBaseUpdate: Update<State> | null,  // 上次渲染时遗的链表头节点
  lastBaseUpdate: Update<State> | null,   // 上次渲染时遗的链表尾节点
  shared: SharedQueue<State>,             // 本次渲染时要执行的任务,
  effects: Array<Update<State>> | null,   // 有回调函数的update
};

其中有两个链表,一个 firstBaseUpdatelastBaseUpdate 组成的链表,也就是遗留链表,因为我们每次调度时都会判断当前任务是否有足够的优先级来执行,若优先级不够,则重新存储到链表中,用于下次渲染时重新调度;一个是 shared 中存储的更新链表,里面存放了本次需要执行的任务。

对应的我们使用 enqueueUpdate 函数将我们的 update 函数添加到 UpdateQueue 中;使用 processUpdateQueue 来执行其中可以执行的任务,这两个函数我们在之后的运行过程中会遇到

关于这个 updateQueue 的详细内容可以参考这篇:React 的源码与原理解读(十):updateQueue 与 processUpdateQueue

初始化时创建更新

有了更新相关的基础知识,在我们可以开始解读 updateContainer 这个函数了:

它首先获取了我们的根 Fiber,它在之前的 render 挂载中被创建,

之后创建了一个更新然后把它放到了根 Fiber 的更新列表中,

之后调用 scheduleUpdateOnFiber 对这个任务进行调度

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // 获取 FiberRootNode 的根节点
  const current = container.current;
    
  //创建一个更新
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(current);
  const update = createUpdate(eventTime, lane);
  update.payload = { element };

  // 处理 callback,不过从React18开始,render不再传入callback了,即这里的if就不会再执行了
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  //把创建的添加到 current 的更新链表中
  enqueueUpdate(current, update, lane);

  //开始在 fiber 上进行调度
  const root = scheduleUpdateOnFiber(current, lane, eventTime);
  if (root !== null) {
    entangleTransitions(root, current, lane);
  }
  return lane;
}

scheduleUpdateOnFiber 函数中,首先我们将这个更新的 lane 合并到 fiber 上,之后寻找这个节点的父节点的 Fiber,自底向上,把这个 lane 放到 childLanes 中,直到同步到根节点为止,之后在 FiberRootNode 中标记这个节点需要更新,最后调用 ensureRootIsScheduled 正式进行调度

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
): FiberRoot | null {
  // 检查是否有循环更新
  checkForNestedUpdates();
  // 自底向上更新整个优先级
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }
  // 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中
  markRootUpdated(root, lane, eventTime);  
  // 注册调度任务, 由 Scheduler 调度, 进行 Fiber 构造
  ensureRootIsScheduled(root, eventTime);
  return root;
}

状态改变时触发更新

除了在初次挂载的时候需要创建更新,我们在页面的内容发生改变的时候,比如 setState 、forceUpdate 等,也会触发更新,当这些 API被触发的时候,他们都会调用 enqueueSetState 函数来创建一个更新,不过根据触发的 API 不同,这些更新会带上不一样的 tag,而从产生不一样的运行效果,enqueueSetState 的运行机制和 updateContainer 大体相同:

Component.prototype.setState = function(partialState, callback) {
  // ... 省略
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

enqueueSetState(inst, payload, callback) {
    // 获得 fiber
    const fiber = getInstance(inst);
    // 获取当前事件触发的时间
    const eventTime = requestEventTime();
    // 获取到当前事件对应的 Lane
    const lane = requestUpdateLane(fiber);
    // 创建更新对象
    const update = createUpdate(eventTime, lane);
    // 挂载
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      //省略 DEV 模式代码
      update.callback = callback;
    }
	// 将更新对象添加进更新队列中
    enqueueUpdate(fiber, update, lane);
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    if (root !== null) {
      entangleTransitions(root, fiber, lane);
    }  
    // ....
    if (enableSchedulingProfiler) {
      markStateUpdateScheduled(fiber, lane);
    }
},

Scheduler 调度更新

ensureRootIsScheduled 函数中,我们决定了接下来哪一个任务将会被调度:

  • 它首先调用了 markStarvedLanesAsExpired 来判断是不是有即将过期的任务,它会先根据任务的 lane 来设定过期时间,然后找出有没有过期任务,如果有则设置高优先级,这样做的目的的防止饥饿
  • 之后通过 getNextLanesgetHighestPriorityLane 选择我们需要的执行的任务中优先级最高的
  • 如果现在有执行的任务,判断这个正在执行的任务和新选出的任务哪个优先级更高,如果新任务优先级更高,则取消现在的任务,执行新任务
  • 如果执行新任务,先判定我们是不是同步优先级,如果是同步模式我们直接走同步模式进行 reconcile 过程,此模式下调度的任务是不可以被打断的,有这几种情况下是使用的同步模式:1. 任务很紧急的,比如饥饿的任务不能在被延期了 2. 老版本的 React 不支持并发模式的或者没有开始并发模式的就会同步渲染
  • 如果不是同步优先级,我们走并发模式进行 reconcile 过程,它可以在运行过程中被打断,我们通过 lanesToEventPriority 将我们的 lane 优先级转为调度的优先级,传入我们的调度函数中,在最新版本的 React 中,默认是使用并发模式进行渲染的。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;

  /**
   * 判断 pendingLanes 中的任务,是否有过期的,
   * 若该任务没有设置过期时间,则根据该任务的lane设置过期时间,
   * 若任务已过期,则将该任务放到 expiredLanes 中,表示马上就要执行,
   * 在后续任务执行中以同步模式执行,避免饥饿问题
   */
  markStarvedLanesAsExpired(root, currentTime);
  // 确定本次执行的 lanes
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  // 如果nextLanes为空则表示没有任务需要执行,则直接中断更新
  if (nextLanes === NoLanes) {
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }
  // 获取这批任务中优先级最高的一个
  const newCallbackPriority = getHighestPriorityLane(nextLanes);
  const existingCallbackPriority = root.callbackPriority;
  if (
    existingCallbackPriority === newCallbackPriority
  ) {
    //....
    // 若新任务的优先级与现有任务的优先级一样,则继续正常执行之前的任务
    return;
  }

  // 新任务的优先级大于现有的任务优先级,取消现有的任务的执行
  if (existingCallbackNode != null) {
    cancelCallback(existingCallbackNode);
  }
    
  // 开始调度任务,判断新任务的优先级是否是同步优先级
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    if (root.tag === LegacyRoot) {
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    //.....
    newCallbackNode = null;
  } else {
    //不是同步优先级,获取对应的事件优先级
    let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

scheduleCallback 中,我们根据是否配置了 delay(延迟) ,将任务分化到 taskQueue (可执行任务) 和 timerQueue(延时任务)两个队列中,然后根据优先级计算出任务的开始和过期时间;在可执行队列中,过期时间越早的任务优先级越高,因为它需要尽快被执行,而延时任务中,开始时间越早的优先级越高,因为它会尽快开始。

var taskQueue = [];
var timerQueue = [];

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();
  //任务开始调度的时间,options 是一个可选项,其中有一个 delay 属性,表示这是一个延时任务,要多少毫秒后再安排执行。         
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }
  //  timeout 跟优先级相互对应,表示这个任务能被拖延执行多久。
  var timeout;
  switch (priorityLevel) {
	//计算 timeout 
  }
  // expirationTime 表示这个任务的过期时间,这个值越小,说明越快过期,任务越紧急,越要优先执行。
  var expirationTime = startTime + timeout;
  // 创建一个任务,其 sortIndex 越小,在排序中就会越靠前
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  //如果有设置 delay 时间,那么它就会被放入 timerQueue 中,表示延期执行的任务;否则放入 taskQueue 表示现在就要执行的任务。
  if (startTime > currentTime) {
    // 更新 sortIndex 为开始时间,这样越晚的任务开始的任务优先级越低
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 更新 sortIndex 为过期时间,这样越紧急的任务优先级越高
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // 调度
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

在把任务放入我们的队列后,我们调用 requestHostCallback函数用于执行任务,它使用了 MessageChannel 这个 API 来创建了一个宏任务,因为宏任务会在下次事件循环中执行调用 performWorkUntilDeadline 逻辑:

let isMessageLoopRunning = false;

function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}
// 初始化了一个 MessageChannel
const channel = new MessageChannel();
const port = channel.port2;
// 当我们调用 schedulePerformWorkUntilDeadline 的时候会触发 performWorkUntilDeadline
channel.port1.onmessage = performWorkUntilDeadline;
let schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};

let startTime = -1;
const performWorkUntilDeadline = () => {
   //.....
};

performWorkUntilDeadline 中则调用了 flushWork 函数通过 workLoop 函数执行我们的任务,这个函数的作用是:

  • 我们首先判断在当前时间下,有没有 timerQueue 队列的任务满足了执行时间,如果有的话,我们需要把他们放入到我们的 taskQueue 队列中等待调度
  • 之后我们的循环调度队列中的任务执行它,如果它将返回了一个回调函数则说明它被中断了,我们将这个回调函数给予我们当前的任务,等待下次继续执行;否则说明我们的任务执行完毕,我们从队列里面移除我们的任务
  • 我们循环执行我们的任务直到任务过期、没有任务或者 shouldYieldToHost 返回 true
  • 之后我们做最后的处理,如果还有任务,我们返回 true;否则我们找到下一个的延时队列里的任务(没有可执行任务了,但是还有延时任务需要去执行),调用 requestHostTimeout 函数重新开始我们的调用过程,这个函数就是我们延时任务的调度函数;如果找不到下一个任务,说明没有剩下的任务了,我们返回 false。
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // 判断 timerQueue 的 startTime 是不是到了,如果到了将它插入我们的 taskQueue 中
  advanceTimers(currentTime);
  // 弹出第一个任务
  currentTask = peek(taskQueue);
  //不断执行任务列表里的任务
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    // 判断是不是要退出本次任务执行
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    // 获取这个任务的内容
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      // 计算任务是不是过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        markTaskRun(currentTask, currentTime);
      }
      //获取任务函数的执行结果
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // 检查callback的执行结果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          markTaskYield(currentTask, currentTime);
        }
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // 任务做完了,抛出这个任务
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    // 下一个任务
    currentTask = peek(taskQueue);
  }
  if (currentTask !== null) {
    return true;
  } else {
    // 找到最近的延时任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

shouldYieldToHost 用于我们判定要不要中断我们一批任务的执行,把进程还给我们的浏览器。执行时间如果小于帧间隔时间(frameInterval,通常为 5ms),不需要让出进程,否则让出。关于这个内容的详细过程可以看这篇:React 的源码与原理解读(八):Scheduler

Reconciler 深度优先遍历 Fiber

在说完了我们任务的调度之后,我们来看看任务的执行,之前在 ensureRootIsScheduled 函数中,我们注册了 SyncWorkConcurrentWork 两类任务,他们分别调用了自己的performSyncWorkOnRootperformConcurrentWorkOnRoot 函数来执行任务,他们分别调用了一系列函数来处理,以 performSyncWorkOnRoot 为例子

  • 它调用了 renderRootConcurrent 函数,其中的逻辑是通过当前的 current 树来初始化一个 WorkInProgress 树,
  • 之后它调用了 workLoopConcurrent 函数,其中是我们的遍历逻辑, shouldYield 函数和上文的 shouldYieldToHost 一样,是判定任务是不是调用时间过长而应该被中断的,而 workLoopSync 中则没有这个逻辑,因为同步任务不能被打断
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

performUnitOfWork 中,我们调用了 beginWork 函数来创建新的 Fiber 节点,同时返回其孩子节点,如果它返回 null ,说明它没有孩子了;如果没有孩子节点,我们会调用completeUnitOfWork 函数处理,其循环判定当前节点是不是有兄弟节点,如果有则返回其兄弟节点,如果没有兄弟节点了,那么把当前节点设置为其父节点,然后继续循环,遍历的结束条件是当前节点不是空,也就是不是根节点。

function performUnitOfWork(unitOfWork: Fiber): void {
  //取出 current 树,传入的是 workInProgress 树的 root 节点,所以它的 alternate 指向的是 current 树的 root 节点
  const current = unitOfWork.alternate;

 //调用 beginWork 创建 Fiber 节点,它是遍历整个 element 后得到的下一个节点,第一次调用则是我们传入的 element 的根节点
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  //如果没有下一个节点了(遍历结束了),那么结束整个流程,否则将 workInProgress 更新为刚刚新建的那个节点
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return; // 该节点的父级节点
	//省略....
    //如果当前节点还有兄弟节点,获得兄弟
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      //继续执行我们的 beginWork
      workInProgress = siblingFiber;
      return;
    }
    //返回父节点
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  //遍历结束,标记遍历完成
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

performUnitOfWorkcompleteUnitOfWork 合作完成了一个深度优先搜索的逻辑,关于更详细的逻辑可以看这一篇文章:React 的源码与原理解读(四):updateContainer 内如何通过深度优先搜索构造 Fiber 树

Reconciler 生成 Fiber 节点

beiginWork 这个函数中,我们首先根据传入的 props 是不是相同,判定是不是可以复用我们之前的 Fiber,这也是我们使用双缓冲架构的原因,在强制更新模式和 props 不一样的情况下,标记为需要更新,否则标记为不需要更新,直接复用之前的节点,要注意如果是第一次调用生成则我们不存在 current 树,所以一定需要更新。在处理完更新逻辑后,我们对每一个不同类型的节点需要一个单独的处理:

  • 当节点是 HostRoot 类型的时候,我们则需要把 current 树的的 updateQueue 队列复制给 workInProgress 树,然后使用 processUpdateQueue 执行其中的更新,我们在之前创建更新的时候说明了,在 Update 的 payload 中存放了它指向的 element 元素,所以我们可以拿到之前的 element,在更新完毕后对比之前的 element 和更新完毕的 element,相同则复用,不同则使用新的 element 调用 reconcileChildren 来创建一个 Fiber
  • 当节点是 FunctionComponent 也就是函数组件,我们需要先挂载 hooks 和 context(这两个部分可以查看作者之前的 hooks 专题,有很详细的讲解),之后我们获取函数组件的函数,它在 type 中,如果可以复用则直接复用节点,如果不能在调用 renderWithHooks 运行我们函数组件的函数,将返回值传入 reconcileChildren 来生成我们的 Fiber
  • 当我们传入是类组件的时候,我们先通过 stateNode 属性获取到组件的实例,如果没有则创建一个实力,因为类组件本身就是一个类,显然可以 new 一个它的实例出来,之后我们通过 context、props 和 state 我们计算出新的 state 更新给节点,然后执行其生命周期例如 componentWillMount,在一切前置操作完毕后,通过实例的 render 方法获取到了其返回的 DOM 元素,传入 reconcileChildren 方法中。
  • 如果你传入一个原生的 HTML 节点,它会判定它是不是文本节点,如果是则停止后续的处理,否则传入 reconcileChildren;如果你传入的是文本节点,那么直接停止后续的处理。
  • 有一种特殊情况是,因为 React 中的函数组件有多种写法,有些形似函数组件的也可能是类组件,为了保险起见,函数组件会先被设定为 IndeterminateComponent,然后在对这个类型的处理中,会识别你写的组件是函数组件还是类组件。

这部分的逻辑总结来说就是,首先判断当前节点是不是复用:如果可以直接使用;否则先处理 state、hooks 等信息,然后调用 reconcileChildren 来生成我们的 Fiber 节点,因为篇幅很长,这里我们不具体展开了,感兴趣的可以去阅读这篇:React 的源码与原理解读(五):beginWork 分类处理逻辑

这部分的重点是 reconcileChildren 函数,其中调用了了 DIFF 算法来判断节点的更新,它的主要逻辑在 reconcileChildFibers 中,它处理我们放入的 element 结构,我们把他和 Fiber 树中同一层的 Fiber 进行比较,主要使用三个函数进行处理:

( PS: 要注意,下文我们所说的新增,删除和移动等操作,我们只是在 Fiber 上打上了标记,而不是真的进行了相关的操作,这些标记会在之后的 commit 进行具体的处理 )

reconcileSingleElement 用于处理一般的React组件,比如函数组件、类组件、html 标签等,

  • 首先提取出当前 element 的 key 属性,找到在 Fiber 树同一层中,有没有和他一个 key 的元素
  • 之后我们判断这个 key 相同的元素和当前元素的类型是不是一致,如果一致则复用这个元素
  • 如果没有匹配元素则创建一个新的节点
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  //当前节点的key
  const key = element.key;
  let child = currentFirstChild;
  // 循环检测一层中和当前节点的 key
  while (child !== null) {
    // 找到 key 相等的元素
    if (child.key === key) {
      const elementType = element.type;
      //元素类型相等
      if (child.key === key) {
        const elementType = element.type;
         //  REACT_FRAGMENT_TYPE,特判
        if (elementType === REACT_FRAGMENT_TYPE) {
          if (child.tag === Fragment) {
            deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点
            const existing = useFiber(child, element.props.children); // 该节点是fragment类型,则复用其children
            existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
            //Fragment没有 ref属性
            if (__DEV__) {
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;
          }
        } else {
          if (
            child.elementType === elementType ||
            (__DEV__
              ? isCompatibleFamilyForHotReloading(child, element)
              : false) ||
            (typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点
            const existing = useFiber(child, element.props); // 复用 child 节点和 element.props 属性
            existing.ref = coerceRef(returnFiber, child, element); // 处理ref
            existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
            if (__DEV__) {
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;
          }
        }
      // key一样,类型不同,直接删除该节点和其兄弟节点
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // 若key不一样,不能复用,标记删除当前单个child节点
      deleteChild(returnFiber, child);
    }
    // 指针指向下一个sibling节点
    child = child.sibling; 
  }
  // 创建一个新的fiber节点
  if (element.type === REACT_FRAGMENT_TYPE) {
    //  REACT_FRAGMENT_TYPE,特判
    const created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
    created.return = returnFiber; // 新节点的 return 指向到父级节点
    // FRAGMENT 节点没有 ref
    return created;
  } else {
    // 普通的html元素、函数组件、类组件等
    // 从 element 创建 fiber 节点
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element); // 处理ref
    created.return = returnFiber;
    return created;
  }
}

对于一个纯 html 文本节点,我们调用 reconcileSingleTextNode 这个函数,因为在比较当前一层的数据时,文本节点不能有兄弟节点,所以只有当 current 的第一个结点是文本节点时才能复用,否则就删除所有元素。

// 调度文本节点
function reconcileSingleTextNode(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  textContent: string,
  lanes: Lanes,
): Fiber {
  // 不再判断文本节点的key,因为文本节点就来没有key
  if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
    // 若当前节点是文本,则直接删除后续的兄弟节点
    deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
    const existing = useFiber(currentFirstChild, textContent); // 复用这个文本的fiber节点,重新赋值新的文本
    existing.return = returnFiber;
    return existing;
  }
  // 若不存在子节点,或者第一个子节点不是文本节点,直接将当前所有的节点都删除,然后创建出新的文本fiber节点
  deleteRemainingChildren(returnFiber, currentFirstChild);
  const created = createFiberFromText(textContent, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
}

当我们需要处理的元素不是单个数据,而是一组数据的时候,我们使用reconcileChildrenArray 进行处理:

  • 我们先按照顺序遍历 Fiber 链表和我们的数组,使用updateSlot 函数判定在相同的对应位置的两个元素是不是能够复用,它判断的依据还是两个元素的 key 是不是相同
  • 如果循环结束之后,旧的链表还没遍历完,说明剩下的节点已经不需要了,直接删除即可
  • 如果经过上面操作后,旧的 Fiber 用完了,但 element 元素没有全部访问到,说明剩下的元素没有对应的可以复用的节点,直接新建节点即可
  • 如果新旧元素都没遍历完,说明出现了元素乱序的情况,我们需要把旧节点放到 Map 中,然后根据 key 或者 index 获取。
let resultingFirstChild: Fiber | null = null; // 用于返回的链表
let previousNewFiber: Fiber | null = null; 

let oldFiber = currentFirstChild; // 旧 Fiber 链表的节点,开始指向同一层中的第一个节点
let lastPlacedIndex = 0; // 表示当前已经新建的 Fiber 长度
let newIdx = 0; // 表示遍历 newChildren 的索引指针
let nextOldFiber = null; // 下一个 fiber 节点

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  // 如果旧的节点大于新的
  if (oldFiber.index > newIdx) {
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    // 旧 fiber 的索引和n ewChildren 的索引匹配上了,获取 oldFiber 的下一个兄弟节点
    nextOldFiber = oldFiber.sibling;
  }

  // 比较旧的节点和将要转换的 element 
  const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
  // 匹配失败,不能复用
  if (newFiber === null) {
    if (oldFiber === null) {
      oldFiber = nextOldFiber;
    }
    break;
  }
  if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
      // newFiber 不是基于 oldFiber 的 alternate 创建的,销毁旧节点
      deleteChild(returnFiber, oldFiber);
    }
  }
  // 更新lastPlacedIndex
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

  // 更新返回的链表
  if (previousNewFiber === null) {
    // 若整个链表为空,则头指针指向到newFiber
    resultingFirstChild = newFiber;
  } else {
    // 若链表不为空,则将newFiber放到链表的后面
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber; 
  oldFiber = nextOldFiber; // 继续下一个节点
}

// 遍历结束(访问相同数量的元素了)
if (newIdx === newChildren.length) {
  // 删除旧链表中剩余的节点
  deleteRemainingChildren(returnFiber, oldFiber);
  // 返回新链表的头节点指针
  return resultingFirstChild;
}

// 若旧数据中所有的节点都复用了
if (oldFiber === null) {
  //创建新元素
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
      continue;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    //拼接链表
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
  return resultingFirstChild;
}
//如果新旧元素都没遍历完, mapRemainingChildren 生成一个以 oldFiber 的 key 为 key, oldFiber 为 value 的 map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

for (; newIdx < newChildren.length; newIdx++) {
  // 从 map 中查找是否存在可以复用的fiber节点,然后生成新的fiber节点
  const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
  if (newFiber !== null) {
    if (shouldTrackSideEffects) {
      if (newFiber.alternate !== null) {
         //newFiber.alternate指向到current,若current不为空,说明复用了该fiber节点,这里我们要在 map 中删除,因为后面会把 map 中剩余未复用的节点删除掉的
        existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    // 接着之前的链表进行拼接
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
}

if (shouldTrackSideEffects) {
  // 将 map 中没有复用的 fiber 节点添加到删除队列中,等待删除
  existingChildren.forEach(child => deleteChild(returnFiber, child));
}
// 返回新链表的头节点指针
return resultingFirstChild;

如果你想要了解更加详细的整体过程,你可以看这篇文章::React 的源码与原理解读(六):reconcileChildren 与 DIFF 算法

Reconciler DIFF 算法

上述的就是 Reconciler 的 DIFF 逻辑,我们来梳理一下整个流程:

React 只对虚拟 DOM 树进行分层比较,不考虑节点的跨层级比较。如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。对比时,每次都选择同一个父节点的孩子进行比较,如下图,同一个父亲节点下的所有孩子会进行比较

img

如果出现的跨层移动的情况,那么在父亲节点的比较时,就会删除跨层移动的节点,比如下图,在对root 进行比较的时候,A已经被标记为删除了,之后在对 B 进行比较的时候,虽然组件 A 出现了,但是我们并不会复用它,而会新建一个组件A

img

React 对于不同类型的组件,默认不需要进行比较操作,直接重新创建。对于同类型组件,使用 diff 策略进行比较,比如下图:两个组件的根节点不同,也就是说不是一个组件,但是组件的内容相同,这种情况下,React 并不会进行复用,而是直接新建:

img

对于位于同层的节点,通过唯一 key 来判断老集合中是否存在相同的节点,如果没有则创建;如果有,则判断是否需要进行移动操作。整个操作分为:删除、新增、移动 三种。如下图:我们对 A 进行了移动,对 C 进行了删除,对 E 进行了 新增。

img

img

在 React 中这样处理可以提升效率的原因是:

  • 在 web 开发中,很少会有跨层级的元素变化,,比如现在我有一个 p 标签,我经常做的操作是:改变这个标签的内容,或者在 p 标签的同一级再插入一个 p 标签,我们很少有在更新页面的时候在 p 标签的外围嵌套一个 div 这样的操作。
  • 两个不同类型的组件会产生两棵不同的树形结构
  • React 中的 key 是唯一的,一个 key 可以唯一标识一个元素

因而经过上述逻辑的优化,React 把经典 DIFF 算法(暴力递归比较)的 O( n^3 ) 优化到了近乎 O( n ) 。

Commit 提交更新

在经过了 DIFF 的处理之后,我们的新的 Fiber 树已经生成了,但是我们的真实 DOM 还没有更新,展示给用户的页面还是原本的样子,现在我们就需要将刚刚 DIFF 出来的更新同步到真实 DOM 上,这个阶段就是 commit 阶段,它在 performSyncWorkOnRoot performConcurrentWorkOnRoot 函数中进入:

export function performSyncWorkOnRoot(root: FiberRoot): null {
  let exitStatus = renderRootSync(root, lanes);
  // 生成完毕,返回我们的 WorkInProgress 树的根节点
  //....
  const finishedWork: Fiber = (root.current.alternate: any);
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  // 进入 commit 阶段
  commitRoot(
    root,
    workInProgressRootRecoverableErrors,
    workInProgressTransitions,
  );
  ensureRootIsScheduled(root);
  return null;
}

这个阶段总的来说它可以分为五个部分来进行分析讲解:

首先是 before mutation 阶段之前,我们主要进行一些准备工作:

  • 首先是处理之前遗留下来的 effect 任务,因为这些任务执行过程中可能被高优先级的任务中断,但是他们未执行会影响我们的 commit,具体可以查看 useEffect 那节的讲解。
  • 之后是判断我们这次调度完成了哪些 lane 上的任务,把他们收集起来,从我们的 lanes 列表中删除,表示这些任务完成了,然后重置我们的 FiberRoot 上的标志位
  • 最后是开始了一个处理 Effect 相关内容的调度,注意这里只是开始了调度,它正在执行并不在这里,具体可以查看 useEffect 那节的讲解。
  // 调用flushPassiveEffects执行完所有effect的任务
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
	
  // 判断哪些任务完成了
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  if (enableSchedulingProfiler) {
    markCommitStarted(lanes);
  }

  if (finishedWork === null) {
  	// 异常处理
  }
      
  //重置 FiberRoot 
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  root.callbackNode = null;
  root.callbackPriority = NoLane;

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

  // 处理 useEffect相关内容
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      pendingPassiveEffectsRemainingLanes = remainingLanes;
      pendingPassiveTransitions = transitions;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

之后是进入了 before mutation 阶段,这个阶段主要做一些前期准备工作,它遍历了整个 Fiber 树,做了以下操作:

  • 首先它触发了 getSnapShotBeforeUpdate 快照机制这个生命周期
  • 处理 DOM 渲染/删除后的 autoFocusblur 等逻辑
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  // ...focus blur相关

  if ((flags & Snapshot) !== NoFlags) {
    setCurrentDebugFiberInDEV(finishedWork);
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        break;
      }
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
	      //触发 getSnapshotBeforeUpdate
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      case HostRoot: {
        if (supportsMutation) {
          const root = finishedWork.stateNode;
          clearContainer(root.containerInfo);
        }
        break;
      }
      //....
    }

    resetCurrentDebugFiberInDEV();
  }
}

之后是进入了 MutationEffects 这个阶段,这个阶段我们就需要将我们的修改同步到我们的 DOM 上了,这个阶段不同类型的 Fiber 会有不同的操作,但是也有都需要调用的部分,我们先看总体的逻辑:

首先是调用了 recursivelyTraverseMutationEffects 进行删除操作,对于需要删除的节点,我们这样操作:

  • 先将它 ref 指向的元素设置为 null
  • 之后对于类组件调用它的 componentWillUnmount 生命周期函数,函数组件则处理它的 useLayoutEffect 的销毁逻辑,具体可以查看 useEffect 那节的讲解。
  • 如果是原生的 HTML 节点,我们调用 removeChild 方法删除真实 DOM
  • 最后向下遍历子节点,执行删除逻辑
function commitDeletionEffectsOnFiber(
  finishedRoot: FiberRoot,
  nearestMountedAncestor: Fiber,
  deletedFiber: Fiber,
) {
  onCommitUnmount(deletedFiber);
  switch (deletedFiber.tag) {
    case HostComponent: {
      if (!offscreenSubtreeWasHidden) {
        // ref 设置回 null
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
      }
    }
    case HostText: {
      if (supportsMutation) {
        const prevHostParent = hostParent;
        const prevHostParentIsContainer = hostParentIsContainer;
        hostParent = null;
        // 往下遍历子节点,执行删除
        recursivelyTraverseDeletionEffects(
          finishedRoot,
          nearestMountedAncestor,
          deletedFiber,
        );
        hostParent = prevHostParent;
        hostParentIsContainer = prevHostParentIsContainer;
        // 删除真正的 DOM,调用了原生的 removeChild 方法
        if (hostParent !== null) {
          if (hostParentIsContainer) {
            removeChildFromContainer(
              ((hostParent: any): Container),
              (deletedFiber.stateNode: Instance | TextInstance),
            );
          } else {
            removeChild(
              ((hostParent: any): Instance),
              (deletedFiber.stateNode: Instance | TextInstance),
            );
          }
        }
      } else {
        recursivelyTraverseDeletionEffects(
          finishedRoot,
          nearestMountedAncestor,
          deletedFiber,
        );
      }
      return;
    }
	// ....
    // 函数组件
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      if (!offscreenSubtreeWasHidden) {
        //  读取 updateQueue 队列,队列用链表的方式保存
        const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any);
        if (updateQueue !== null) {
          const lastEffect = updateQueue.lastEffect;
          if (lastEffect !== null) {
            const firstEffect = lastEffect.next;
            let effect = firstEffect;
            do {
              const {destroy, tag} = effect;
              if (destroy !== undefined) {
                if ((tag & HookInsertion) !== NoHookEffect) {
                  // 处理 useInsertionEffect 副作用
                  safelyCallDestroy(
                    deletedFiber,
                    nearestMountedAncestor,
                    destroy,
                  );
                } else if ((tag & HookLayout) !== NoHookEffect) {
                  // 处理 useLayoutEffect 副作用
                  if (enableSchedulingProfiler) {
                    markComponentLayoutEffectUnmountStarted(deletedFiber);
                  }
                  if (
                    enableProfilerTimer &&
                    enableProfilerCommitHooks &&
                    deletedFiber.mode & ProfileMode
                  ) {
                    startLayoutEffectTimer();
                    safelyCallDestroy(
                      deletedFiber,
                      nearestMountedAncestor,
                      destroy,
                    );
                    recordLayoutEffectDuration(deletedFiber);
                  } else {
                    safelyCallDestroy(
                      deletedFiber,
                      nearestMountedAncestor,
                      destroy,
                    );
                  }
                  if (enableSchedulingProfiler) {
                    markComponentLayoutEffectUnmountStopped();
                  }
                }
              }
              effect = effect.next;
            } while (effect !== firstEffect);
          }
        }
      }
	  // 遍历子节点执行删除逻辑
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
    // 类组件
    case ClassComponent: {
      if (!offscreenSubtreeWasHidden) {
        // 移除 ref
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
        const instance = deletedFiber.stateNode;
        if (typeof instance.componentWillUnmount === 'function') {
          // 调用类组件实例的 componentWillUnmount 方法
          safelyCallComponentWillUnmount(
            deletedFiber,
            nearestMountedAncestor,
            instance,
          );
        }
      }
      //遍历子节点执行删除逻辑
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
	// ....省略
    default: {
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
  }
}

另一个是插入的逻辑 commitReconciliationEffects

  • 判断节点的父 Fiber 是不是需要重置,如果是则调用 parent.textContent = '' 来重置它的内容
  • 之后寻找它有没有兄弟,如果有则调用 insertBefore 方法将内容插入到兄弟节点之前,如果没有,则调用父节点的 appendChild 进行插入
function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }
  //获取父 fiber
  const parentFiber = getHostParentFiber(finishedWork);
  switch (parentFiber.tag) {
    case HostComponent: {
      const parent: Instance = parentFiber.stateNode;
      //  判断父 fiber 是否有 ContentReset(内容重置)标记
      if (parentFiber.flags & ContentReset) {
        // 通过 parent.textContent = '' 的方式重置
        resetTextContent(parent);
        parentFiber.flags &= ~ContentReset;
      }
      // 找它的下一个兄弟 DOM 节点,
      const before = getHostSibling(finishedWork);
      // 如果存在,用 insertBefore 方法;如果没有,就调用原生的 appendChild 方法
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }
    case HostRoot:
    case HostPortal: {
      const parent: Container = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }
    default:
      throw new Error(
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
}

最后回到 commitMutationEffectsOnFiber 再进行分类处理:

  • 对于我们的类组件,我们不做处理
  • 对于我们的函数组件,我们处理 useInsertionEffect 的全部逻辑,useLayoutEffect 的销毁逻辑
  • 对于原生组件,如果可以复用,我们调用 commitUpdate 执行更新逻辑,否则直接重置这个原生节点
  • 如果是原生文本,我们调用 commitTextUpdate 函数,更新其文本内容
 switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
	  // ....
      if (flags & Update) {
        try {
          // 找出 useInsertionEffect 的 destroy 方法去调用
          commitHookEffectListUnmount(
            HookInsertion | HookHasEffect,
            finishedWork,
            finishedWork.return,
          );
          // 执行 useInsertionEffect 的回调函数,并将返回值保存到 effect.destory 里。
          commitHookEffectListMount(
            HookInsertion | HookHasEffect,
            finishedWork,
          );
        } catch (error) {
          captureCommitPhaseError(finishedWork, finishedWork.return, error);
        }
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          finishedWork.mode & ProfileMode
        ) {
          try {
            startLayoutEffectTimer();
            // 执行 useLayoutEffect 对应的 destroy 方法
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
          recordLayoutEffectDuration(finishedWork);
        } else {
          try {
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
    // 类组件不会进行操作
    case ClassComponent: {
      // ...
      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      return;
    }
    //原生组件
    case HostComponent: {
      //...
      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      if (supportsMutation) {
        // 判断是不是需要重置
        if (finishedWork.flags & ContentReset) {
          const instance: Instance = finishedWork.stateNode;
          try {
            resetTextContent(instance);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
		// 判断是不是需要更新
        if (flags & Update) {
          const instance: Instance = finishedWork.stateNode;
          if (instance != null) {
            const newProps = finishedWork.memoizedProps;
            const oldProps =
              current !== null ? current.memoizedProps : newProps;
            const type = finishedWork.type;
            const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
            finishedWork.updateQueue = null;
            if (updatePayload !== null) {
              try {
                // 更新操作
                commitUpdate(
                  instance,
                  updatePayload,
                  type,
                  oldProps,
                  newProps,
                  finishedWork,
                );
              } catch (error) {
                captureCommitPhaseError(
                  finishedWork,
                  finishedWork.return,
                  error,
                );
              }
            }
          }
        }
      }
      return;
    }
    //原生文本
    case HostText: {
	  //....
      if (flags & Update) {
        if (supportsMutation) {
          if (finishedWork.stateNode === null) {
            throw new Error(
              'This should have a text node initialized. This error is likely ' +
                'caused by a bug in React. Please file an issue.',
            );
          }

          const textInstance: TextInstance = finishedWork.stateNode;
          const newText: string = finishedWork.memoizedProps;
          const oldText: string =
            current !== null ? current.memoizedProps : newText;
          try {
            // 更新操作
            commitTextUpdate(textInstance, oldText, newText);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
    //...
}

再经过这些操作后,我们的真实 DOM 已经生成完毕了,我们也通过改变了 current 的指向来改变了我们当前工作的 Fiber 树了

    // 新的树生成完毕了,改变 current 的指向
    root.current = finishedWork;

所以在 LayoutEffects 这个阶段,我们已经可以调用到我们的真实 DOM 了,这个阶段,我们的主要工作是:

  • 如果是函数组件,我们处理它的 useLayoutEffect 的创建逻辑,具体可以查看 useEffect 那节的讲解。
  • 如果是类组件,我们需要执行它的 componentDidMount 或者 componentDidUpdate 生命周期和状态更新的回调函数
  • 如果根节点,我们需要处理其回调,也就是是 ReactDOM.render 的调回函数
  • 如果是原生组件,我们处理其自动更新的情况
function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        if (
          !enableSuspenseLayoutEffectSemantics ||
          !offscreenSubtreeWasHidden
        ) {
          if (
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            finishedWork.mode & ProfileMode
          ) {
            try {
              startLayoutEffectTimer();
              // 提交 useLayoutEffect 
              commitHookEffectListMount(
                HookLayout | HookHasEffect,
                finishedWork,
              );
            } finally {
              recordLayoutEffectDuration(finishedWork);
            }
          } else {
            commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
          }
        }
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
            if (current === null) {
              if (
                enableProfilerTimer &&
                enableProfilerCommitHooks &&
                finishedWork.mode & ProfileMode
              ) {
                try {
                  startLayoutEffectTimer();
                  // 首次渲染,触发 componentDidMount 生命周期
                  instance.componentDidMount();
                } finally {
                  recordLayoutEffectDuration(finishedWork);
                }
              } else {
                instance.componentDidMount();
              }
            } else {
              const prevProps =
                finishedWork.elementType === finishedWork.type
                  ? current.memoizedProps
                  : resolveDefaultProps(
                      finishedWork.type,
                      current.memoizedProps,
                    );
              const prevState = current.memoizedState;
              if (
                enableProfilerTimer &&
                enableProfilerCommitHooks &&
                finishedWork.mode & ProfileMode
              ) {
                try {
                  startLayoutEffectTimer();
                  // 非首次渲染,触发 componentDidUpdate 生命周期
                  instance.componentDidUpdate(
                    prevProps,
                    prevState,
                    instance.__reactInternalSnapshotBeforeUpdate,
                  );
                } finally {
                  recordLayoutEffectDuration(finishedWork);
                }
              } else {
                instance.componentDidUpdate(
                  prevProps,
                  prevState,
                  instance.__reactInternalSnapshotBeforeUpdate,
                );
              }
            }
          }
        }

        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
          // 执行 commitUpdateQueue 处理回调
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostRoot: {
        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
          let instance = null;
          if (finishedWork.child !== null) {
            switch (finishedWork.child.tag) {
              case HostComponent:
                instance = getPublicInstance(finishedWork.child.stateNode);
                break;
              case ClassComponent:
                instance = finishedWork.child.stateNode;
                break;
            }
          }
          //  调用 commitUpdateQueue 处理 ReactDOM.render 的回调
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostComponent: {
        const instance: Instance = finishedWork.stateNode;
        // commitMount 处理 input 标签有 auto-focus 的情况
        if (current === null && finishedWork.flags & Update) {
          const type = finishedWork.type;
          const props = finishedWork.memoizedProps;
          commitMount(instance, type, props, finishedWork);
        }

        break;
      }
      case HostText: 
        break;
      }
      case HostPortal: {
        break;
      }
      default:
        throw new Error(
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
    }
  }
}

LayoutEffects 这个阶段结束之后,我们将 rootWithPendingPassiveEffects 赋值。根据上面的我们在 commit 的三个阶段开始前,已经把开始了一个 flushPassiveEffects 的调度 Effect 相关内容,而这个调度使用了 flushPassiveEffects 函数,它只有在 rootWithPendingPassiveEffects 不是 null 的情况下才会运行,而此时,我们将其进行赋值,这样就可以开始执行 useEffect 相关副作用的处理,包括其挂载和更新

  if (rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsLanes = lanes;
  } else {
    //....
  }


export function flushPassiveEffects(): boolean {
  if (rootWithPendingPassiveEffects !== null) {
    // .... 省略优先级和DEV操作
    try {
      // 省略
      return flushPassiveEffectsImpl();
    } finally {
	  //省略
    }
  }
  return false;
}

至此,我们的整个 React 的运行流程已经讲解完毕了,关于 commit 这一节的更加详细的内容可以查看这一篇:React 的源码与原理解读(七):commit 阶段

Hook 的原理的作用方式

最后我们来讲解以下 hooks 相关的内容,他的作用在 renderWithHooks 函数中,我们之前已经提到了,其中根据 current === null 的判断来决定我们是使用初始化的 hooksDispatcher 还是更新的 hooksDispatcher ,Dispatcher ,其中每一项都包含了我们用到的各种 hooks 的操作函数

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  // 获取当前函数组件对应的 fiber,并且初始化
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;

  if (__DEV__) {
    // ....
  } else {
    // 根据当前阶段决定是初始化hook,还是更新hook
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
  // 调用函数组件
  let children = Component(props, secondArg);
    
  // 重置hook链表指针
  currentHook = null;
  workInProgressHook = null;
    
  return children;
}

const HooksDispatcherOnMount: Dispatcher = {
  ...
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
  ...
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  ...
};

之后我们看到每一个 mount 和 update 逻辑,他们分别都调用了 mountWorkInProgressHookupdateWorkInProgressHook 进行操作,前者用于创建一个 hook 元素,挂载到 Fiber 的 hook 链表上;而当我们要更新一个 hooks 的时候,我们首先获取两棵树的下一个 hook, 我们用 nextCurrentHook nextWorkInProgressHook 来标识下一个需要操作的 hooks 。如果 nextWorkInProgressHook 已经存在,我们直接使用它即可,否则,我们需要从我们的 nextCurrentHook 处克隆一份我们的 hook 放入其中

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null, // 上次渲染时所用的 state
    baseState: null,  // 已处理的 update 计算出的 state
    baseQueue: null,  // 未处理的 update 队列(上一轮没有处理完成的)
    queue: null,      // 当前的 update 队列
    next: null, // 指向下一个hook
  };

  // 保存到链表中
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

function updateWorkInProgressHook(): Hook {

  // 获取 current 树上的 Fiber 的 hook 链表
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // workInProgress 树上的 Fiber 的 hook 链表
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }
    
  // 如果 nextWorkInProgressHook 不为空,直接使用
  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    //否则我们克隆一份 hooks
    currentHook = nextCurrentHook;
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这部分的详细讲解可以查看这篇 React 的源码与原理解读(十一):hooks 的原理

useCallback

useCallback 用于优化代码, 可以让对应的函数只有在依赖发生变化时才重新定义:

useCallback(fn, dependencies)

mountCallback 的原理很简单,如果是第一次运行,我们获取传入的 callback 和 deps,直接把数据缓存到 memoizedState 即可,之后我们返回我们的 callback ,更新它则是将我们缓存的 deps 和传入的进行比较,如果相同则返回我们缓存的 callback,否则把新的 callback 和 deps 进行缓存:

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps]; 
  return callback;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState; 
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

useMemo

useMemouseCallback 的功能很像,只不过 useMemo 用来缓存函数执行的结果

const cachedValue = useMemo(calculateValue, dependencies)

它们的源码也很类似,和 useCallback 唯一的区别是,我们需要通过传入的 nextCreate 函数算出结果,然后将结果进行缓存:

function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook(); 
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps]; 
  return nextValue; 
}

function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

关于这两个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十二):Hooks解读之一 useCallback&useMemo

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue),返回的 ref 对象在组件的整个生命周期内持续存在,我们也可以使用 useRef 去保存对 DOM 对象的引用。

const refContainer = useRef(initialValue);

useRef 的源码非简洁:

  • mountRef 获取我们传入的初始值,然后把它存到我们的 memoizedState 中,因为我们返回了我们的 ref,它是一个对象,也就是引用类型,所以我们可以直接修改这个 ref 值,它也会作用在 memoizedState

  • updateRef 则是返回我们的 memoizedState

function mountRef<T>(initialValue: T): {| current: T |} {
  const hook = mountWorkInProgressHook();

  // 存储数据,并返回这个数据
  const ref = { current: initialValue };
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {| current: T |} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}

而关于它操作 DOM 的能力,则是在解析为 React 节点的时候,成为了节点上的 ref 属性

const element = {
    $$typeof: REACT_ELEMENT_TYPEtype: type,                    
    key: key,                      
    ref: ref,                      //对组件实例的引用
    props: props,                  
    _owner: owner                  
}

beginWork 这个函数中,我们调用了 markRef 这个函数,它的作用是告诉我们的 React ,这个fiber 节点对应的 DOM 被 ref 了,我们通过 effectTag 中的一个标志位来标识我们的这个 fiber 有没有 ref,这些用于我们之后的逻辑判定要不要进行 ref 的相关操作。

function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  if (
    (current === null && ref !== null) ||
    (current !== null && current.ref !== ref)
  ) {
    workInProgress.effectTag |= Ref;
  }
}

而在 commit 阶段的 recursivelyTraverseMutationEffects 函数中,我们在删除一个 Fiber 的同时,需要解绑 ref,它调用了safelyDetachRef 函数来完成这个操作,如果它是一个函数类型的 ref,会执行 ref 函数,参数为null,这个函数类型的 ref 就是我们的 class 组件使用 createRef 创造的,执行这个函数函数可以将其内部的值置为空,如果它不是一个 function, 也就是一个带有 current 的 DOM ,我们就把它的 useRef 属性设置为 null,从而清空我们的挂载:

function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
  const ref = current.ref;
  if (ref !== null) {
    if (typeof ref === 'function') {
      let retVal;
      try {
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          current.mode & ProfileMode
        ) {
          try {
            startLayoutEffectTimer();
            retVal = ref(null);
          } finally {
            recordLayoutEffectDuration(current);
          }
        } else {
          retVal = ref(null);
        }
      } catch (error) {
        captureCommitPhaseError(current, nearestMountedAncestor, error);
      }
    } else {
      ref.current = null;
    }
  }
}

而在 commit 阶段的 commitLayoutEffect 函数中,这个阶段会操作我们的真实 DOM,在函数的最后有这样的操作,如果我们处理的 Fiber 的标记中有 Ref 标记,我们将进入 commitAttachRef 这个函数,它的作用就是把 ref 和我们的 useRef 绑定,在类组件中或者一些另外的组件中,我们会通过safelyAttachRef 这个函数来调用 commitAttachRef

 function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
   //.......省略
   // 确保能拿到我们的 dom
   if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
    if (enableScopeAPI) {
      // 排除 ScopeComponent 是因为之前已经处理了
      if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
        commitAttachRef(finishedWork);
      }
    } else {
      if (finishedWork.flags & Ref) {
        commitAttachRef(finishedWork);
      }
    }
  }
}

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  switch (finishedWork.tag) {
    //....
    // 对于 ScopeComponent ,这里已经处理了 safelyDetachRef 和 safelyAttachRef
    case ScopeComponent: {
      if (enableScopeAPI) {
        recursivelyTraverseMutationEffects(root, finishedWork, lanes);
        commitReconciliationEffects(finishedWork);
        if (flags & Ref) {
          if (current !== null) {
            safelyDetachRef(finishedWork, finishedWork.return);
          }
          safelyAttachRef(finishedWork, finishedWork.return);
        }
        if (flags & Update) {
          const scopeInstance = finishedWork.stateNode;
          prepareScopeUpdate(scopeInstance, finishedWork);
        }
      }
      return;
    }
  }
}

这个函数通过当前 fiber 的 tag 来获取对应的实例:对于 HostComponent ,实例就是获取到的 DOM 节点,其他情况就是 fiber.stateNode,之后判断 ref 的类型,如果是函数类型,调用 ref 函数并将实例传过去;若不是,则将 ref.current 赋值为该实例

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;
    }
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    if (typeof ref === 'function') {
      let retVal;
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        finishedWork.mode & ProfileMode
      ) {
        try {
          startLayoutEffectTimer();
          retVal = ref(instanceToUse);
        } finally {
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
        retVal = ref(instanceToUse);
      }
    } else {
      ref.current = instanceToUse;
    }
  }
}

关于这个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十三):Hooks解读之二 useRef

useState

useState()是我们最常见的几个 hooks 之一,它运行我们传入一个初始值来初始化一个 state,之后返回给我们这个 state 和改变它的方法 setState:

const [state, setState] = useState(initialstate)

useState 的源码是这样的:

  • 首先是我们获取传入的初始值,如果是函数的话,我们执行它获取结果作为我们的初始值
  • 之后我们将我们的初始化放到 hook 节点的 memoizedStatebaseState 属性上
  • 之后我们创建一个更新队列,我们将他们放在 hook 的 queue 属性中
  • 最后我们使用 dispatchSetState 生成一个更新函数,返回给用户
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  // 如果传入的初始值是一个函数,直接执行获得结果
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // 更新 hook 缓存的数据
  hook.memoizedState = hook.baseState = initialState;
  // 创建一个更新队列
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,     // 依然是环状链表,这个属性指向链表的最后一个节点
    interleaved: null,
    lanes: NoLanes,  
    dispatch: null,    // setState
    lastRenderedReducer: basicStateReducer,    // 上次render传入的操作
    lastRenderedState: (initialState: any),    // 上次render后的state
  };
  hook.queue = queue;
  // 生成更新函数
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

dispatchSetState 这个函数将这个更新封装成一个 Update 节点,然后把它拼接到 queue 的 pending 上;之后判定这个更新是不是在渲染阶段发生:如果是则设定一个标识;否则计算出新的 state,然后与之前的 state 对比,若没有更新,则直接退出;如果有更新,我们调用 scheduleUpdateOnFiber 函数开始一个调度,这个函数在 Lane 这章节已经详细讲过了,可以回去看这个函数:

function dispatchSetState<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {
  // 获取lane
  const lane = requestUpdateLane(fiber);

  // 将 action 操作封装成一个 update节点,用于后续构建链表使用
  const update: Update<S, A> = {
    lane,                 // 优先级
    action,               // setState 传入的内容,可能是操作或者数值
    hasEagerState: false, // 紧急状态
    eagerState: null,     // 紧急状态下提前计算出的结果
    next: (null: any),    // 指向到下一个节点的指针
  };
  // 在渲染阶段的更新,拼接到 queue.pending 的后面
  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
	// 把 update 更新到 queue.pending 指向的环形链表里
    enqueueUpdate(fiber, queue, update, lane);
    const alternate = fiber.alternate;
    // 如果当前节点没有更新任务
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      const lastRenderedReducer = queue.lastRenderedReducer; // 上次render后的reducer,在mount时即 basicStateReducer
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        const currentState: S = (queue.lastRenderedState: any); // 上次render后的state,mount时为传入的initialState
        const eagerState = lastRenderedReducer(currentState, action);
        update.hasEagerState = true; // 表示该节点的数据已计算过了
        update.eagerState = eagerState; // 存储计算出来后的数据
        if (is(eagerState, currentState)) {
          // 若这次得到的state与上次的一样,则不再重新渲染
          return;
        }
      }
    }
    // 有更新任务
    const eventTime = requestEventTime();
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    if (root !== null) {
      entangleTransitionUpdate(root, queue, lane);
    }
  }
  markUpdateInDevTools(fiber, lane, action);
}

function enqueueRenderPhaseUpdate<S, A>(
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
) {
  // 标识render阶段的更新产生了
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
}

function enqueueUpdate<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
  lane: Lane,
) {
  // 交错更新
  if (isInterleavedUpdate(fiber, lane)) {
    const interleaved = queue.interleaved;
    if (interleaved === null) {
      update.next = update;
      pushInterleavedQueue(queue);
    } else {
      update.next = interleaved.next;
      interleaved.next = update;
    }
    queue.interleaved = update;
  } else {
    const pending = queue.pending;
    if (pending === null) {
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    queue.pending = update;
  }
}

在更新阶段我们则调用了函数updateReducer,它首先获取当前的 pending 队列和上次遗留下来的队列,把他们合并到一起,之后遍历队列中所有的 hook 更新,根据当前的优先级判定它能不能执行,若符合当前优先级的,则执行该 update 节点的 action,计算出新的 state,它将作为我们的返回值,遍历完所有可以执行的任务后,得到一个新的 newState,然后判断与之前的 state 是否一样,若不一样,则标记该 fiber 节点需要更新,并返回新的 newState 和 dispatch 方法。

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current: Hook = (currentHook: any);
  //上次遗留下来的优先级不够的任务
  let baseQueue = current.baseQueue;
  // 获取 queue 队列
  const pendingQueue = queue.pending;
  // 拼接链表
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null; 
  }

  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState; // 上次的渲染的结果值,每次循环时都计算得到该值,然后供下次循环时使用
	// 新的 BaseState,用于下次渲染的
    let newBaseState = null;
    // 新的 basequeue
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateLane = update.lane;
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 优先级不足,跳过此更新,放到暂存执行的队列中
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };
        // 如果第一次出现了不能执行的了,我们要把当前的计算结果保存下来,因为我们会将此节点到最后的所有节点都存储起来,所以此时我们的 newBaseState 就是当前得到的值,这样下次渲染的时候,我们获得的就是这个不能执行的节点前的执行结果
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        // 更新优先级
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        // 标识跳过的任务的优先级
        markSkippedUpdateLanes(updateLane);
      } else {
        // 之前的节点有不能执行的,那么后面的节点都要缓存
        if (newBaseQueueLast !== null) {
          const clone: Update<S, A> = {
            // 该update需要执行,所以我们永远不能跳过他,使用NoLane优先级,可以避免上面的判断会跳过该步骤
            lane: NoLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: (null: any),
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
		// 已经执行过了(mount中)
        if (update.hasEagerState) {
          newState = ((update.eagerState: any): S);
        } else {
          // 计算出当前位置的新的state,注意,这个 newState 是作为这一步的返回结果的,不一定我们的 newBaseState,所以就算要被缓存的节点,只要能执行就需要处理
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      // 所有的update都执行了,那么没有下一次渲染了,所以下一次需要的 baseState就是计算结果
      newBaseState = newState;
    } else {
      // 有低优先级的update任务,则next指针指向到第1个,形成单向环形链表,
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // 若newState和之前的state不一样,标记该fiber需要更新
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    hook.memoizedState = newState; // 整个update链表执行完,得到的newState,用于本次渲染时使用
    hook.baseState = newBaseState; // 下次执行链表时的初始值
    hook.baseQueue = newBaseQueueLast; // 新的update链表,可能为空
    queue.lastRenderedState = newState; // 将本次的state存储为上次rendered后的值
  }
  // 交错更新,省略.....
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

useReducer

useReduceruseState 类似,但是它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

const [state, dispatch] = useReducer(reducer, initState,init);

因为 useReducer 其实就是一个简单的可以自定义更新方法的 useState ,那么他们的源码也应该极为相似,所以这里我们就直接给出源码和注释:

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  // 如果是懒创建的 initState,我们调用函数得到其值,否则直接获取其值
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
  // 初始化 memoizedState 和 baseState
  hook.memoizedState = hook.baseState = initialState;
  // 初始化更新队列
  const queue: UpdateQueue<S, A> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  // 创建 dispatch
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

关于这两个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十四):Hooks解读之三 useState&useReducer

useEffect

useEffect 是我们最常见的几个 hooks 之一,给函数组件增加了操作副作用的能力。其第一个参数是一个副作用函数,React 会在每次渲染后调用副作用函数 ,副作用函数还可以通过返回一个函数来指定如何清除副作用。其第二个参数是一个依赖项数组,只有其中有一项发生变化的情况才会触发当前的 userEffect ,否则不触发,如果我们设置第二个参数为空,那么相当于我们会监听所有的数据变化。

useEffect(effect, deps?);

useLayoutEffect

useLayoutEffectuseEffect 的定义完全相同,都是其传入一个副作用函数和一个依赖项数组,他的区别在于:

  • useEffect 是异步的,useLayoutEffect 是同步的
  • useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前

这两个 hook 源码也基本一致,mount 都是调用了一个 mountEffectImpl 函数:他首先把我们的设定的 fiberFlags 设定到了当前的 Fiber 上,后续我们会通过这个标志判定要不要处理 effect。然后使用 pushEffect ,它会先创建一个 effect 数据结构,接着将 effect 添加到函数组件 fiber 的更新队列 updateQueue 之上,最后返回这个创建的 effect 作为 hook.memoizedState。

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
    return mountEffectImpl(
      PassiveEffect | PassiveStaticEffect,
      HookPassive,
      create,
      deps,
    );
}

function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  let fiberFlags: Flags = UpdateEffect;
  if (enableSuspenseLayoutEffectSemantics) {
    fiberFlags |= LayoutStaticEffect;
  }
  return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; 
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // 获取更新队列
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  // 没有的话先创建更新队列
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

update 阶段和 mount 阶段大同小异,调用了 updateEffectImpl 这个函数,我们获取了上次的缓存的 effect,拿出本次的 deps 和这次的进行对比,如果两次的依赖项没有变化,或者依赖项是空,我们本次不会执行这个 hook,推入一个没有 HookHasEffect 标记的 effect ,并且直接退出函数;如果有变化,或者 nextDeps 不存在,那么我们先在 fiber 上打标记,然后推入一个包含 HookHasEffect 标记的 effect,说明这个 effect 包含副作用需要被执行。

上述操作只是把副作用加入到了我们 Fiber 的 updateQueue 属性上,其具体的运行则是在 commit 阶段中,我们之前已经简单的提到过,这里再总结一下。

对于 useLayoutEffect

  • commitMutationEffects 这个阶段,执行我们删除节点和更新节点的销毁逻辑,也就是 destroy 函数
  • commitLayoutEffects 这个阶段,执行我们节点的副作用函数 create 逻辑并且挂载我们的 destroy 函数

对于 useEffect

  • 首先我们在 commit 阶段一开始就执行了一次 flushPassiveEffects 函数,这个函数遍历了我们的 fiber 上的副作用列表,对每个标记的 HookPassive 的副作用(useEffect 创建的)调用它的 destroy 逻辑,然后在调他们的 create 逻辑并且挂载 destroy 函数,这个逻辑中把我们的 rootWithPendingPassiveEffects 变量设置为了 null,这一步是因为上一次的渲染可能因为我们开启的flushPassiveEffects 被高优先级的任务抢占或者其他情况而没执行,我们需要确保进入 commit 阶段前我们没有未执行的useEffect 副作用了,这个逻辑也需要通过 do… while… 逻辑保证我们能顺利执行完毕(执行过程中也可能被抢占)
  • 在进入 commitBeforeMutationEffects 之前,如果显示我们的节点上具有 HookPassive 标记(在 render 过程中会做好记号),说明我们有需要处理的 useEffect 副作用,我们把 rootDoesHavePassiveEffects 这个全局变量设置为 true ,之后我们开启一个调度,给我们的 flushPassiveEffects 一个低优先级的任务,然后开始我们的 commit 阶段的逻辑
  • commit 阶段的三个子阶段执行完毕后,我们根据 rootDoesHavePassiveEffects 判断是不是有需要处理的 useEffect 副作用, 如果有,那么我们把 FiberRoot 给予我们的 rootWithPendingPassiveEffects 变量,这个,之后随着 commit 阶段的结束,我们注册的任务开始了调度,此时我们的 rootWithPendingPassiveEffects 包含了 effectList,就可以正常执行我们的处理逻辑
  • 如果遍历正常结束,那么我们的 rootWithPendingPassiveEffects 会被置为 null,此时我们下一轮调度就不会走开头的 do… while… 逻辑清空 rootWithPendingPassiveEffects ,但是如果我们的 flushPassiveEffects 运行中被中断了,下一次运行时就仍会存在 rootWithPendingPassiveEffects ,此时就需要清空他们了,要注意,如果我们没有后续的需要调度任务了,那么我们的 flushPassiveEffects 肯定也不会被打断,就能顺利执行完毕,所以不用担心出现有 useEffect 没执行的情况

关于这两个 hooks 的详细解读和用法讲解可以看这篇:React 的源码与原理解读(十五):Hooks解读之四 useLayoutEffect&useEffect

context

Context 是 React 官方提供的一种让父组件可以为它下面的整个组件树提供数据的数据传递方式,useContext 是一个 React Hook,可以让你读取和订阅组件中的 Context。

在 React 中,Context 的使用步骤如下:

  1. 创建 一个 context。
  2. 在指定数据的组件中 提供 这个 context。
  3. 在子组件中 消费 这个 context

首先我们使用 createContext 这个 API 来创建一个 Context ,它传入一个初始值,返回一个 context

const Context = React.createContext('default-value')

我们可以通过 Provider 包裹组件来提供这个 context,其中的 value 就是给子组件的这个 Provider 的初始值,下面的相当于 Context.Provider 包裹的所有子组件都可以通过 Context 来获取相同的数据,这些数据的初始值是 new-value。

const Context = React.createContext('default-value')
function Parent() {
 return ( 
  // 在内部的后代组件都能够通过相同的 Ract.createContext() 的实例访问到 context 数据
  <Context.Provider value="new-value">
     <Children>
  <Context.Provider>
 )
}

而我们的子组件可以通过的来消费我们的 context ,这里我们需要从之前定义 Context 的位置将其引入,只有使用了同一个 Context 才能获取相同的数据

import Context from "xxxxxx"
<Context.Consumer>
      { v => {
        // 内部通过函数访问祖先组件提供的 Context 的值
        return <div> {v} </div>
      }}
</Context.Consumer>

useContext 是一个 React Hook,可以让你读取和订阅组件中的 Context

const value = useContext(SomeContext)
import Context from "xxxxxx"
function Child() {
  const { ctx } = useContext(Context)
  return <div> {ctx} </div>
}

context 这个类是 React 中定义的一个数据结构,createContext 就是新建了这样一个数据结构,包括了数据、Consumer 和 Provider 来提供用户使用

export type ReactContext<T> = {
  $$typeof: Symbol | number,
  Consumer: ReactContext<T>,           // 消费 context 的组件
  Provider: ReactProviderType<T>,      // 提供 context 的组件
  // 保存 2 个 value 用于支持多个渲染器并发渲染
  _currentValue: T,
  _currentValue2: T,
  _threadCount: number, // 用来追踪 context 的并发渲染器数量
  // DEV only
  _currentRenderer?: Object | null,
  _currentRenderer2?: Object | null,
    
  displayName?: string,  // 别名
  _defaultValue: T,      
  _globalName: string,
  ...
};

export function createContext<T>(defaultValue: T): ReactContext<T> {
    
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,   // 用 $$typeof 来标识这是一个 context
    _currentValue: defaultValue,    // 给予初始值
    _currentValue2: defaultValue,   // 给予初始值
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
    _defaultValue: (null: any),
    _globalName: (null: any),
  };
	
  // 添加 Provider ,并且 Provider 中的_context指向的是 context 对象
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,   // 用 $$typeof 来标识这是一个 Provider 的 symbol
    _context: context,
  };

  let hasWarnedAboutUsingNestedContextConsumers = false;
  let hasWarnedAboutUsingConsumerProvider = false;
  let hasWarnedAboutDisplayNameOnConsumer = false;
	
  // 添加 Consumer
  context.Consumer = context;
  return context;
}

建立 Fiber 的过程中,我们会根据 $$typeof 的值给我们的 Fiber 的 tag 添加了不同的值,上文中,在创建 context 时,Provider 给予了 REACT_PROVIDER_TYPE 类型,而 Consumer 指向 context 本身,所以就是 REACT_CONTEXT_TYPE 类型字段,因而,当我们在 jsx 中解析到这两个类型时,就会判定为对应的字段。

我们在 beginWork 这个函数中,我们会对不同 tag 的进行处理:

  • 在 Provider 初始化的时候,beginWork 中我们会将我们 context 的值压入栈中

  • 而在 Consumer 初始化的时候,一个 Fiber 上依赖的所有 context 会被放入一个 dependencies 链表中,因为 Consumer 指向ReactContext 本身,所以我们直接通过 _currentValue 就可以拿到需要的对象

  • 当一个 context 更新后,Provider 会进行判定,如果值发生变化不可复用,会调用 propagateContextChange 递归遍历所有的孩子节点,节点中使用了这个 Provider 的会被标识为强制更新优先级,在之后过程中被更新

  • 当一个 Provider 处理完毕,在 commit 阶段,入栈的数值会被 pop 出去,然后对应 context 的值也会更新为栈中上一个节点的内容,这样做是为了保证在多次嵌套的 context 中,用户获得的始终是离它最近的的 Provider 提供的值

  • useContext 作为一个钩子,它本身只是为了适配 function 组件,它做的就是调用 Consumer 逻辑中的 readContext 函数来获取 context 的值

这部分的详细代码解读可以查看这篇:React 的源码与原理解读(十六):Context 与 useContext

写在后面

以上就是这半年来,作者对于 React v18.1.0 版本的解读,如果后续版本 React 的机制有了一定的更新迭代,这个教程可能会缺乏时效性。本教程的资料和解读部分有来自掘金 ,CSDN,知乎和公开博客网站等各方的资料,也有作者自己的解读和理解,肯定有不正确和不到位的部分,请各位海涵,一下的参考过的资料各位可以自行查阅:

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.js

https://zhuanlan.zhihu.com/p/386897467

https://juejin.cn/post/7171000978278187038

https://www.xiabingbao.com/post/react/reconcile-children-fiber-riezuz.html

https://segmentfault.com/a/1190000015552387

https://xiaochen1024.com/courseware/60b1b2f6cf10a4003b634718/60b1b581cf10a4003b63472c

https://www.xiabingbao.com/post/react/react-element-jsx-rfl0yh.html

https://react.jokcy.me/book/api/react-children.html

https://juejin.cn/post/6844903873077837831

https://www.yuque.com/weixiaofuyanxintong-2tkrh/wzur6d/bnan90#mdMtM

如果有发现教程中有任何问题或者有疑问的,都欢迎私信作者。

作者的 Git:https://github.com/aiai0603

后续作者也会更新一些其他的前端教程,欢迎大家关注!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/677362.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

django中模板的使用

django中模板的使用 第一步 创建模板文件夹第二步 把模板存放进去第三步 把模板路径 加入到setting.py第四步 在视图函数处理第五步 路由挂载第六步 网页访问 第一步 创建模板文件夹 在项目的同层级下 新建模板文件夹 第二步 把模板存放进去 index.html <!DOCTYPE html&…

【Docker】一文了解Docker

文章目录 什么是Docker?为什么要使用Docker&#xff1f;与虚拟机的比较Docker架构Docker使用场景Docker安装阿里云镜像加速器1、登录阿里云2、找到镜像加速器3、配置使用 如今Docker的使用已经非常普遍&#xff0c;特别在一线互联网公司。使用Docker技术可以帮助企业快速水平扩…

C++ 自己动手实现简单的文件操作 (2023.6.23)

C 自己动手实现简单的文件操作 2023.6.23 引言1、文件简介2、各式各样的文件格式2.1 不同类型文件的扩展名2.1.1 文本文件2.1.2 数据文件2.1.3 音频文件2.1.4 视频文件2.1.5 电子书文件2.1.6 3D图像文件2.1.7 位图文件2.1.8 矢量图文件2.1.9 相机原始文件2.1.10 页面布局文件2.…

自监督对比学习框架SimCLR原理

目录 一、前言 人工智能发展近况 对比学习 二、数据集介绍 STL-10数据集 三、无监督图像表征对比学习 SimCLR SimCLR算法基本原理 数据增强与正负样本匹配 编码器 损失函数 对比学习全过程 四、有监督的图像下游任务迁移 替换下游任务网络层 有监督训练 五、实…

环境配置 | Git的安装及配置[图文详情]

Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从小到大的项目版本管理。下面介绍了基础概念及详细的用图文形式介绍一下git安装过程. 目录 1.Git基础概念 2.Git的下载及安装 3.常见的git命令 Git高级技巧 Git与团队协作 1.Git基础概念 仓库&#…

Charm-Crypto在Anaconda虚拟环境下的安装教程--基于Ubuntu20.04

第零步 VMware虚拟机设置和安装Anaconda虚拟环境 因为后面要编译源码&#xff0c;所以最好把CPU设置为最大&#xff0c;例如我的电脑是4核8线程&#xff0c;则&#xff1a; 关于Anaconda虚拟环境&#xff0c;这里不再赘述&#xff0c;后面都假设已经安装好虚拟环境&#xff0c;…

包装类--Math 类--Arrays 类--System 类

包装类–Math 类–Arrays 类–System 类 包装类 包装类的分类 包装类和基本数据的转换 演示包装类和基本数据类型的相互转换&#xff0c;这里以int和Integer演示。 1&#xff09;jdk5前的手动装箱和拆箱方式&#xff0c;装箱&#xff1a;基本类型&#xff0d;>包装类型&…

OpenAI收费标准,ChatGPT调用须知!

OpenAI收费标准&#xff0c;ChatGPT调用须知&#xff01; 免费镜像站价格说明GPT4GPT3.5图片模型如何付费 免费镜像站 ChatGPT有很多镜像站&#xff0c;需要输入API-KEY才可以使用&#xff0c;镜像站不会进行收费&#xff0c;而是OpenAI会对您进行收费。本文主要说明OpenAI的收…

【好书精读】网络是怎样连接的 —— IP 与以太网的包收发操作

&#xff08; 该图由AI制作 &#xff09; 目录 包的基本知识 包收发操作概览 生成包含接收方 IP 地址的 IP 头部 生成以太网用的 MAC 头部 通过 ARP 查询目标路由器的 MAC 地址 以太网的基本知识 将 IP 包转换成电或光信号发送出去 给网络包再加 3 个控制数据 向集线…

代码随想录算法训练营第四十一天 | 背包问题(一维、二维)、416. 分割等和子集

01背包&#xff1a;n种物品&#xff0c;每种物品只有1个&#xff0c;有相应的重量和价值 最多只能装m的重量&#xff0c;最多价值为多少&#xff1f; dp[i][j] : [0, i]物品任取放进容量为j的背包里 不放物品i&#xff1a;dp[i-1][j] 放物品i&#xff1a;dp[i-1][j-weight[…

如何系统性的学习Python语言

零基础同学的福音来了&#xff0c;如果你对Python语言的学习感兴趣&#xff0c;接下来可以由浅入深的了解下Python语言&#xff0c;哪怕你是零基础的小白也完全可以学会的&#xff0c;最后也会给大家放出学习和实例相结合的教程及方法&#xff0c;给到各位同学系统性的教学&…

ES-索引管理

前言 数据类型 ​ 搜索引擎是对数据的检索&#xff0c;所以我们先从生活中的数据说起。我们生活中的数据总体分为两种&#xff1a; 结构化数据非结构化数据 结构化数据&#xff1a; 也称作行数据&#xff0c;是由二维表结构来逻辑表达和实现的数据&#xff0c;严格地遵循数…

<C语言> 数组

1.一维数组的创建和初始化。 1.1 数组的创建 数组是一组相同类型元素的集合。 使用以下方式声明一个一维数组&#xff1a; type arrayName[arraySize];type是数组中元素的类型&#xff0c;arrayName是数组的名称&#xff0c;arraySize是数组的大小&#xff08;即元素的个数&a…

linux系统中如何制作rootfs?详细教程

如何制作rootfs&#xff1f;安排&#xff01;想直奔主题的&#xff0c;直接跳到第四部分。 一、分析 1. 文件系统简介 理论上说一个嵌入式设备如果内核能够运行起来&#xff0c;且不需要运行用户进程的话&#xff0c;是不需要文件系统的&#xff0c;文件系统简单的说就是一种…

硬件入门之什么是mos管

硬件入门之什么是mos管 文章目录 硬件入门之什么是mos管一、mos管是什么&#xff1f;MOS管常用于&#xff1a; 驱动大功率电路中。MOS选型参数mos管调参数 二、实际应用场景1.防反接保护电路&#xff09;2.防过压保护电路3.防反接防过压电路一体电路4.驱动电路 总结 一、mos管是…

VS+QT+VTK三维网格显示-点面选择-法线法向量显示-配准-分割窗体程序

程序示例精选 VSQTVTK三维网格显示-点面选择-法线法向量显示-配准-分割窗体程序 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<VSQTVTK三维网格显示-点面选择-法线法向量显示-配准-分…

Quartz使用H2数据库(嵌入模式)进行持久化

使用H2(嵌入模式)持久化Quartz任务 &#x1f51d;前言&#xff1a; Quartz在包内提供了多种数据库的sql文件&#xff0c;大家可以选择方便的使用。路径如下。 1.初始化h2(不使用Server模式) public class InitH2 {private static String USER_NAME "lee";private…

Unity简单操作:InputSystem获取WASD键盘输入 移动人物

目录 安装InputSystem 在编辑的脚本中使用 InputSystem生成的脚本 Unity版本&#xff1a;2019.2.3f1 安装InputSystem 菜单栏/Window/Package Manager/Input System 工程面板内 右键-->创建Input Actions 选中New Controls改名为PlayerControls 然后属性 面板按下Edit as…

软件项目管理 第七章 软件项目的质量管理与配置管理 课后习题参考答案——主编:李冰、张桥珍、刘玉娥

第七章 软件项目的质量管理与配置管理 课后习题参考答案 1.选择题 (1)项目质量管理的最终责任由谁来承担?&#xff08;D&#xff09; A.项目开发人员 B.采购经理 C.质量经理 D.项目经理 (2)“质量成本”是一个项目管理概念,它说明了下列哪项成本?…

HDFS 写流程源码分析

HDFS 写流程源码分析 一、客户端&#xff08;一&#xff09;文件创建及Pipeline构建阶段&#xff08;二&#xff09;数据写入&#xff08;三&#xff09;输出流关闭 二、NameNode端&#xff08;一&#xff09;create 环境为hadoop 3.1.3 一、客户端 以下代码创建并写入文件。 …