有一段时间,我想开始使用带有反应挂钩的反应功能组件,而不是扩展反应组件的类,但是有一件事让我感到沮丧。这是来自第一个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的角度来看,这有多糟?
答案 0 :(得分:3)
更改句柄是否在每次渲染时都初始化?
是的。这是使用useCallback
钩子的原因之一。
从JavaScript的角度来看,这有多糟糕?
从根本上讲,它只是在创建一个新对象。函数对象和基础函数代码不是同一件事。该函数的基础代码仅解析一次,通常解析为字节码或简单,快速的编译版本。如果足够频繁地使用该函数,则会对其进行积极地编译。
因此,每次创建一个新的函数对象都会造成一些内存混乱,但是在现代JavaScript编程中,我们一直创建和释放对象,因此JavaScript引擎在进行处理时进行了优化。 / p>
但是使用useCallback
可以避免不必要地重新创建它((有点,请继续阅读) ),只需在依赖项发生变化时更新我们使用的那个。您需要列出的依赖项(在数组useCallback
的第二个参数中)是handleNameChange
结束时可以更改的事物。在这种情况下,handleNameChange
不会覆盖任何更改的内容。它唯一关闭的是setName
,React保证不会改变(请参见useState
上的“注意”)。它确实使用来自输入的值,但是它通过参数接收输入,但不会覆盖该输入。因此,对于handleNameChange
,您可以通过将空数组作为第二个参数传递给useCallback
来使依赖项保持空白。 (在某个阶段,可能有些东西会自动检测到这些依赖关系;现在,您可以声明它们。)
敏锐的眼睛会注意到,即使使用useCallback
,每次您仍然 创建一个新函数(将您作为{{1}的第一个参数传入的函数) }。但是,如果先前版本的依存关系与新版本的依存关系匹配,useCallback
将返回它的先前版本(在useCallback
情况下,由于没有任何依存关系,它们总是会返回)。这意味着您作为第一个参数传入的函数可立即用于垃圾回收。 JavaScript引擎在垃圾回收对象(包括对函数调用(在对handleNameChange
的调用)中创建的对象,包括函数)中特别有效,但在该调用返回时未在任何地方引用它,即Greeting
有意义的部分原因。 (与流行的看法相反,现代引擎可以并且可能在堆栈上创建对象。)而且,在useCallback
的props中重用相同的功能可以使React更有效地渲染树(通过最小化差异) )。
该代码的input
版本是:
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
。现在,更改名称。请注意,没有任何内容被重新创建(好吧,好的,新的不被使用并且可以立即进行GC处理)。现在,单击对勾旁边的incrementTicks
按钮。请注意,[+]
是重新创建的(因为关闭的incrementTicks
是过时的,因此ticks
返回了我们创建的新函数),但是useCallback
仍然相同。>
答案 1 :(得分:2)
严格从JavaScript的角度来看(忽略React),在循环(或定期调用的另一个函数)内定义一个函数不太可能成为性能瓶颈。
看看这些jsperf cases。当我运行此测试时,函数声明用例的运行速度为797,792,833 ops / second。这也不一定是最佳实践,但它常常成为假定程序员定义函数必须很慢的程序员过早优化的受害者。
现在,从React的角度来看。当您将该函数传递给子组件时,由于每次从技术上来说都是新函数,因此最终重新渲染该子组件时,性能可能成为挑战。在这种情况下,明智的做法是useCallback
在多个渲染器之间保留功能的标识。
值得一提的是,即使使用useCallback
钩子,每个渲染器仍会重新声明函数表达式,只是除非依赖项数组发生更改,否则它将忽略其值。