当前位置: 首页 > article >正文

React 源码揭秘 | Effect更新流程

前面的文章介绍了 hooks和commit流程,算是前置知识,这篇来讨论一下useEffect的原理。

useEffect用来处理副作用,比如网络请求,dom操作等等, 其本质也是个hooks,包含hooks的memorizedState, updateQueue, next 

Effect对象

useEfffect这个hooks的memorizedState存储的是Effect对象,其ts定义为

/** 定义effect */
export interface Effect {
  tags: HookEffectTag;
  // 依赖数组
  deps: HookDeps;
  // 传入的创建effect
  create: EffectCallback | null;
  // 清除effect
  destory: EffectCallback | null;
  // next 用来连接在updateQueu中的Effect对象,Effect存在hook.memorizedState和fiber.updateQueue上
  next: Effect;
}

其中,tags定义为HookEffectTag,其为数字类型,定义在hookEffectTag.ts

export const Passive = 0b0010;
// 当前hook存在effect需要处理
export const HookHasEffect = 0b0001;

export type HookEffectTag = number;

其中,Passive表示当前存在useEffect,而HookHasEffect表示当前useEffect在本次更新中需要处理。

deps就是依赖数组,类比useMemo useCallback

create是useEffect传入的create函数

destory是create函数返回的销毁函数

next表示下一个Effect对象

虽然useEffect本身也是个Hook,但是其hook节点的updateQueue是没有被使用的。useEffect将其Effect对象保存在memorizedState,并且使用了当前渲染Fiber上的updateQueue来存储Effect链,结构如下:

FCUpdateQueue

其中,对于函数类型的Fiber,其updateQueue的作用就是存储当前函数组件挂载的Effect对象,其使用的是FCUpdateQueue 是专门给函数组件提供呢更新队列,其继承updateQueue,多了lastEffect指针,指向Effect链(其本质也是环)。如下:

/** 函数组件专用的UpdateQueue增加了lastEffect 指向当前收集到的Effect */
export class FCUpdateQueue<State> extends UpdateQueue<State> {
  public lastEffect: Effect | null = null;
}

useEffect工作原理

每次更新的时候,mountEffect或者updateEffect都会检查当前的Effect是否被执行

  •  如果需要被执行,则将其放到Fiber对象的updateQueue中,并且将其tag设置为Passive|HookHasEffect
  • 如果不需要被执行,也需要将其推入updateQueue,只是其tag设置为Passive 代表有副作用,但是不执行

给当前的Fiber节点,设置PassiveEffect的flag

当commitMutation阶段时,会检查节点的Flag,如果包含passiveEffect,就会将当前节点的updateQueue推入root.pendingPassiveEffect数组中,你当前可以直接的将其理解为一个数组

在commitPassive阶段,会读取root.pendingPassiveEffect,依次执行FCUpdateQueue的内容

下面我们来看mount/updateEffect函数做了什么

mountEffect

挂载阶段,useEffect本质上执行的是mountEffect,其步骤如下:

1. 获取当前hook对象

2. 给当前的currentRenderingFiber.flag 设置PassiveEffect 表示当前Fiber对象包含Effect

3. 创建Effect对象,保存进creat函数和tag,并且将其分别保存到memorizedState和fiber.updateQueue

实现如下:

/** 挂载Effect */
function mountEffect(
  create: EffectCallback,
  deps: HookDeps
): EffectCallback | void {
  /** effect 在hook中的存储方式是:
   *  hook:
   *     memorizedState = Effect
   *     updateQueue = null
   *     next = nextHook
   *  fiber:
   *     updateQueue -> Effect1 -next-> Effect2 -...
   */

  // 获取到hook
  const hook = mountWorkInProgressHook();
  // 给fiber设置PassiveEffect 表示存在被动副作用
  (currentRenderingFiber as FiberNode).flags |= PassiveEffect;
  hook.memorizedState = pushEffect(
    // 初始化状态下,所有的useEffect都执行,所以这里flag设置为   Passive|HookHasEffect
    Passive | HookHasEffect,
    create,
    null,
    deps
  );
}

为什么要设置 fiber.flags?

因为effect真正执行的阶段在commit的passive阶段,而其收集阶段在commit的mutation阶段,所以render阶段时,需要在fiber上设置PassiveMask标记,表示当前fiber节点有副作用需要处理,commit阶段才会收集!

需要注意的是,mount阶段的tags为Passive | HookHasEffect 代表挂载阶段的所有Effect都会线执行一次!

其中,创建Effect和挂载到fiber.updateQueue的操作是pushEffect完成的!

pushEffect

pushEffect函数创建一个Effect对象,并且为其设置mask create destory deps依赖这些参数,可以将其看成是Effect对象的构造器。

同时,pushEffect还会将创建的Effect对象挂载到当前渲染Fiber的updateQueue上 并且返回这个Effect对象,实现如下:

/** 创建Effect对象,把effect加入到fiber.updateQueue 并且返回创建的Effect */
function pushEffect(
  tags: Flags,
  create: EffectCallback | null,
  destory: EffectCallback | null,
  deps: HookDeps
) {
  const effect: Effect = {
    tags,
    create,
    destory,
    deps: deps === undefined ? null : deps,
    next: null,
  };

  const updateQueue = currentRenderingFiber.updateQueue;

  if (!updateQueue || !(updateQueue instanceof FCUpdateQueue)) {
    // 创建一个FCUpdateQueue
    const fcUpdateQueue = new FCUpdateQueue<Effect>();
    effect.next = effect; // 构建环
    fcUpdateQueue.lastEffect = effect;
    currentRenderingFiber.updateQueue = fcUpdateQueue;
  } else {
    // 已经存在 FCUpdateQueue 添加 后加环
    const fcUpdateQueue =
      currentRenderingFiber.updateQueue as FCUpdateQueue<Effect>;
    if (fcUpdateQueue.lastEffect) {
      effect.next = fcUpdateQueue.lastEffect.next;
      fcUpdateQueue.lastEffect.next = effect;
      fcUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}

pushEffect会检查,如果currentRenderingFiber.updateQueue为null (在renderWithHook时,会将updateQueue置为空以重置Effect) 则先创建一个FCUpdateQueue再加入,如果已经存在就直接加入。

updateEffect

更新阶段,由于fiber.updateQueue被清空,所以需要根据hook.memorizedState存储的Effect对象信息,重新判断哪些Effect需要重新执行。

判断是否要重新加入的依据就是deps,Effect存储了上一次的deps,updateEffect比较当前的deps和上一次prevDeps是否相等来判断是否需要重新执行。

判断方式为areInputDepsEqual, 前面有讲过。

  • 如果需要执行,则把Effect的tags改成 Passive | HookHasEffect 推入updateQueue
  • 如果不需要执行,则把Effect的tags改成Passive并且推入updateQueue

同时,在mount阶段,由于create函数还没有运行,所以无法获得destory函数,但是子啊update阶段,create函数已经在Passive Commit阶段运行过了,commit阶段会将create的返回值destory存入Effect对象中,直接获取即可,

也就是说,不论是mount还是update阶段,所有的Effect都会被挂载fiber.updateQueue上,其是否执行通过Effect.tag是否包含HookHasEffect这个HookEffectTag判断!

注意,更新阶段如果有需要执行的effect,也需要吧fiber.flags merge passiveEffect

实现如下:

/** 更新Effect */
function updateEffect(
  create: EffectCallback,
  deps: HookDeps
): EffectCallback | void {
  // 获取当前hook
  const hook = updateWorkInProgressHook();
  const prevDeps = hook.memorizedState.deps;
  const destory = hook.memorizedState.destory;
  if (areHookInputsEqual(prevDeps, deps)) {
    // 相等 pushEffect 并且设置tag为Passive 被动副作用
    hook.memorizedState = pushEffect(
      Passive,
      create,
      // 前一个副作用hook的destory
      destory,
      deps
    );
  } else {
    /** 不等 表示hook有Effect */
    hook.memorizedState = pushEffect(
      Passive | HookHasEffect, // 注意这里是 Passive 是Effect的tag 区分fiber的tag PassiveEffect
      create,
      // 前一个副作用hook的destory
      destory,
      deps
    );
  }
  (currentRenderingFiber as FiberNode).flags |= PassiveEffect;
}

Effect收集阶段

effect的收集在commit阶段完成,我们上篇说了,如果某个Fiber节点存在需要执行的Effect,则其PassiveEffect的flag就会在completeWork阶段被冒泡到root节点。

commitRoot阶段,通过判断root节点是否有PassiveEffect的flags或subTreeFlags来判断是否开启Passive流程!

  /** 设置调度 执行passiveEffect */ty
  /** 真正执行会在commit之后 不影响渲染 */
  /** commit阶段会收集effect到root.pendingPassiveEffect */
  // 有删除 或者收集到Passive 都运行
  if (
    (finishedWork.flags & PassiveMask) !== NoFlags ||
    (finishedWork.subTreeFlags & PassiveMask) !== NoFlags
  ) {
    // 调度副作用
    scheduler.scheduleCallback(
      PriorityLevel.NORMAL_PRIORITY,
      flushPassiveEffect.bind(null, root.pendingPassiveEffects)
    );
  }

此时root.pendingPassiveEffect为空,需要在Mutation阶段收集

export const commitMutationEffects = commitEffect(
  "mutation",
  MutationMask | PassiveMask,
  commitMutationEffectsOnFiber
);

可以看到commitMutationEffects中 mash包含了PassiveMask

pendingPassiveEffect的结构

root.pendingPassiveEffect的结构如下:

export interface PendingPassiveEffect {
  // 更新的effect
  update: Effect[];
  // 卸载的effect
  unmount: Effect[];
}

可以看到,其包含update和unmount两个数组,分别存储本次更新收集到的需要更新的effect和需要卸载的effect,在每次Passive Commit阶段接诉后,pendingPassEffect都会被置空! 

收集的几个地方

通过pendingPassEffect的结构就能看出,Effect的收集阶段分别发生在

1. 函数组件的卸载阶段

2. 函数组件的更新阶段

副作用的收集由commitPassiveEffect完成,其定义如下:

/** 收集被动副作用,这个函数可能会在
 *  1. commitMutationEffectsOnFiber调用
 *  2.  在delection时调用
 */
function commitPassiveEffect(
  fiber: FiberNode,
  root: FiberRootNode,
  type: "update" | "unmount"
) {
  if (fiber.tag !== FunctionComponent) return;
  if (type === "update" && (fiber.flags & PassiveEffect) === NoFlags) return;
  const fcUpdateQueue = fiber.updateQueue as FCUpdateQueue<Effect>;
  if (fcUpdateQueue && fcUpdateQueue.lastEffect) {
    // 收集effect
    root.pendingPassiveEffects[type].push(fcUpdateQueue.lastEffect);
  }
}

可以看到,其逻辑就是把当前存在PassiveEffect的Fiber节点的FCUpdateQueue.lastEffect 存入对应的update unmount数组,对于卸载阶段,是允许其flag不存在PassiveEffect的,因为卸载函数组件要destory其所有的effect

在commitMutationEfffectOnFiber 中,调用次函数,用来收集update阶段的effect

  if ((flags & PassiveEffect) !== NoFlags) {
    // 存在被动副作用
    commitPassiveEffect(finishedWork, root, "update");
  }

在commitDeletion阶段中,调用次函数用来收集unmount阶段的effect

    if (childToDelete.tag === FunctionComponent) {
      /** 函数组件的情况下,需要收集Effect */
      commitPassiveEffect(childToDelete, root, "unmount");
    }

在完成了Mutation Commit阶段之后,root.pendingPassiveEffect 就分别收集到了更新和卸载阶段的FCUpdateQueue

Effect的执行

effect的执行发生在Passive Commit阶段,我们前面说了,在Mutation阶段开始之前,会把一个flushPassiveEffect函数交给scheduler调度,以便在Mutation之后异步执行,其实现如下

function flushPassiveEffect(pendingPassiveEffect: PendingPassiveEffect) {
  // 处理卸载 把所有的Passive flag的effect都执行destor
  pendingPassiveEffect.unmount.forEach((unmountEffect) => {
    commitHookEffectListUnmount(Passive, unmountEffect);
  });
  pendingPassiveEffect.unmount = [];
  // 处理update 的destory flag为Passive|HookHasEffect
  pendingPassiveEffect.update.forEach((updateEffect) => {
    commitHookEffectListDestory(Passive | HookHasEffect, updateEffect);
  });
  // 处理update的create flag为Passive| HookHasEffect
  pendingPassiveEffect.update.forEach((updateEffect) => {
    commitHookEffectListCreate(Passive | HookHasEffect, updateEffect);
  });
  pendingPassiveEffect.update = [];
}

其实本质就是按顺序处理pendingPasiveEffect

首先处理卸载的effect -> 再处理更新的effect的destory -> 再处理更新effect的create

最后清空pendingPassiveEffect 以方便下次收集

我们首先看更新阶段处理create 其定义在commitHookEffectListCreate

function commitHookEffectList(
  flags: HookEffectTag,
  lastEffect: Effect | null,
  callback: (effect: Effect) => void
) {
  let currentEffect = lastEffect.next;
  do {
    if ((flags & currentEffect.tags) === flags) {
      // flag必须完全相等 执行callback
      callback(currentEffect);
    }
    currentEffect = currentEffect.next;
  } while (currentEffect !== lastEffect.next);
}

/** 执行创建的effect */
export function commitHookEffectListCreate(
  flags: HookEffectTag,
  lastEffect: Effect | null
) {
  commitHookEffectList(flags, lastEffect, (effect) => {
    const create = effect.create;
    if (typeof create === "function") {
      // 设置destory
      effect.destory = create() as EffectCallback;
    }
  });
}

可以看到,其本质就是遍历pendingPassiveEffect.update,找到哪些包含HookHasEffect的Effect对象,执行其create函数,并且把返回值作为destory存入effect对象。

commitHookEffectListDestory,同样是遍历,找到包含HookHasEffect tag的Effect对象,执行其destory函数 如下:

/** 执行destory的effect */
export function commitHookEffectListDestory(
  flags: HookEffectTag,
  lastEffect: Effect | null
) {
  commitHookEffectList(flags, lastEffect, (effect) => {
    const destory = effect.destory;
    if (typeof destory === "function") {
      destory();
    }
  });
}

 最后在看卸载的情况,即commitHookEffectListUnmount 把unmount中所有的effect都执行一遍,并且去除其tag上的HookHasEffect标记(如果有的话)

/** 执行卸载的effect */
export function commitHookEffectListUnmount(
  flags: HookEffectTag,
  lastEffect: Effect | null
) {
  commitHookEffectList(flags, lastEffect, (effect) => {
    const destory = effect.destory;
    if (typeof destory === "function") {
      destory();
    }
    effect.tags &= ~HookHasEffect;
  });
}

这样就完成了一次更新的Effect的运行,最后清空pendingPassiveEffect即可

所以,Effect的执行一定是从下到上的,因为commit Mutation是在归的阶段执行,收集的Effect自然也是从下到上的!

 

 

 

 

 

 


http://www.kler.cn/a/568046.html

相关文章:

  • Unity小功能实现:鼠标点击移动物体
  • Spring AI:让AI应用开发更简单
  • 回归预测 | Matlab基于SSA-BiLSTM-Attention的数据多变量回归预测(多输入单输出)
  • AI人工智能机器学习之神经网络
  • springBoot连接远程Redis连接失败(已解决)
  • 最新Git入门到精通完整教程
  • Python办公自动化教程(008):设置excel单元格边框和背景颜色
  • Windows 11 下正确安装 Docker Desktop 到 D 盘的完整教程
  • EasyRTC嵌入式WebRTC技术与AI大模型结合:从ICE框架优化到AI推理
  • 基于 SSM+Vue的 车辆管理系统 系统的设计与实现
  • Brave 132 编译指南 Android 篇 - 配置编译环境 (五)
  • 从JSON过滤到编程范式:深入理解JavaScript数据操作
  • MySQL在线、离线安装
  • 蓝桥杯备考:DFS剪枝之数的划分
  • 机器学习数学基础:33.分半信度
  • 区块链的原理、技术与应用场景
  • 金融项目管理:合规性与风险管理的实战指南
  • C#上位机--关键字
  • 松灵机器人地盘 安装 ros 驱动 并且 发布ros 指令进行控制
  • [Windows] 批量为视频或者音频生成字幕 video subtitle master 1.5.2