在React中,参照平等是一个重要的概念,它影响到你的应用程序中的组件重新渲染的频率。在这篇文章中,我们将探讨来自React的useEvent Hook,它允许你定义一个带有函数身份的事件处理程序,这个函数身份始终是稳定的,有助于在你的应用程序中管理参考平等。
值得注意的是,在撰写本文时,useEvent Hook还不能使用,目前正在React社区讨论。
- JavaScript中的参照平等
- 为什么引用平等在React中很重要?
useEvent钩子- 实现RFC中的
useEventHook - 什么时候不应该使用
useEventHook? - 卸载
useEffect对比。useLayoutEffect
JavaScript中的引用平等
在JavaScript中,你可以使用身份运算符来比较两个值是否相等,这也被称为严格平等。例如,在下面的代码中,你正在比较值a 和b 。
a === b; // strict equality or indentity operator
其结果是一个布尔值,告诉你值a 是否等于b 。
对于原始数据类型,比较是用实际值进行的。例如,如果a =10 ,而b =10 ,身份运算符将返回true 。
像对象和函数一样,复合值是一种参考类型。即使它们有相同的代码,两个函数也是不相等的。看一下下面的例子,身份运算符将返回false 。
let add = (a, b) => a + b;
let anotherAdd = (a, b) => a + b;
console.log(add === anotherAdd); // false
然而,如果你比较同一个函数的两个实例,它将返回true 。
let thirdAdd = add;
console.log(add === thridAdd); // true
换句话说,thirdAdd 和add 在引用上是相等的。
为什么参考性平等在React中很重要?
理解React中的引用平等很重要,因为我们经常用相同的代码创建不同的函数。例如,考虑下面的代码。
function AComponent() {
// handleEvent is created and destroyed on each re-render
const handleEvent = () => {
console.log('Event handler');
}
// ...
}
React会销毁当前版本的handleEvent 函数,并在每次AComponent re-renders时创建一个新版本。然而,在某些情况下,这种方法不是很有效,例如:
- 你使用一个像
useEffect,在其依赖数组中接受事件处理程序的Hook - 你有一个接受事件处理程序的记忆化组件
在这两种情况下,你都想保持一个事件处理程序的单一实例。但是,每次重新渲染时,你都会得到一个新的函数实例,这将进一步影响性能,要么重新渲染一个备忘组件,要么启动useEffect 回调。
你可以通过使用useCallback Hook轻松解决这个问题,如下图所示:
function AComponent() {
const handleEvent = useCallback(() => {
console.log('Event handled');
}, []);
// ...
}
useCallback Hook对函数进行了备忘,也就是说,每当一个函数被调用时有一个唯一的输入,useCallback Hook就会保存该函数的一个副本。因此,如果在重新渲染的过程中输入没有变化,你会得到相同的函数实例。
但是,当你的事件处理程序依赖于一个状态或道具时,useCallback Hook会在每次变化时创建一个新的处理函数。例如,看一下下面的代码:
function AComponent() {
const [someState, setSomeState] = useState(0);
const handleEvent = useCallback(() => {
console.log('log some state: `, someState);
}, [someState]);
// ...
}
现在,每次组件被重新渲染时,该函数将不会被创建。但是,如果someState 发生变化,它将创建一个新的handleEvent 的实例,即使该函数的定义没有变化。
useEvent 钩子
useEvent 钩子试图解决这个问题;你可以使用useEvent 钩子来定义一个事件处理程序,它的函数身份始终是稳定的。换句话说,在每次重新渲染时,事件处理程序的引用都是相同的。基本上,事件处理程序将有以下属性。
- 该函数不会在每次道具或状态改变时被重新创建
- 该函数将能够访问道具和状态的最新值。
你会按以下方式使用useEvent Hook:
function AComponent() {
const [someState, setSomeState] = useState(0);
const handleEvent = useEvent(() => {
console.log('log some state: `, someState);
});
// ...
}
由于useEvent Hook确保了一个函数只有一个实例,所以你不需要提供任何依赖关系。
实现RFC中的useEvent Hook
下面的例子是对RFC中useEvent Hook的近似实现。
// (!) Approximate behavior
function useEvent(handler) {
const handlerRef = useRef(null);
// In a real implementation, this would run before layout effects
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
// In a real implementation, this would throw if called during render
const fn = handlerRef.current;
return fn(...args);
}, []);
}
useEvent 钩子在使用它的组件的每次渲染时被调用。在每次渲染时,handler 函数被传递给useEvent Hook。handler 函数总是具有最新的props 和state 的值,因为当一个组件被渲染时,它基本上是一个新函数。
在useEvent Hook里面,useLayoutEffect Hook也在每次渲染时被调用,并将handlerRef 改为handler 函数的最新值。
在真实版本中, 在所有的 useLayoutEffect函数被调用 之前, handlerRef 将被切换到最新的处理函数 。
最后一块是useCallback 的返回。useEvent 钩子返回一个被useCallback 钩子包裹的函数,其依赖数组为空[] 。这就是为什么这个函数总是具有稳定的参考身份。
你可能想知道这个函数怎么总是有props 和state 的新值。如果你仔细看看,用于useCallback Hook的匿名函数使用了handlerRef 的当前值。这个current 的值代表了handler 的最新版本,因为它是在调用useLayoutEffect 的时候切换的。
什么时候不应该使用useEvent 钩子?
在某些情况下,你不应该使用useEvent Hook。让我们了解一下什么时候和为什么。
首先,你不能在渲染过程中使用用useEvent Hook创建的函数。例如,下面的代码会失败。
function AComponent() {
const getListOfData = useEvent(() => {
// do some magic and return some data
return [1, 2, 3];
});
return <ul>
{getListOfData().map(item => <li>{item}</li>}
</ul>;
}
解除挂载useEffect 与 useLayoutEffect
解除挂载的useEffect Hook和useLayoutEffect Hook会有不同版本的useEvent handler。看一下下面的例子。
function Counter() {
const [counter, setCounter] = React.useState(0);
const getValue = useEvent(() => {
return counter;
});
React.useLayoutEffect(() => {
return () => {
const value = getValue();
console.log('unmounting layout effect:', value);
};
});
React.useEffect(() => {
return () => {
const value = getValue();
console.log('unmounting effect:', value);
};
});
return (
<React.Fragment>
Counter Value: {counter}
<button onClick={() => setCounter(counter + 1)}>+</button>
</React.Fragment>
);
}
如果你运行这个程序,你会看到unmountinguseLayoutEffect 有旧版本的getValue 事件处理程序。请随时查看Stackblitz的例子。
结论
尽管useEffect Hook还不能使用,但对于React开发者来说,它绝对是一个有前途的发展。在这篇文章中,我们探讨了useEffect Hook背后的逻辑,回顾了你应该和不应该使用它的场景。
绝对值得关注useEffect Hook,我期待着最终能够将它整合到我的应用程序中。我希望你喜欢这篇文章。编码愉快