把主题放进 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 文档。