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
,遇到问题时在考虑也不迟