前言

​createContext​​是 react 提供的用于全局状态管理的一个 api,我们可以通过​​Provider​​组件注入状态,用​​Consumer​​组件或者​​useContext​​api 获取状态(推荐使用​​useContext​​方式,更加简洁)。

​createContext​​让组件间的通信更为方便,但如果使用不当却会带来很大的性能问题。下面我们会讨论引起性能问题的原因以及如何优化。

性能问题的根源

先来看一个例子:​​createContext性能问题原因​​,注意例子中的2个问题点。

import { useState, useContext, createContext } from "react";
import { useWhyDidYouUpdate } from "ahooks";

const ThemeCtx = createContext({});

export default function App() {
const [theme, setTheme] = useState("dark");
/**
* 性能问题原因:
* ThemeCtx.Provider 父组件渲染导致所有子组件跟着渲染
*/

return (
<div className="App">
<ThemeCtx.Provider value={{ theme, setTheme }}>
<ChangeButton />
<Theme />
<Other />
</ThemeCtx.Provider>
</div>
);
}

function Theme() {
const ctx = useContext(ThemeCtx);
const { theme } = ctx;
useWhyDidYouUpdate("Theme", ctx);
return <div>theme: {theme}</div>;
}

function ChangeButton() {
const ctx = useContext(ThemeCtx);
const { setTheme } = ctx;
useWhyDidYouUpdate("Change", ctx);
// 问题2:value 状态中没有改变的值导致组件渲染
console.log("setTheme 没有改变,其实我也不应该渲染的!!!");
return (
<div>
<button
onClick={() => setTheme((v) => (v === "light" ? "dark" : "light"))}
>
改变theme
</button>
</div>
);
}

function Other() {
// 问题1:和 value 状态无关的子组件渲染
console.log("Other render。其实我不应该重新渲染的!!!");
return <div>other组件,讲道理,我不应该渲染的!</div>;
}


问题1(整体重复渲染):​​Provider​​组件包裹的子组件全部渲染

从这个例子可以看出来,用​​ThemeCtx.Provider​​直接包裹子组件,每次​​ThemeCtx.Provider​​组件渲染会导致所有子组件跟着重新渲染,原因是使用​​React.createElement(type, props: {}, ...)​​创建的组件,每次​​props: {}​​都会是一个新的对象。

问题2(局部重复渲染):使用​​useContext​​导致组件渲染

​createContext​​是根据发布订阅模式来实现的,​​Provider​​的​​value​​值每次发生变化都会通知所有使用它的组件(使用​​useContext​​的组件)重新渲染。

解决方案

上面我们分析了问题的根源,下面就开始解决问题。 同样先看一下优化后的例子:​​createContext性能优化​​。

import { useState, useContext, createContext, useMemo } from "react";
import { useWhyDidYouUpdate } from "ahooks";
import "./styles.css";

const ThemeCtx = createContext({});

export default function App() {
return (
<div className="App">
<ThemeProvide>
<ChangeButton />
<Theme />
<Other />
</ThemeProvide>
</div>
);
}

function ThemeProvide({ children }) {
const [theme, setTheme] = useState("dark");

return (
<ThemeCtx.Provider value={{ theme, setTheme }}>
{children}
</ThemeCtx.Provider>
);
}

function Theme() {
const ctx = useContext(ThemeCtx);
const { theme } = ctx;
useWhyDidYouUpdate("Theme", ctx);
return <div>{theme}</div>;
// return <ThemeCtx.Consumer>{({ theme }) => <div>{theme}</div>}</ThemeCtx.Consumer>;
}

function ChangeButton() {
const ctx = useContext(ThemeCtx);
const { setTheme } = ctx;
useWhyDidYouUpdate("Change", ctx);

/**
* 解决方案:使用 useMemo
*
*/
const dom = useMemo(() => {
console.log("re-render Change");
return (
<div>
<button
onClick={() => setTheme((v) => (v === "light" ? "dark" : "light"))}
>
改变theme
</button>
</div>
);
}, [setTheme]);

return dom;
}

function Other() {
console.log("Other render,其实我不应该重新渲染的!!!");
return <div>other,讲道理,我不应该渲染的!</div>;
}


解决问题1

把​​ThemeContext​​抽离出来,子组件通过​​props​​的​​children​​属性传递进来。即使​​ThemeContext.Provider​​重新渲染,​​children​​也不会改变。这样就不会因为​​value​​值改变导致所有子组件跟着重新渲染了。

解决问题2

通过上面的方式可以一刀切的解决整体重复渲染的问题,但局部渲染的问题就比较繁琐了,需要我们用​​useMemo​​一个个的修改子组件,或者使用​​React.memo​​把子组件更加细化。