入解析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 英雄在 赛道系统 指挥下光速帮你处理的哦!⚡

相关推荐
小吕学编程5 分钟前
ES练习册
java·前端·elasticsearch
Asthenia041213 分钟前
Netty编解码器详解与实战
前端
袁煦丞17 分钟前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛1 小时前
vue组件间通信
前端·javascript·vue.js
一笑code1 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员2 小时前
layui时间范围
前端·javascript·layui
NoneCoder2 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19702 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端
烛阴2 小时前
面试必考!一招教你区分JavaScript静态函数和普通函数,快收藏!
前端·javascript
GetcharZp2 小时前
xterm.js 终端神器到底有多强?用了才知道!
前端·后端·go