也许我误会了一些东西,但是useCallback Hook每次重新渲染时都会运行。
我传递了输入-作为useCallback的第二个参数-不可更改的常量-但返回的备忘录化回调在每次渲染时仍运行我的昂贵计算(我很确定-您可以在下面的代码段中自行检查)
我已经将useCallback更改为useMemo,并且useMemo可以按预期工作—在传递的输入发生更改时运行。并真正记住了昂贵的计算。
'use strict';
const { useState, useCallback, useMemo } = React;
const neverChange = 'I never change';
const oneSecond = 1000;
function App() {
const [second, setSecond] = useState(0);
// This expensive function executes everytime when render happens:
const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();
// This executes once
const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
setTimeout(() => setSecond(second + 1), oneSecond);
return `
useCallback: ${computedCallback} times |
useMemo: ${computedMemo} |
App lifetime: ${second}sec.
`;
}
const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };
function expensiveCalc(hook) {
let i = 0;
while (i < tenThousand) i++;
return ++expensiveCalcExecutedTimes[hook];
}
ReactDOM.render(
React.createElement(App),
document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
答案 0 :(得分:30)
TL; DR;
useMemo
用来记住函数调用之间以及渲染之间的计算结果useCallback
是要记住渲染之间的回调本身(引用相等)useRef
用于在渲染器之间保留数据(更新不会触发重新渲染)useState
用于在渲染器之间保留数据(更新将触发重新渲染)长版:
useMemo
专注于避免繁重的计算。
useCalback
专注于不同的事物:它解决了当onClick={() => { doSomething(...); }
之类的内联事件处理程序使PureComponent
子级重新呈现时的性能问题(因为函数表达式每次都存在参照差异)>
这表示useCallback
更接近useRef
来记忆计算结果。
PS看着docs,我确实同意它在那里令人困惑。
useCallback
将返回一个已记忆的回调版本,仅当输入之一发生更改时才会更改。 在将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染时很有用(例如,shouldComponentUpdate)。
示例
假设我们有一个基于PureComponent
的子级<Pure/>
,只有在其props
更改后才会重新渲染
每次重新渲染父级时,下一个代码都会重新渲染子级-因为内联函数每次都参照不同,所以
function Parent({ ... }) {
const [a, setA] = useState(0);
...
return (
...
<Pure onChange={() => { doSomething(a); }} />
);
}
我们可以借助useCallback
function Parent({ ... }) {
const [a, setA] = useState(0);
const onPureChange = useCallback(() => {doSomething(a);});
...
return (
...
<Pure onChange={onPureChange} />
);
}
但是一旦更改a
,我们将发现我们创建的onPureChange
和为我们记住的React仍然指向旧的a
值!我们有一个错误而不是性能问题!这是因为onPureChange
使用闭包功能来访问变量(而不是通过变量名访问)。为了使这项工作正确进行,我们需要让React知道将onPureChange
放到哪里,并重新创建/记住(记忆)指向正确数据的新版本。这里我们需要第二个参数:
const [a, setA] = useState(0);
const onPureChange = useCallback(() => {doSomething(a);}, [a]);
现在,如果a
被更改,React将重新渲染组件(这是useState
的举动)。并且在重新渲染期间,它发现onPureChange
的输入数据是不同的,并且需要重新创建/存储新版本的回调。终于,一切正常!
答案 1 :(得分:17)
useCallback
和useMemo
的单线:
useCallback(fn, deps)
与useMemo(() => fn, deps)
等价。
使用useCallback
记住函数,useMemo
记住所有计算值:
const fn = () => 42 // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]) // (1)
const memoFnReturn = useMemo(fn, [dep]) // (2)
(1)
将返回fn
的记忆版本-只要dep
是相同的,则在多个渲染器中使用相同的引用。但是每次您调用 memoFn
,复杂的计算又会重新开始。
(2)
将在每次fn
更改时调用dep
,并记住其返回值(此处为42
),然后将其存储在{ {1}}。
memoFnReturn
const App = () => {
const [dep, setDep] = useState(0);
const fn = () => 42 + dep; // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]); // (1)
const memoFnReturn = useMemo(fn, [dep]); // (2)
return (
<div>
<p> memoFn is {typeof memoFn} </p>
<p>
Every call starts new calculation, e.g. {memoFn()} {memoFn()}
</p>
<p>memoFnReturn is {memoFnReturn}</p>
<p>
Only one calculation for same dep, e.g. {memoFnReturn} {memoFnReturn}
</p>
<button onClick={() => setDep((p) => p + 1)}>Change dep</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
答案 2 :(得分:4)
每次您执行以下操作时,您都会调用记住的回调:
const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();
这就是useCallback
的数量增加的原因。但是该函数永远不会改变,它永远不会*****创建一个新的回调,它始终是相同的。表示useCallback
正确地完成了任务。
让我们对代码进行一些更改,以确保这是正确的。让我们创建一个全局变量lastComputedCallback
,该变量将跟踪是否返回了新的(不同的)函数。如果返回新函数,则意味着useCallback
只是“再次执行”。因此,当它再次执行时,我们将调用expensiveCalc('useCallback')
,因为这是您计算useCallback
是否起作用的方式。我在下面的代码中执行此操作,现在很明显useCallback
正在按预期进行记录。
如果您想每次看到useCallback
都重新创建该函数,则取消注释通过second
的数组中的行。您将看到它重新创建了函数。
'use strict';
const { useState, useCallback, useMemo } = React;
const neverChange = 'I never change';
const oneSecond = 1000;
let lastComputedCallback;
function App() {
const [second, setSecond] = useState(0);
// This is not expensive, and it will execute every render, this is fine, creating a function every render is about as cheap as setting a variable to true every render.
const computedCallback = useCallback(() => expensiveCalc('useCallback'), [
neverChange,
// second // uncomment this to make it return a new callback every second
]);
if (computedCallback !== lastComputedCallback) {
lastComputedCallback = computedCallback
// This executes everytime computedCallback is changed. Running this callback is expensive, that is true.
computedCallback();
}
// This executes once
const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
setTimeout(() => setSecond(second + 1), oneSecond);
return `
useCallback: ${expensiveCalcExecutedTimes.useCallback} times |
useMemo: ${computedMemo} |
App lifetime: ${second}sec.
`;
}
const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };
function expensiveCalc(hook) {
let i = 0;
while (i < 10000) i++;
return ++expensiveCalcExecutedTimes[hook];
}
ReactDOM.render(
React.createElement(App),
document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
useCallback
的好处是返回的函数是相同的,因此每次都不会对元素进行removeEventListener
和addEventListener
的反应,除非computedCallback
发生变化。并且computedCallback
仅在变量更改时更改。因此,反应只会addEventListener
一次。
很好的问题,我通过回答中学到了很多东西。
答案 3 :(得分:1)
useMemo
和useCallback
使用备忘录。
我喜欢将回忆记为记住的东西。
在渲染之间useMemo
和useCallback
记住之前,在依赖关系改变之前,区别只是它们记住。
useMemo
将记住函数返回的值。
useCallback
将记住您的实际功能。