react 中组件缓存的两种方式:
-
使用
memo缓存子组件jsconst App = () => { const [count, setCount] = useState(0); const onClick = () => setCount(count + 1); return ( <> <div> {count} <button onClick={onClick}>按钮</button> </div> <Child /> </> ); }; const Child = memo(() => { console.log("child render"); return <div>Child</div>; }); -
使用
useMemo缓存组件jsconst App = () => { const [count, setCount] = useState(0); const onClick = () => setCount(count + 1); const ChildMemo = useMemo(() => <Child />, []); return ( <> <div> {count} <button onClick={onClick}>按钮</button> </div> {ChildMemo} </> ); }; const Child = () => { console.log("child render"); return <div>Child</div>; };
这两种方式的区别是:
memo是通过比较prevProps和nextProps是否相同来决定是否更新useMemo是通过依赖是否变化来决定是否更新
什么时候使用 useMemo 缓存组件,什么时候使用 memo 缓存组件呢?
大部分情况都是使用 memo 取缓存组件的,因为 memo 的使用更简单,而且 memo 也是 react 官方推荐的
使用 useMemo 缓存的组件的话,可以用于切换组件的封装,比如下面的代码:
js
{
count % 2 === 0 ? <Child1 /> : <Child2 />;
}
🔽
const element = useMemo(() => (count % 2 === 0 ? <Child1 /> : <Child2 />), [count]);
在使用 memo 缓存组件时有些坑要注意,比如下面这几种
缓存失效1------未使用 useCallback
子组件使用了 memo,但是父组件没有使用 useCallback 包裹 onClick 函数,这样 memo 就失效了
js
function App() {
const [count, setCount] = useState(0);
const onClick = () => console.log("app click");
const onClickAppButton = () => setCount(count + 1);
return (
<div>
app---{count}
<button onClick={onClickAppButton}>app 按钮</button>
<Child onClick={onClick} />
</div>
);
}
const Child = memo(({ onClick }) => {
console.log("child render"); // 父组件更新,子组件也会更新
return <div>Child</div>;
});
因为在父组件中,每次 setCount,都会创建一个新的 onClick 函数,而 memo 是通过比较 prevProps 和 nextProps 来决定是否更新的,所以每次父组件更新,子组件也会更新
正确的写法是,在父组件中将 onClick 使用 useCallback 缓存起来
js
function App() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => console.log("app click"), []);
const onClickAppButton = () => setCount(count + 1);
return (
<div>
app---{count}
<button onClick={onClickAppButton}>app 按钮</button>
<Child onClick={onClick} />
</div>
);
}
const Child = memo(({ onClick }) => {
console.log("child render"); // 父组件更新,子组件不会更新了
return <div>Child</div>;
});
useCallback 错误使用
这种方式不会造成缓存失效,而是拿不到最新的状态
子组件中使用了 useCallback 缓存函数,但是依赖是空数组(或者有依赖忘记写了)
js
function App() {
const [count, setCount] = useState(0);
// count 永远是初始值 0
const onClick = useCallback(() => console.log("app click", count), [count]);
const onClickAppButton = () => setCount(count + 1);
return (
<div>
app---{count}
<button onClick={onClickAppButton}>app 按钮</button>
<Child onClick={onClick} />
</div>
);
}
const Child = memo(({ onClick }) => {
const [childCount, setChildCount] = useState(0);
const onClickChildButton = useCallback(() => {
setChildCount((childCount) => childCount + 1);
onClick();
}, []);
return (
<div>
Child---{childCount}
<button onClick={onClickChildButton}>child 按钮</button>
</div>
);
});
子组件中的依赖是空数组,它无法感知父组件 count 的变化
正确的做法是将将 onClick 放入 useCallback 的依赖项里
js
function App() {
const [count, setCount] = useState(0);
// count 永远是初始值 0
const onClick = useCallback(() => console.log("app click", count), [count]);
const onClickAppButton = () => setCount(count + 1);
return (
<div>
app---{count}
<button onClick={onClickAppButton}>app 按钮</button>
<Child onClick={onClick} />
</div>
);
}
const Child = memo(({ onClick }) => {
const [childCount, setChildCount] = useState(0);
const onClickChildButton = useCallback(() => {
setChildCount((childCount) => childCount + 1);
onClick();
}, [onClick]);
return (
<div>
Child---{childCount}
<button onClick={onClickChildButton}>child 按钮</button>
</div>
);
});
缓存失效------props 使用普通对象
将对象直接写在 jsx 的属性中,这种和上面的 useCallback 错误使用 1 是一样的,每次父组件更新,都会创建一个新的 style 对象,导致子组件更新
js
function App() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => console.log("app click"), []);
const onClickAppButton = () => setCount(count + 1);
return (
<div>
app---{count}
<button onClick={onClickAppButton}>app 按钮</button>
<Child onClick={onClick} style={{ color: "red" }} />
</div>
);
}
const Child = memo(({ onClick, style }) => {
console.log("child render"); // 父组件更新,子组件也会更新
return <div style={style}>Child</div>;
});
正确的写法使用使用 useMemo 将 style 缓存起来
js
function App() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => console.log("app click"), []);
const onClickAppButton = () => setCount(count + 1);
const style = useMemo(() => ({ color: "red" }), []); // 这里使用了 useMemo
return (
<div>
app---{count}
<button onClick={onClickAppButton}>app 按钮</button>
<Child onClick={onClick} style={style} />
</div>
);
}
const Child = memo(({ onClick, style }) => {
console.log("child render"); // 父组件更新,子组件不会更新了
return <div style={style}>Child</div>;
});
当然也可以使用 useState 定义对象
js
function App() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => console.log("app click"), []);
const onClickAppButton = () => setCount(count + 1);
const [style] = useState({ color: "red" }); // 这里使用 useState
return (
<div>
app---{count}
<button onClick={onClickAppButton}>app 按钮</button>
<Child onClick={onClick} style={style} />
</div>
);
}
const Child = memo(({ onClick, style }) => {
console.log("child render"); // 父组件更新,子组件不会更新了
return <div style={style}>Child</div>;
});
缓存失效3------第三方状态管理工具
使用第三方状态管理工具,比如 zustand,在使用 memo 缓存组件时也会遇到失效的情况
如下代码:
styleParent是父组件使用的样式,styleChild是子组件使用的样式- 父组件中点击按钮,会更新
styleParent,同时子组件也会重新渲染,导致memo失效了
js
const useStyle = create((set) => ({
styleChild: { color: "red" },
styleParent: { color: "blue" },
setStyle: (color) => set({ styleParent: { color } }),
}));
function App() {
const [count, setCount] = useState(0);
const { setStyle, styleParent } = useStyle();
const onClick = useCallback(() => console.log("app click"), []);
const onClickAppButton = () => {
setCount(count + 1);
setStyle(styleParent.color === "blue" ? "green" : "blue");
};
return (
<div>
app---{count}
<button onClick={onClickAppButton} style={styleParent}>
app 按钮
</button>
<Child onClick={onClick} />
</div>
);
}
const Child = memo(() => {
console.log("child render"); // 父组件更新,子组件也会更新
const { styleChild } = useStyle();
return <div style={styleChild}>Child</div>;
});
如果要避免 memo 失效,有两种方式:
-
不要使用解构的语法,而是在
useStyle中将styleChild从state中提取出来jsconst useStyle = create((set) => ({ styleChild: { color: "red" }, styleParent: { color: "blue" }, setStyleParent: (color) => set({ styleParent: { color } }), })); function App() { const [count, setCount] = useState(0); const { setStyleParent, styleParent } = useStyle(); const onClick = useCallback(() => console.log("app click"), []); const onClickAppButton = () => { setCount(count + 1); setStyleParent(styleParent.color === "blue" ? "green" : "blue"); }; return ( <div> app---{count} <button onClick={onClickAppButton} style={styleParent}> app 按钮 </button> <Child onClick={onClick} /> </div> ); } const Child = memo(() => { console.log("child render"); // 父组件更新,子组件不会更新 // 在 useStyle 中将 styleChild 从 state 中提取出来 const styleChild = useStyle((state) => state.styleChild); return <div style={styleChild}>Child</div>; }); -
子组件中不使用
useStyle,而是将styleChild从父组件中传递过来jsconst useStyle = create((set) => ({ styleChild: { color: "red" }, styleParent: { color: "blue" }, setStyle: (color) => set({ styleParent: { color } }), })); function App() { const [count, setCount] = useState(0); const { setStyle, styleParent, styleChild } = useStyle(); const onClick = useCallback(() => console.log("app click"), []); const onClickAppButton = () => { setCount(count + 1); setStyle(styleParent.color === "blue" ? "green" : "blue"); }; return ( <div> app---{count} <button onClick={onClickAppButton} style={styleParent}> app 按钮 </button> // 通过 props 传递 styleChild <Child onClick={onClick} style={styleChild} /> </div> ); } const Child = memo(({ style }) => { console.log("child render"); // 父组件更新,子组件不会更新了 return <div style={style}>Child</div>; });
推荐使用第一种方式,毕竟使用第二种方式的话 zustand 就有点浪费了
缓存失效4------Context
使用 Context 后也会存在缓存失效的情况
context 提供 styleChild 和 styleParent 两个样式,styleChild 是子组件使用的样式,styleParent 是父组件使用的样式
子组件中如果直接使用 useContext 的话,就会导致 memo 失效
js
const StyleContext = createContext(null);
const StyleProvider = ({ children }) => {
const [styleChild, setStyleChild] = useState({ color: "red" });
const [styleParent, setStyleParent] = useState({ color: "blue" });
const style = useMemo(() => ({ styleChild, styleParent, setStyleParent }), [styleChild, styleParent, setStyleParent]);
return <StyleContext.Provider value={style}>{children}</StyleContext.Provider>;
};
const useStyle = () => useContext(StyleContext);
const App = () => (
<StyleProvider>
<Parent />
</StyleProvider>
);
const Parent = memo(() => {
const [count, setCount] = useState(0);
const { styleParent, setStyleParent, styleChild } = useStyle();
const onClick = useCallback(() => console.log("app click"), []);
const onClickAppButton = () => {
setCount(count + 1);
setStyleParent((style) => (style.color === "green" ? { color: "blue" } : { color: "green" }));
};
return (
<div>
app---{count}
<button onClick={onClickAppButton} style={styleParent}>
app 按钮
</button>
<Child onClick={onClick} style={styleChild} />
</div>
);
});
const Child = memo(({ style }) => {
console.log("child render"); // 父组件更新,子组件也会更新
return <div style={style}>Child</div>;
});
解决 memo 失效的方式还是要通过 props 传值
js
const StyleContext = createContext(null);
const StyleProvider = ({ children }) => {
const [styleChild, setStyleChild] = useState({ color: "red" });
const [styleParent, setStyleParent] = useState({ color: "blue" });
const style = useMemo(() => ({ styleChild, styleParent, setStyleParent }), [styleChild, styleParent, setStyleParent]);
return <StyleContext.Provider value={style}>{children}</StyleContext.Provider>;
};
const useStyle = () => useContext(StyleContext);
const App = () => (
<StyleProvider>
<Parent />
</StyleProvider>
);
function Parent() {
const [count, setCount] = useState(0);
const { styleParent, setStyleParent, styleChild } = useStyle();
const onClick = useCallback(() => console.log("app click"), []);
const onClickAppButton = () => {
setCount(count + 1);
setStyleParent((style) => (style.color === "green" ? { color: "blue" } : { color: "green" }));
};
return (
<div>
app---{count}
<button onClick={onClickAppButton} style={styleParent}>
app 按钮
</button>
<Child onClick={onClick} style={styleChild} />
</div>
);
}
const Child = memo(({ style }) => {
console.log("child render"); // 父组件更新,子组件不会更新
return <div style={style}>Child</div>;
});
总结
在开发过程中如果考虑性能优化的话,心智负担会太重,所以一般情况下不用考虑,只有在遇到性能问题时,再考虑优化
毕竟 react 自身已经做了很多优化了,比如 fiber 架构下的 setState 是异步的,react 会将多次 setState 合并成一次
所以在开发过程中,不需要考虑要不要使用 useCallback、useMemo、memo,遇到问题时在考虑也不迟