有一段时间,我想开始使用带有反应钩子的 react 函数组件,而不是类扩展 react 组件,但有一件事让我气馁。下面是 react 钩子的第一个介绍示例:
import React, { useState } from 'react'
import Row from './Row'
export default function Greeting(props) {
const [name, setName] = useState('Mary');
function handleNameChange(e) {
setName(e.target.value);
}
return (
<section>
<Row label="Name">
<input
value={name}
onChange={handleNameChange}
/>
</Row>
</section>
)
}
有一个handleNameChange
声明用作输入的更改处理程序。让我们想象一下,由于某种原因,Greeting
组件更新非常频繁。更改句柄是否每次在每次渲染时初始化?从JavaScript的角度来看,这有多糟糕?
更改句柄是否每次在每次渲染时初始化?
是的。这是useCallback
钩的原因之一。
从JavaScript的角度来看,这有多糟糕?
从根本上说,它只是创建一个新对象。函数对象和底层函数代码不是一回事。函数的底层代码只解析一次,通常解析为字节码或简单、快速的编译版本。如果该函数使用得足够频繁,它将得到积极的编译。
因此,每次创建一个新的函数对象都会产生一些内存改动,但在现代 JavaScript 编程中,我们一直在创建和发布对象,因此 JavaScript 引擎经过高度优化,可以在我们这样做时处理它。
但是使用useCallback
可以避免不必要地重新创建它(嗯,有点,继续阅读),只需在其依赖项更改时更新我们使用的那个。您需要列出的依赖项(在数组中是要useCallback
的第二个参数)是handleNameChange
关闭的内容,可以更改。在这种情况下,handleNameChange
不会关闭任何更改的内容。它唯一关闭的是setName
,React 保证不会改变(参见useState
上的"注释")。它确实使用来自输入的值,但它通过参数接收输入,它不会关闭它。因此,对于handleNameChange
,您可以通过传递一个空数组作为第二个参数来将依赖项留空useCallback
.(在某个阶段,可能会有一些东西自动检测这些依赖项;现在,您声明它们。
敏锐的人会注意到,即使有useCallback
,你仍然每次都在创建一个新函数(你作为useCallback
的第一个参数传入的函数)。但是,如果以前版本的依赖项与新版本的依赖项匹配,useCallback
将返回它的先前版本(在handleNameChange
情况下,它们总是会这样做,因为没有任何依赖项)。这意味着您作为第一个参数传入的函数立即可用于垃圾回收。JavaScript 引擎在垃圾收集对象(包括函数)方面特别有效,这些对象是在函数调用(调用Greeting
)期间创建的,但在调用返回时不会在任何地方引用,这也是useCallback
有意义的部分原因。(与普遍的看法相反,对象可以并且尽可能由现代引擎在堆栈上创建。此外,在input
上的 props 中重用相同的函数可以让 React 更有效地渲染树(通过最小化差异)。
该代码的useCallback
版本为:
import React, { useState, useCallback } from 'react' // ***
import Row from './Row'
export default function Greeting(props) {
const [name, setName] = useState('Mary');
const handleNameChange = useCallback(e => { // ***
setName(e.target.value) // ***
}, []) // *** empty dependencies array
return (
<section>
<Row label="Name">
<input
value={name}
onChange={handleNameChange}
/>
</Row>
</section>
)
}
这是一个类似的例子,但它还包括第二个回调(incrementTicks
),它确实使用了它关闭的东西(ticks
)。请注意handleNameChange
和incrementTicks
何时实际更改(由代码标记):
const { useState, useCallback } = React;
let lastNameChange = null;
let lastIncrementTicks = null;
function Greeting(props) {
const [name, setName] = useState(props.name || "");
const [ticks, setTicks] = useState(props.ticks || 0);
const handleNameChange = useCallback(e => {
setName(e.target.value)
}, []); // <=== No dependencies
if (lastNameChange !== handleNameChange) {
console.log(`handleNameChange ${lastNameChange === null ? "" : "re"}created`);
lastNameChange = handleNameChange;
}
const incrementTicks = useCallback(e => {
setTicks(ticks + 1);
}, [ticks]); // <=== Note the dependency on `ticks`
if (lastIncrementTicks !== incrementTicks) {
console.log(`incrementTicks ${lastIncrementTicks === null ? "" : "re"}created`);
lastIncrementTicks = incrementTicks;
}
return (
<div>
<div>
<label>
Name: <input value={name} onChange={handleNameChange} />
</label>
</div>
<div>
<label>
Ticks: {ticks} <button onClick={incrementTicks}>+</button>
</label>
</div>
</div>
)
}
ReactDOM.render(
<Greeting name="Mary Somerville" ticks={1} />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
当您运行它时,您会看到handleNameChange
和incrementTicks
都已创建。现在,更改名称。请注意,没有重新创建任何内容(好吧,好吧,新的不会使用,并且可以立即进行GC'able)。现在单击刻度旁边的[+]
按钮。请注意,incrementTicks
是重新创建的(因为它关闭ticks
已过时,因此useCallback
返回我们创建的新函数),但handleNameChange
仍然相同。
严格从 JavaScript 的角度来看(忽略 React),在循环中(或在另一个定期调用的函数内)定义一个函数不太可能成为性能瓶颈。
看看这些jsperf案例。当我运行此测试时,函数声明案例以 797,792,833 次/秒的速度运行。这也不一定是最佳实践,但它通常是一种模式,成为程序员过早优化的受害者,他们认为定义函数必须很慢。
现在,从 React 的角度来看。当您将该函数传递给最终重新渲染的子组件时,这可能会成为性能挑战,因为从技术上讲,它每次都是一个新函数。在这种情况下,明智的做法是使用useCallback
在多个渲染中保留函数的标识。
还值得一提的是,即使使用useCallback
钩子,函数表达式仍然会在每次渲染时重新声明,只是除非依赖数组发生变化,否则它的值会被忽略。