问题
事件处理函数会被重复定义
数据计算过程没有缓存
useCallback - 缓存回调函数
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
// ...
return <button onClick={handleIncrement}>+</button>
}
每次组件状态发生变化的时候,函数组件实际上都会重新执行一遍。在每次执行的时候,实际上都会创建一个新的事件处理函数 handleIncrement。这个事件处理函数中呢,包含了 count 这个变量的闭包,以确保每次能够得到正确的结果。
即使 count 没有发生变化,但是函数组件因为其它状态发生变化而重新渲染时,这种写法也会每次创建一个新的函数。创建一个新的事件处理函数,虽然不影响结果的正确性,但其实是没必要的。因为这样做不仅增加了系统的开销,更重要的是:每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。
比如这个例子中的 button 组件,接收了 handleIncrement ,并作为一个属性。如果每次都是一个新的,那么这个 React 就会认为这个组件的 props 发生了变化,从而必须重新渲染。因此,我们需要做到的是:只有当 count 发生变化时,我们才需要重新定一个回调函数。而这正是 useCallback 这个 Hook 的作用。
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>
}
useMemo - 缓存计算结果
如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。
- 避免重复计算。
- 避免子组件的重复渲染。
useCallback 的功能可以用 useMemo 来实现
建立了一个绑定某个结果到依赖数据的关系。只有当依赖变了,这个结果才需要被重新得到。
// 只有当 count 发生变化时,才会重新创建回调函数 );
const handleIncrement = useCallback( () => setCount(count + 1), [count],
const myEventHandler = useMemo(() => {
// 返回一个函数作为缓存结果
return () => {
// 在这里进行事件处理
setCount(count + 1)
}
}, [count]);
useRef - 在多次渲染之间共享数据
我们可以把 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.
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
使用 useRef 保存的数据一般是和 UI 的渲染无关的,因此当 ref 的值发生变化时,是不会触发组件的重新渲染的
除了存储跨渲染的数据之外,useRef 还有一个重要的功能,就是保存某个 DOM 节点的引用。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useContext - 定义全局状态
解决跨层次,或者同层的组件之间进行数据共享的问题
React提供了Context这样一个机制,能够让所有在某个组件开始的组件树上创建一个Context。这样这个组件树上的所有组件,就都能访问和修改这个Context了。那么在函数组件里,我们就可以使用useContext这样一个Hook来管理Context。
定义Context
// 创建ThemeContext
const ThemeContext = React.createContext(themes.light);
使用Context.Provider作为根节点
function App() {
// 整个应用使用 ThemeContext.Provider 作为根组件
return (
// 使用 themes.dark 作为当前 Context, themes中保存了一些主题
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
子组件中用useContext读取Context.Provider提供的值
// 在 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>
);
}
Context
Context 相当于提供了一个定义 React 世界中全局变量的机制,而全局变量则意味着两点:
- 会让调试变得困难,因为你很难跟踪某个
Context的变化究竟是如何产生的。 - 让组件的复用变得困难,因为一个组件如果使用了某个
Context,它就必须确保被用到的地方一定 有这个Context的Provider在其父组件的路径上。
所以在
React的开发中,除了像Theme、Language等一目了然的需要全局设置的变量外,我们很少会使用Context来做太多数据的共享。
需要再三强调的是,Context更多的是提供了一个强大的机制,让React应用具备定义全局的响应式数据的能力。
很多状态管理框架,比如 Redux,正是利用了 Context 的机制来提供一种更加可控的组件之间的状 态管理机制。因此,理解 Context 的机制,也可以让我们更好地去理解 Redux 这样的框架实现的原理。
思考题
useState 其实也是能够在组件的多次渲染之间共享数据的,那么在 useRef 的计时器例子中,我们能否用 state 去保存 window.setInterval() 返回的 timer 呢?









网友评论