内置 Hooks(2):为什么要避免重复定义回调函数?
useCallback:缓存回调函数
在 React 函数组件中,每一次 UI 的变化,都是通过重新执行整个函数来完成的
比如下面的代码中,我们在加号按钮上定义了一个事件处理函数,用来让计数器加 1。但是因为定义是在函数组件内部,因此在多次渲染之间,是无法重用 handleIncrement 这个函数的,而是每次都需要创建一个新的(所以才可以拿到最新的 state):
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
// ...
return <button onClick={handleIncrement}>+</button>;
}
每次组件状态发生变化的时候,函数组件实际上都会重新执行一遍。在每次执行的时候,实际上都会创建一个新的事件处理函数 handleIncrement。这个事件处理函数中呢,包含了 count 这个变量的闭包,以确保每次能够得到正确的结果。
即使 count 没有发生变化,但是函数组件因为其它状态发生变化而重新渲染时,这种写法也会每次创建一个新的函数。创建一个新的事件处理函数,虽然不影响结果的正确性,但其实是没必要的。因为这样做不仅增加了系统的开销,更重要的是:每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。
import React, { useState, useCallback } from "react";
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(
() => setCount(count + 1),
[count] // 只有当 count 发生变化时,才会重新创建回调函数
);
// ...
return <button onClick={handleIncrement}>+</button>;
}
而接收这个回调函数作为属性的组件(button),也不会频繁地需要重新渲染。
useCallback 缓存的是一个函数,而 useMemo 缓存的是计算的结果。
useMemo:缓存计算的结果
和 useCallback 一样都接收一个函数作为第一个参数,但是 useCallback 缓存的是函数本身,但是 useMemo 缓存的是函数的返回值
这个场景应该很容易理解:如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。
这也是 userMemo 的一大好处:避免重复计算。
除了避免重复计算之外,useMemo 还有一个很重要的好处:避免子组件的重复渲染。
const myEventHandler = useMemo(() => {
// 返回一个函数作为缓存结果
return () => {
// 在这里进行事件处理
};
}, [dep1, dep2]);
useRef:在多次渲染之间共享数据
在类组件中,我们可以定义类的成员变量,以便能在对象上通过成员属性去保存一些数据。但是在函数组件中,是没有这样一个空间去保存数据的。因此,React 让 useRef 这样一个 Hook 来提供这样的功能。---- 类似于类组件中的 this
我们可以把 useRef 看作是在函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的 current 属设置一个值,从而在函数组件的多次渲染之间共享这个值。
使用 useRef 保存的数据一般是和 UI 的渲染无关的因此当 ref 的值发生变化时,是不会触发组件的重新渲染的
除了存储跨渲染的数据之外,useRef 还有一个重要的功能,就是保存某个 DOM 节点的引用。我们知道,在 React 中,几乎不需要关心真实的 DOM 节点是如何渲染和修改的。但是在某些场景中,我们必须要获得真实 DOM 节点的引用,所以结合 React 的 ref 属性和 useRef 这个 Hook,我们就可以获得真实的 DOM 节点,并对这个节点进行操作。
DOM 组件被渲染到页面上的时候我们就可以通过 useRef 拿到这个 DOM 节点的引用了
useContext:定义全局状态
让所有在某个组件开始的组件树上创建一个 Context。这样这个组件树上的所有组件,就都能访问和修改这个 Context 了。那么在函数组件里,我们就可以使用 useContext 这样一个 Hook 来管理 Context。
Context 看上去就是一个全局的数据,为什么要设计这样一个复杂的机制,而不是直接用一个全局的变量去保存数据呢?
答案其实很简单,就是为了能够进行数据的绑定。当这个 Context 的数据发生变化时,使用这个数据的组件就能够自动刷新。但如果没有 Context,而是使用一个简单的全局变量,就很难去实现了。
要让它变得动态,其实只要用一个 state 来保存,通过修改 state,就能实现动态的切换 Context 的值了。
注意:
Context 相当于提供了一个定义 React 世界中全局变量的机制,而全局变量则意味着两点:
会让调试变得困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。