useCallback,useMemo,useRef,useContext
useCallback:缓存回调函数
函数组件中并没有一个直接的方式在多次渲染之间维持一个状态,所以一些定义在函数组件内的函数在多次渲染之间无法重用,每次都需要创建一个新的。这样也会导致每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。
思路:只有当函数参数或调用变量发生变化时,才需要重新定义一个回调函数。
于是就有了 useCallback 这个 Hook。
useCallback(fn, deps); // fn: 回调函数, deps: 依赖项
const handleIncrement = useCallback(() => setCount(count + 1), [count]);
可以减少不必要的渲染,主要体现在将回调函数作为属性传给某个组件,因为如果函数被重新创建,那么组件的 props 就会发生变化也会导致子组件的重新渲染。
useMemo 也是为了缓存而设计的,useCallback 缓存的是一个函数,而 useMemo 缓存的是计算结果。
useMemo:缓存计算结果
useMemo(fn, deps); // fn: 计算函数, deps: 依赖项
如果某个数据是通过其他数据计算得到,那么只有当用到的变量(依赖项)发生变化时才会重新计算。
import React, { useState, useMemo, useEffect } from "react";
export default function SearchUserList() {
const [users, setUsers] = useState(null);
const [searchKey, setSearchKey] = useState("");
useEffect(() => {
const doFetch = async () => {
// 组件首次加载时发请求获取用户数据
const res = await fetch("https://reqres.in/api/users/");
setUsers(await res.json());
};
doFetch();
}, []);
const usersToShow = useMemo(() => {
if (!users) return null;
return users.data.filter((user) => user.first_name.includes(searchKey));
}, [users, searchKey]);
return (
<div>
<input
type="text"
value={searchKey}
onChange={(evt) => setSearchKey(evt.target.value)}
/>
<ul>
{usersToShow &&
usersToShow.length > 0 &&
usersToShow.map((user) => {
return <li key={user.id}>{user.first_name}</li>;
})}
</ul>
</div>
);
}
可以避免一些重复计算,还可以避免子组件的重复渲染!
♦️ 其实 useCallback 的功能其实可以用 useMemo 来实现。
useRef:在多次渲染之间共享数据
const refCon = useRef(initialValue);
可以把 useRef 看作是在函数组件之外创建的一个容器空间,在这个容器上,这样就可以通过唯一的 current 属设置的一个值,从而在函数组件的多次渲染之间共享这个值。
import React, { useState, useCallback, useRef } from "react";
export default function Timer() {
// 定义 time state 用于保存计时的累积时间
const [time, setTime] = useState(0);
// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
const timer = useRef(null);
// 开始计时的事件处理函数
const handleStart = useCallback(() => {
// 使用 current 属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1);
}, 100);
}, []);
// 暂停计时的事件处理函数
const handlePause = useCallback(() => {
// 使用 clearInterval 来停止计时
window.clearInterval(timer.current);
timer.current = null;
}, []);
return (
<div>
{time / 10} seconds.
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
使用 useRef 保存的数据一般是和 UI 渲染无关的,因此当 ref 的值发生变化时是不会触发组件的重新渲染的。
保存某个 DOM 节点的引用
就可以利用这个保存功能,就可以在渲染界面上访问到真实的 DOM 节点。
useContext:定义全局状态
能够让所有组件在某个组件开始的组件树上创建一个 Context,这样这个组件树上的所有组件都能访问和修改这个 Context。
const context = React.createContext(initialValue);
const value = useContext(context);
context 具有一个 Provider 的属性,一般作为组件树的根组件。
import React, { useState, useContext, useCallback } from "react";
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee",
},
dark: {
foreground: "#ffffff",
background: "#222222",
},
};
// 创建一个 Theme 的 Context
const ThemeContext = React.createContext(themes.light);
// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button
style={{
background: theme.background,
color: theme.foreground,
}}
>
I am styled by theme context!
</button>
);
}
// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
export default function () {
// 使用 state 来保存 theme 从而可以动态修改
const [theme, setTheme] = useState("light");
// 切换 theme 的回调函数
const toggleTheme = useCallback(() => {
setTheme((theme) => (theme === "light" ? "dark" : "light"));
}, []);
return (
// 使用 theme state 作为当前 Context
<ThemeContext.Provider value={themes[theme]}>
<button onClick={toggleTheme}>Toggle Theme</button>
<Toolbar />
</ThemeContext.Provider>
);
}
♦️ 缺点
- 让调试变得困难
- 让组件复用变得困难
思考:
useState 也能够在组件的多次渲染之间共享数据,那个那个 timer 计时器是否能用 state 去保存 window.setInterval()?
- 可以但没必要,用 useState 保存的话会触发组件的重新渲染?
- useRef 结果的改变不会触发重新渲染