•
阅读时长 3 分钟

Context 里的颜色没变,为什么组件还是更新了

目录

把主题放进 Context 很方便,但父组件里一个无关计数变化,也可能让读主题的组件重新渲染。原因不一定是 Context 传得太远,有时候只是每次都创建了一个新的 value 对象。

下面放两组相同的组件:一组直接传 { color },另一组用 useMemo 保持对象引用。消费者都用 memo 包一下,排除普通父组件更新带来的影响。

创建一个空目录,安装 React 和打包工具。示例不用 JSX,省去 Babel 配置,组件逻辑和 JSX 的写法是一样的。

npm init -y
npm install --save-exact react@18.3.1 react-dom@18.3.1
npm install --save-dev --save-exact webpack@5.99.9

创建 build.cjs:

const webpack = require("webpack");
const path = require("path");
webpack(
  {
    mode: "development",
    entry: path.resolve("main.js"),
    output: { path: path.resolve("dist"), filename: "main.js" },
    devtool: "source-map",
  },
  (error, stats) => {
    if (error || stats.hasErrors()) {
      console.error(error || stats.toString("errors-only"));
      process.exitCode = 1;
    }
  },
);

创建 index.html:

<!doctype html>
<html lang="zh-CN">
  <meta charset="utf-8" /><meta name="viewport" content="width=device-width" />
  <title>React 实验</title>
  <div id="root"></div>
  <script src="./dist/main.js"></script>
</html>

把下面的代码存为 main.js,运行 node build.cjs,然后用浏览器打开 index.html。这里用开发构建和根节点 StrictMode,观察次数时要把这两个条件算进去。

import React, {
  useState,
  useRef,
  useEffect,
  useMemo,
  useContext,
  memo,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
const e = React.createElement;
window.lab = { effects: [], contexts: { stable: 0, unstable: 0 }, measurements: [] };
const Theme = React.createContext(null);
const Consumer = memo(function Consumer({ kind }) {
  const { color } = useContext(Theme);
  window.lab.contexts[kind]++;
  return e("p", null, kind + ":" + color);
});
function ContextCase({ stable }) {
  const [count, setCount] = useState(0);
  const [color, setColor] = useState("black");
  const stableValue = useMemo(() => ({ color }), [color]);
  return e(
    Theme.Provider,
    { value: stable ? stableValue : { color } },
    e(
      "div",
      null,
      e(
        "button",
        { id: stable ? "stable" : "unstable", onClick: () => setCount((v) => v + 1) },
        "无关计数 " + count,
      ),
      e(
        "button",
        {
          id: stable ? "stable-color" : "unstable-color",
          onClick: () => setColor((v) => (v === "black" ? "blue" : "black")),
        },
        "换颜色",
      ),
      e(Consumer, { kind: stable ? "stable" : "unstable" }),
    ),
  );
}

createRoot(document.querySelector("#root")).render(
  e(
    StrictMode,
    null,
    e(
      "main",
      null,
      e(ContextCase, { stable: false }),
      e(ContextCase, { stable: true }),
    ),
  ),
);

window.lab.contexts 是调试用的渲染计数,不参与页面状态。在 StrictMode 开发模式下,具体次数可能增加,比较操作前后有没有新增即可。

记录一次计数,分别点两边的无关计数,再查看。直接传对象的一边增加,用 useMemo 的一边没有增加。换颜色后,两边都会更新。

React 用 Object.is 比较 Context 的 value。对象内容一样,但引用变了,也算更新。memo 不能阻止组件读取的 Context 发生变化,稳定引用才解决了这个例子里的无关传播。

value 稳定之后,问题还可能在哪里

如果消费者还收到每次新建的 props,或者父子结构发生其他变化,也可能重新渲染。这个例子把条件缩小了,不代表给 Provider 加 useMemo 就能解决所有问题。

一个 Context 同时放主题、用户和高频编辑状态,任何部分变化都可能通知所有消费者。可以按更新频率和使用范围拆开,也可以把状态放回更近的组件。复杂的数据订阅再考虑专门的状态库。

渲染次数多不一定就卡。真正要优化时,还需要看一次更新做了多少工作,不能只靠控制台次数评价体验。Context 更新规则参考 React useContext 文档。