[英]How React.memo works with useCallback
据我了解,React.memo 是一个用于记忆组件的 API:如果它的 props 没有改变,则 react 使用该组件的最新渲染,而不将其与之前的版本进行比较。 跳过新渲染并与旧渲染进行比较可以加快应用程序的速度。 凉爽的。
现在,这是我没有得到的:如果道具没有改变,那么一个未记忆的组件也不会被重新渲染,正如我从那个简单的代码中看到的那样(使用这个链接查看演示,代码片段on this page有点令人困惑) :普通组件+usecallback和memoized one+useCallback之间的渲染次数没有区别。 基本上,我只需要 useCallbacks,因为普通组件不会使用相同的道具重新渲染。 那么,我错过了什么? 当备忘录帮助优化?
const { useCallback, useEffect, useState, memo, useRef } = React; function Child({ fn, txt }) { const [state, setState] = useState(0); console.log(txt + " rendered!"); useEffect(() => { setState((state) => state + 1); }, [fn]); return ( <div style={{ border: "solid" }}> I'm a Child {!fn && <div>And I got no prop</div>} {fn && <div>And I got a fn as a prop</div>} <div> and I've got rendered <strong>{state}</strong> times </div> </div> ); } const MemoChild = memo(Child); function App() { const [, setState] = useState(true); const handlerOfWhoKnows = () => {}; return ( <div className="App"> I'm the parent <br /> <button onClick={() => setState((state) => !state)}> Change parent state </button> <h3>Ref</h3> ref: <Child txt="ref" fn={handlerOfWhoKnows} /> <h3>test</h3> useCB: <Child txt="useCb" fn={useCallback(handlerOfWhoKnows, [])} /> memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} /> memo + useCB: <MemoChild txt="memo+useCb" fn={useCallback(handlerOfWhoKnows, [])} /> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
该示例中发生了几件事,但让我们从标题开始:您的孩子标记为"memo+useCb"
是完全正确的,它显示了避免使用组件的正确方法(或至少是正确的方法)不必要地重新渲染。 它正在做两件必要的事情:
记忆组件(通过memo
)
和
确保组件的 props 不会发生不必要的变化(通过useCallback
)
你的其他人,那些标记为"useCb"
和"memo"
的人,每个人都缺少一半的必要因素:
"useCb"
没有被记忆,所以每次父渲染时它都会重新渲染
"memo"
每次都获得不同的fn
道具,因此每次父级渲染时都会重新渲染
这是记忆和确保道具不改变(通过useCallback
或useMemo
或useRef
等)的组合,避免了不必要的重新渲染。 他们中的任何一个都没有,这就是您的示例所显示的。
该示例可能会稍微误导您的一件事是,您有组件说“我已经渲染了{state}
次”,但state
不计算渲染次数,它计算fn
道具的次数改变了价值,这不是一回事。 “渲染”是对函数组件的函数(或class
组件的render
方法)的调用。 在您的示例中,渲染次数由“useCb 渲染!”显示。 和“备忘录呈现!” 消息,每次父级呈现时都会看到,因为我们单击了按钮。
你说过(你的重点) :
现在,这就是我没有得到的:如果道具没有改变,那么一个未记忆的组件也不会被重新渲染......
您的示例向您展示了未记忆的孩子确实会重新渲染,即使它的道具没有改变: "useCb"
版本具有稳定的道具,但每次父母渲染时它仍然会重新渲染。 您可以看到,因为它再次输出“useCb 渲染!” 每次单击导致父级重新渲染的按钮时。
这是您示例的更新版本,希望在渲染发生时与道具更改发生时更清楚地显示,使用日志记录进行渲染并使用组件的渲染输出进行道具更改:
const { useCallback, useEffect, useState, memo, useRef } = React; function Child({ fn, txt }) { const [fnChanges, setFnChanges] = useState(0); const rendersRef = useRef(0); ++rendersRef.current; console.log(`${txt} rendered! (Render #${rendersRef.current})`); useEffect(() => { console.log(`${txt} saw new prop, will render again`); setFnChanges((changes) => changes + 1); }, [fn]); return ( <div style={{ border: "solid" }}> {txt}: <code>fn</code> changes: {fnChanges} </div> ); } const MemoChild = memo(Child); /*export default*/ function App() { const [, setState] = useState(true); const handlerOfWhoKnows = () => {}; const memoizedHandler = useCallback(handlerOfWhoKnows, []); return ( <div className="App"> <button onClick={() => setState((state) => !state)}>Change parent state</button> <div> Not memoizing anything: <Child txt="ref" fn={handlerOfWhoKnows} /> </div> <div> Just memoizing the <code>fn</code> prop, not the component: <Child txt="useCb" fn={memoizedHandler} /> </div> <div> Just memoizing the component, not the <code>fn</code> prop: <MemoChild txt="memo" fn={handlerOfWhoKnows} /> </div> <div> Memoizing <strong>both</strong> the component and the <code>fn</code> prop: <MemoChild txt="memo+useCb" fn={memoizedHandler} /> </div> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
但是,很难清楚地看到日志记录,所以这里有第二个版本,它在组件的渲染输出中包含了渲染的数量(在渲染结果中包含非状态是非常不寻常的,但仅用于说明目的在这种情况下,看看发生了什么):
const { useCallback, useEffect, useState, memo, useRef } = React; function Child({ fn, txt }) { const [fnChanges, setFnChanges] = useState(0); const rendersRef = useRef(0); ++rendersRef.current; useEffect(() => { setFnChanges((changes) => changes + 1); }, [fn]); return ( <div style={{ border: "solid" }}> {txt}: Renders: {rendersRef.current}, <code>fn</code> changes: {fnChanges} </div> ); } const MemoChild = memo(Child); /*export default*/ function App() { const [, setState] = useState(true); const handlerOfWhoKnows = () => {}; const memoizedHandler = useCallback(handlerOfWhoKnows, []); return ( <div className="App"> <button onClick={() => setState((state) => !state)}>Change parent state</button> <div> Not memoizing anything: <Child txt="ref" fn={handlerOfWhoKnows} /> </div> <div> Just memoizing the <code>fn</code> prop, not the component: <Child txt="useCb" fn={memoizedHandler} /> </div> <div> Just memoizing the component, not the <code>fn</code> prop: <MemoChild txt="memo" fn={handlerOfWhoKnows} /> </div> <div> Memoizing <strong>both</strong> the component and the <code>fn</code> prop: <MemoChild txt="memo+useCb" fn={memoizedHandler} /> </div> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
同样,您的示例确实显示了使用"memo+useCb"
子项的正确方法。
除了@TJ Crowder 的回答,
您的示例项目在<StrictMode>
中运行,因此 React 将在开发模式下运行一个组件两次以验证事物。 要理解这个问题,首先让我们暂时从index.js
中删除<ScrictMode
基本上,我只需要 useCallbacks,因为普通组件不会使用相同的道具重新渲染。 那么,我错过了什么? 当备忘录帮助优化?
通过使用memo
,您告诉协调算法如果道具相同,则不要继续渲染子组件。 但是在没有memo
的普通组件中,协调算法将遍历树并调用您的子组件(因为这是算法应该做的)。 遍历(渲染)完成后,react 会将更改刷新到 DOM。 因为没有任何更改将提交给 DOM。 但是使用memo
可以加快遍历过程。
笔记:
在 React 中,“渲染”并不意味着更新 DOM 元素。 渲染意味着调用你的函数组件或 Component.render() 方法。 这不是一个昂贵的过程,因此 React 会这样做,但 React 不会盲目地将更改刷新到 DOM。 您可以通过打开检查元素来验证这一点。 当您展开检查元素时,单击按钮,您不会看到任何更改(在 dom 元素上创建/删除带有紫色突出显示的动画)。 通过使用备忘录,您可以手动停止 render() 并移动到兄弟元素(如果可用)以继续重新渲染而不是进入子元素。 这也是为什么您不能使用console.log
来实际测试 react 应用程序的性能的原因。 console.log
是一个副作用。 React 严格告诉你不要在渲染阶段(函数体的顶层)添加副作用。 要处理副作用,您需要使用useEffect
。
现在让我们看看您的代码中的问题,根据您的沙盒代码,在删除<StrictMode>
并重新加载预览后,我们将在控制台上获得以下日志,
对于这个组件,
<div className="App">
ref: <Child txt="ref" fn={handlerOfWhoKnows} />
useCB: <Child txt="useCb" fn={useCallback(handlerOfWhoKnows, [])} />
memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} />
memo + useCB: <MemoChild txt="memo+useCb" fn={useCallback(handlerOfWhoKnows, [])} />
</div>
它将在安装时记录以下内容,
ref rendered!
useCb rendered!
memo rendered!
memo+useCb rendered!
ref rendered!
useCb rendered!
memo rendered!
memo+useCb rendered!
您看到每个日志打印两次的原因是,您在<Child/>
组件中使用了useEffect
。
useEffect(() => {
setState((state) => state + 1);
}, [fn]);
由于触发了状态更改,React 也将第二次重新运行<Child />
。 跟道具没关系。 如果我们删除这个useEffect
,现在你可以看到挂载日志,
ref rendered!
useCb rendered!
memo rendered!
memo+useCb rendered!
删除子项中的StrictMode
和useEffect
后,单击按钮。 这次将打印日志
ref rendered! // rendered due to the prop changed
useCb rendered! // rendered due to the prop changed
memo rendered! // rendered due to the prop changed
这次可以看到,没有打印memo+useCb
。 下一个问题是为什么,
memo rendered!
在第二步。 这是因为您没有记住fn
,所以每次渲染时它都会重新创建。 所以 props 改变 -> 组件重新渲染
所以代码应该是,
const handlerOfWhoKnows = useCallback(() => {}, []);
<MemoChild txt="memo" fn={handlerOfWhoKnows} />
现在随着更改,组件看起来像,
const handlerOfWhoKnows = useCallback(() => {}, []);
return (
<div className="App">
ref: <Child txt="ref" fn={handlerOfWhoKnows} />
useCB: <Child txt="useCb" fn={handlerOfWhoKnows} />
memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} />
memo + useCB: <MemoChild txt="memo+useCb" fn={handlerOfWhoKnows} />
</div>
);
现在当我们点击按钮时,我们只会看到,
ref rendered!
useCb rendered!
所以你的问题的答案,
当备忘录帮助优化?
如果道具相同,备忘录有助于手动停止进一步的渲染。 一旦发现memo
和道具相同,对账算法将停止遍历。 它将完成渲染树或继续渲染兄弟元素。
过多地使用备忘录也会导致性能问题,因为memo
本身会执行一些过程来确定是否渲染子节点。 谨慎使用useCallback
和useMemo
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.