入解析React性能优化策略:eagerState的工作原理

性能优化策略之eagerState

面试题:谈一谈 React 中的 eagerState 策略是什么?

在 React 中,有很多和性能优化相关的 API:

  • shouldComponentUpdate
  • PureComponent
  • React.memo
  • useMemo
  • useCallback

实际上,开发者调用上面的 API,内部是在命中 React 的性能优化策略:

  • eagerState
  • bailout
jsx 复制代码
import { useState } from "react";
​
// 子组件
function Child() {
  console.log("child render");
  return <span>child</span>;
}
​
// 父组件
function App() {
  const [num, updateNum] = useState(0);
  console.log("App render", num);
​
  return (
    <div onClick={() => updateNum(1)}>
      <Child />
    </div>
  );
}

在上面的代码中,渲染结果如下:

首次渲染:

jsx 复制代码
App render 0
child render

第一次点击

jsx 复制代码
App render 1
child render

第二次点击

jsx 复制代码
App render 1

第三次以及之后的点击

不会有打印

上面的这个例子实际上就涉及到了我们所提到的 React 内部的两种性能优化策略:

  • 在第二次打印的时候,并没有打印 child render,此时实际上是命中了 bailout 策略。命中该策略的组件的子组件会跳过 reconcile 过程,也就是说子组件不会进入 render 阶段。
  • 后面的第三次以及之后的点击,没有任何输入,说明 App、Child 都没有进入 render 阶段,此时命中的就是 eagerState 策略,这是一种发生于触发状态更新时的优化策略,如果命中了该策略,此次更新不会进入 schedule 阶段,更不会进入 render 阶段。

eagerState 策略

该策略的逻辑其实是很简单:如果某个状态更新前后没有变化,那么就可以跳过后续的更新流程。

state 是基于 update 计算出来的,计算过程发生在 render 的 beginWork,而 eagerState 则是将计算过程提前到了 shcedule 之前执行。

该策略有一个前提条件,那就是当前的 FiberNode 不存在待执行的更新,因为如果不存在待执行的更新,那么当前的更新就是第一个更新,那么计算出来的 state 即便有变化也可以作为后续更新的基础 state 来使用。

例如,在使用 useState 触发更新的时候,对应的 dispatchSetState 逻辑如下:

jsx 复制代码
if (
  fiber.lanes === NoLanes &&
  (alternate === null || alternate.lanes === NoLanes)
) {
  // 队列当前为空,这意味着我们可以在进入渲染阶段之前急切地计算下一个状态。 如果新状态与当前状态相同,我们或许可以完全摆脱困境。
  const lastRenderedReducer = queue.lastRenderedReducer;
  if (lastRenderedReducer !== null) {
    let prevDispatcher;
    try {
      const currentState = queue.lastRenderedState; // 也就是 memoizedState
      const eagerState = lastRenderedReducer(currentState, action); // 基于 action 提前计算 state
      // 将急切计算的状态和用于计算它的缩减器存储在更新对象上。 
      // 如果在我们进入渲染阶段时 reducer 没有改变,那么可以使用 eager 状态而无需再次调用 reducer。
      update.hasEagerState = true; // 标记该 update 存在 eagerState
      update.eagerState = eagerState; // 存储 eagerState 的值
      if (is(eagerState, currentState)) {
        // ...
        return;
      }
    } catch (error) {
      // ...
    } finally {
      // ...
    }
  }
}

在上面的代码中,首先通过 lastRenderedReducer 来提前计算 state,计算完成后在当前的 update 上面进行标记,之后使用 is(eagerState, currentState) 判断更新后的状态是否有变化,如果进入 if,说明更新前后的状态没有变化,此时就会命中 eagerState 策略,不会进入 schedule 阶段。

即便不为 true,由于当前的更新是该 FiberNode 的第一个更新,因此可以作为后续更新的基础 state,因此这就是为什么在 FC 组件类型的 update 里面有 hasEagerState 以及 eagerState 字段的原因:

jsx 复制代码
const update = {
  hasEagerState: false,
  eagerState: null,
  // ...
}

在上面的示例中,比较奇怪的是第二次点击,在第二次点击之前,num 已经为 1 了,但是父组件仍然重新渲染了一次,为什么这种情况没有命中 eagerState 策略?

FiberNode 分为 current 和 wip 两种。

在上面的判断中,实际上会对 current 和 wip 都进行判断,判断的条件为两个 Fiber.lanes 必须要为 NoLanes

jsx 复制代码
if (
  fiber.lanes === NoLanes &&
  (alternate === null || alternate.lanes === NoLanes)
){
  // ....
}

对于第一次更新,当 beginWork 开始前,current.lanes 和 wip.lanes 都不是 NoLanes。当 beginWork 执行后, wip.lanes 会被重置为 NoLanes,但是 current.lanes 并不会,current 和 wip 会在 commit 阶段之后才进行互换,这就是为什么第二次没有命中 eagerState 的原因。

那么为什么后面的点击又命中了呢?

虽然上一次点击没有命中 eagerState 策略,但是命中了 bailout 策略,对于命中了 bailout 策略的 FC,会执行 bailoutHooks 方法:

jsx 复制代码
function bailoutHooks(
  current: Fiber,
  workInProgress: Fiber,
  lanes: Lanes,
) {
  workInProgress.updateQueue = current.updateQueue;
  // ...
  current.lanes = removeLanes(current.lanes, lanes);
}

在执行 bailoutHooks 方法的时候,最后一句会将当前 FiberNode 的 lanes 移除,因此当这一轮更新完成后,current.lanes 和 wip.lanes 就均为 NoLanes,所以在后续的点击中就会命中 eagerState 策略。

真题解答

题目:谈一谈 React 中的 eagerState 策略是什么?

参考答案:

在 React 内部,性能优化策略可以分为:

  • eagerState 策略
  • bailout 策略

eagerState 的核心逻辑是如果某个状态更新前后没有变化,则可以跳过后续的更新流程。该策略将状态的计算提前到了 schedule 阶段之前。当有 FiberNode 命中 eagerState 策略后,就不会再进入 schedule 阶段,直接使用上一次的状态。

该策略有一个前提条件,那就是当前的 FiberNode 不存在待执行的更新,因为如果不存在待执行的更新,当前的更新就是第一个更新,计算出来的 state 即便不能命中 eagerState,也能够在后面作为基础 state 来使用,这就是为什么 FC 所使用的 Update 数据中有 hasEagerState 以及 eagerState 字段的原因。

简单理解一下概念

标题:React 的小聪明:为什么有时候它懒得干活?

简单版解释: React 就像一个小助手,负责更新你网页上的内容。但它有时候很聪明,会偷懒不干活!

  1. 什么时候偷懒?

    • 如果你让它改一个数字,但它发现改完还是和原来一样,它就会说:"哼,反正没变化,我不干了!"(这就是 eagerState 策略)。
    • 如果爸爸组件没变,它也会跳过检查孩子组件,直接说:"孩子不用管了!"(这叫 bailout 策略)。
  2. 为什么第一次点按钮它没偷懒?

    • 因为 React 刚开始有点忙,没空检查数字变没变,所以先干完活再说。但第二次发现数字没变,就真的偷懒了!
  3. 偷懒是好事吗?

    • 是的!这样网页不会卡,速度更快,就像你不用重新写作业如果答案没改一样!

总结: React 很聪明,能少干活就少干,让你的网站跑得更快!

好的!让我们用一个有趣的玩具工厂故事来解释 eagerState 在 React 源码里是做什么的!🎢


🧸 玩具工厂的故事

想象你有一个玩具工厂(React),里面有很多工人(React 的代码)。每次你想改变玩具(状态 state),比如把小汽车🚗的颜色从红色变成蓝色,工厂需要决定:

  1. 立刻重新涂色(Eager State)

    • 如果 React 很确定 这次改变不会影响其他玩具(比如其他 state 或组件),它就会 立刻 把颜色改掉,不浪费时间!
    • 就像你只涂一辆车,不用检查整个工厂。
  2. 先等等,可能还要改别的(Lazy State)

    • 如果 React 不确定 会不会影响其他东西(比如其他 state 或组件),它会先记下来,等到合适的时候再一起改。

🔍 React 源码里的秘密

在 React 的代码里,eagerState 是一个优化技巧!它会在 真正更新组件之前 偷偷检查:

perl 复制代码
if (新state === 当前state && React不依赖其他东西) {
  直接用它!不用排队等更新!🚀
} else {
  放进更新队列,晚点处理... 🐢
}

这样 React 就能跳过没必要的计算,让你的玩具工厂(App)跑得更快!⚡


🌟 总结给小朋友

eagerState = 如果能立刻改,就马上改!不然就等等。 就像你玩积木时,如果只换一块积木,不用拆整个塔!😃

好的!让我们用一个 超级英雄变身 的故事来解释 FiberNode 的 currentwip(Work-In-Progress)!🦸♂️🦸♀️


🎭 超级英雄的"双重身份"

想象 React 就像一个超级英雄,但它 有两个身份

  1. current(当前英雄)

    • 这是 现在正在保护城市(显示在屏幕上) 的英雄。
    • 比如,钢铁侠正在和坏人打架,大家看到的 就是他现在的样子
  2. wip(Work-In-Progress,准备中的英雄)

    • 这是 正在后台偷偷升级 的英雄!
    • 比如,钢铁侠正在实验室里换新装甲,但 还没上场,大家还看不到!

🔧 React 是怎么用它们的?

当 React 要更新界面(比如改变按钮颜色),它不会直接改 current,而是:

  1. 先复制一个 current(克隆现在的钢铁侠)。
  2. wip 上偷偷修改(给克隆体换新装甲)。
  3. wip 完全准备好了 ,再让它 替换 current(新钢铁侠上场!)。

这样,屏幕不会突然卡住,用户也不会看到"半成品"!✨


📜 React 源码里的样子

在代码里,FiberNode 就像这样:

ini 复制代码
let currentFiber = { type: 'IronMan', armor: 'Mark50' }; // 现在的英雄
let wipFiber = { ...currentFiber, armor: 'Mark85' };    // 升级中的英雄

wipFiber 准备好了,React 就会做一次 "英雄交接"

ini 复制代码
currentFiber = wipFiber; // 新装甲钢铁侠正式登场!

🌟 总结给小朋友

  • current = 现在屏幕上的英雄(当前界面)。
  • wip = 后台偷偷准备的升级版英雄(下次要显示的界面)。 React 用这种方式让动画更流畅,不会让屏幕闪来闪去!🚀

好的!让我们把 超级英雄 🦸和 赛车赛道 🏎️ 结合起来,变成一个更酷的故事!


🌟 超级英雄的"任务优先级"系统(Lanes)

想象你的 React 超级英雄(FiberNode)现在不光有 双重身份 (current 和 wip),还接到了一个 任务清单 !但英雄也知道:不是所有任务都一样紧急!

🚨 紧急任务(快车道任务)
  • 例子:用户突然点击了按钮,坏人入侵城市!

  • 英雄的反应

    • 立刻放下所有事情,优先处理!(就像按了警报器 🔴)
    • 在 React 里,这叫 SyncLaneInputLane(同步/输入赛道)。
🐢 普通任务(慢车道任务)
  • 例子:更新天气预报、加载一张大图片。

  • 英雄的反应

    • "等打完坏人再说吧~" 😴
    • 在 React 里,这叫 DefaultLaneIdleLane(默认/空闲赛道)。

🦸 英雄如何管理任务?(React 源码逻辑)

  1. 任务来了!

    • 比如用户点击了按钮(onClick),React 会标记这个任务为 高优先级(快车道)
    jsx 复制代码
    const taskLane = requestUpdateLane(); // 决定任务放哪个赛道
  2. 检查赛道

    • React 会问:"现在有没有更急的任务?"
    jsx 复制代码
    if (currentPriorityLane > newLane) {
      // 有更急的!先处理那个!
    } else {
      // 可以处理这个新任务~
    }
  3. 执行任务

    • 如果是 紧急任务 ,英雄(React)会立刻用 最快速度 更新界面(甚至可能打断其他任务!)。
    • 如果是 普通任务,就等英雄有空了(比如动画播完、浏览器空闲时)再处理。

🌍 为什么需要"赛道"(Lanes)?

  • 如果没有赛道,英雄可能 先做不重要的事(比如加载图片),而让用户点了按钮没反应!😡
  • 有了赛道,React 就像 智能指挥官 ,永远让英雄先做 最影响用户体验的事

(在源码里,lanes 是用二进制位(0b00010b0010)表示的,就像英雄的"任务徽章" 🎖️,每个数字代表不同优先级!)


🎯 总结给小朋友

  • currentwip = 英雄的"现在身份"和"准备中的新身份"。
  • lanes = 英雄的任务清单,排好谁先谁后,保证最急的事先做!
  • 这样,你的 App 就像 一个聪明的超级英雄,又快又靠谱!💪

下次点按钮时,记得是 React 英雄在 赛道系统 指挥下光速帮你处理的哦!⚡

相关推荐
&白帝&3 小时前
前端实现截图的几种方法
前端
动能小子ohhh3 小时前
html实现登录与注册功能案例(不写死且只使用js)
开发语言·前端·javascript·python·html
小小小小宇3 小时前
大文件断点续传笔记
前端
Jimmy4 小时前
理解 React Context API: 实用指南
前端·javascript·react.js
保持学习ing4 小时前
SpringBoot电脑商城项目--显示勾选+确认订单页收货地址
java·前端·spring boot·后端·交互·jquery
李明一.5 小时前
Java 全栈开发学习:从后端基石到前端灵动的成长之路
java·前端·学习
观默5 小时前
我用AI造了个“懂我家娃”的育儿助手
前端·人工智能·产品
crary,记忆5 小时前
微前端MFE:(React 与 Angular)框架之间的通信方式
前端·javascript·学习·react.js·angular
星空寻流年5 小时前
javaScirpt学习第七章(数组)-第一部分
前端·javascript·学习
烛阴6 小时前
Python多进程开发实战:轻松突破GIL瓶颈
前端·python