深入理解 Hooks

- Published on

深入理解 Hooks
Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。
Hooks 学习图谱

1. 为什么需要 Hooks
React 官方文档里写了他们开发 Hook 的动机,动机恰恰正是 Hook 要解决的问题:
- 在组件之间复用状态逻辑很难:Hook 使你在无需修改组件结构的情况下复用状态逻辑
- 复杂组件变得难以理解,特别是 state 无处不在,往往需要引入 Redux 等状态管理库:Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
- 难以理解的 class:Hook 使你在非 class 的情况下可以使用更多的 React 特性
1.1 为什么 Class Component 复用状态逻辑很难?
1.2 为什么 Class Component 复杂组件变得难以理解?
Class Component
是有状态和生命周期的,所以,复杂一些的组件往往用 Class Component 来实现。
这样往往会导致组件很复杂,难以理解,如:
生命周期中的逻辑毫不相关,如:
componentDidMount 和 componentDidUpdate 用来获取数据;
设置多个事件监听,然后必须在 componentWillUnmount 清除所有监听;
然而,多数情况下,我们无法再把组件拆分成更小的粒度
,因为状态逻辑无处不在。
2. Class VS Function
React 的哲学之一是数据驱动。

目前写 React 组件的两种方式:
Class Component(类组件)
有生命周期,有状态,可以胜任复杂组件。
Function Component(函数组件)
简单易用效率高,但是无生命周期、无状态,无法胜任复杂组件。
我们知道,React 的架构遵循 schedule - render - commit 的运行流程,这个流程是 React 世界最底层的运行规律。
ClassComponent
作为 React 世界的原子,他的生命周期(componentWillXXX/componentDidXXX)是为了介入 React 的运行流程而实现的更上层抽象,这么做是为了方便框架使用者更容易上手。
函数组件又叫“无状态组件”
,在它内部是不能使用 state 的,虽然效率更高,但缺点也足够致命。
所以,我们的组件基本都是 ClassComponent
组织起来的,如果组件很简单,那就用纯函数写的无状态组件
作为一个补充。
思考 🤔:
Class 组件真的比 Function 组件更适合数据驱动??
其实 Class 并不适合数据驱动。
Class 的最大好处是继承,可是 React 推荐的组件方式是组合;
Class 的另一个好处是可以使用实例的属性和方法来保存数据,可是我们一般是不会在外部调用实例的属性和方法;
React 出现前,调用组件的方式往往是:dialog.show(), dialog.hide() 这样的方式细粒度地去控制 UI 的变化。
函数组件就显得比较合适了。新的问题是,函数组件如何保存数据?
于是,Hooks 就诞生了。
3. Hooks 的作用与工作机制
如果我们想要让函数组件更有用,目标就是给函数组件加上状态。
所以,Hooks 的机制就是:
把某个目标结果钩到某个可能会变化的数据源或者事件源上;
当被钩到的数据或事件发生变化时,产生这个目标结果的代码(钩子函数)会重新执行
产生更新后的结果(DOM 树)
Hooks 工作原理

4. Hooks 是什么
Hooks是一些可以让你在函数组件
里“钩入” React state 及生命周期等特性的函数。
我们来分析下 React 官方给的这个定义。
Hooks 是 函数。
是什么样的函数?
是在函数组件里“钩入” React state
及生命周期
等特性的函数。
那么 Hooks 的特性就很好理解了:
只能写在函数组件中
可以用它来“钩入”React state 及生命周期等特性
在 React 中,我们可以说Class Component
是一类原子。
那么,Hook 在 React 中就是电子,能在函数组件中更细粒度控制 React 组件。
Hook 拥抱了函数,是我们在函数组件
里可以使用几乎所有Class Component
的特性。
5. Hooks 优势
需要特别注意,Hooks 中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:逻辑的复用。
5.1 最大优势:逻辑复用
Hooks 带来的最大好处:逻辑复用。
Class 实现逻辑复用
第一步:定义一个高阶函数,获取监听窗口变化,并把结果传递给子组件
const withWindowSize = Component => {
// 产生一个高阶组件 WrappedComponent,只包含监听窗口大小的逻辑
class WrappedComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
size: this.getSize()
};
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
getSize() {
return window.innerWidth > 1000 ? "large" :"small";
}
handleResize = ()=> {
const currentSize = this.getSize();
this.setState({
size: this.getSize()
});
}
render() {
// 将窗口大小传递给真正的业务逻辑组件
return <Component size={this.state.size} />;
}
}
return WrappedComponent;
};
class MyComponent extends React.Component {
render() {
const { size } = this.props;
if (size === "small") return;
else return;
}
}
// 使用 withWindowSize 产生高阶组件,用于产生 size 属性
export default withWindowSize(MyComponent);
Hooks 实现同样功能:
第一步:定义一个 Hooks
const getSize = () => {
return window.innerWidth > 1000 ? "large" : "small";
};
const useWindowSize = () => {
const [size, setSize] = useState(getSize());
useEffect(() => {
const handler = () => {
setSize(getSize());
};
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}, []);
return size;
};
第二步:使用 Hooks
const Demo = () => {
const size = useWindowSize();
if (size === "small") return <SmallComponent />;
else return <LargeComponent />;
};
5.2 好处二:有助于关注点分离
仍然以上面监听浏览器窗口大小的变化为例,我们来看 Hooks 是如何做到关注分离的。
在过去的 Class 组件中,我们需要在 componentDidMount 中监听事件,在 componentWillUnmount 中去解绑事件。
而在函数组件中,我们可以把所有逻辑写在一起。
Class 组件和 Hooks 最大区别:
- Class 组件中,代码是从技术角度组织在一起的(例如在 componentDidMount 中都去做一些初始化的事情)。
- 函数组件中,代码是从业务角度组织在一起的,相关代码能够出现在集中的地方,从而更容易理解和维护。
6. Hooks 分类和使用
Hooks 大致可以分为 4 种:
6.1 State Hook
React 内置了一些像 useState
这样的 Hook
,它是 State Hook
。
import React, { useState } from "react";
function Example() {
// 声明一个叫 “count” 的 state 变量
// 设置了 “count” 初始化值为 0
// 改变 “count” 可以使用 setCount 这个函数
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
使用原则:
- state 中永远不要保存可以通过计算得到的值
如:props 传递过来的值、从 URL 中读到的值、从 cookie、localStorage 中读取的值
6.2 Effect Hook
useEffect 作用?
React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。
在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为
“副作用
”,或者简称为“作用(Effect)”
。
6.2.1 什么是副作用
副作用
是函数执行中和返回值无关或改变返回值的操作。
如:console.log()、Ajax 获取数据并调用 setState 更新状态。
以上操作 console.log 和返回值无关,Ajax 请求的数据会使得函数返回不确定的值,因此都算是副作用。
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。
当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。
useEffect 和 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。
例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:
import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
// 相当于 componentDidMount 、 componentDidUpdate 和 componentWillUnMount 的组合
// 挂载和更新都会执行这个副作用
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
6.2.2 useEffect 的使用
源码中的 useEffect:
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
useEffect 接收两个参数:
effect(Function):可以返回清理函数的执行函数
deps(Array): 可选,effect 更新的依赖,只有当列表中的值发生变化时,effect 才会执行
deps 为空数组“[]”时代表只有挂载时执行
示例:
useEffect(() => {
// 执行函数获取数据并更新数据
fetch(url)
.then((res) => res.json())
.then(updateData())
.catch((err) => console.log(err));
// 返回清理函数
// 会在调用一个新的 effect 之前对前一个 effect 进行清理
// 一般不需要我们手动清理,关于这部分问题请看官方文档,有详细介绍
return () => {
resetData();
};
});
6.2.3 useEffect 特性 📢
1. useEffect 会在每次组件更新后执行
例如:我们 setState 引起了组件更新,那么 useEffect 就会执行。
显然,这样有时是不合理的,我们应该尽量减少 effect 的默认更新。
2. deps 为空[] 时,effect 只在 mount 和 unmount 时执行
useEffect(() => {
// do Something
}, []);
3. deps 是不为空的[] 时,effect 只在 deps 里的状态值发生变化时执行。
// mount 发生变化时才会执行
useEffect(() => {
// do Something
}, [mount]);
6.2.4 useEffect 中使用异步函数
示例:
useEffect(() => {
// 异步函数包裹一下业务
const fetchData = async () => {
const resData = await fetch(url);
const data = await resData.json();
// 操作data
setData(data);
};
// 手动执行函数
fetchData();
});
6.2.5 useEffect 的好处
1. 使用多个 Effect 实现关注点分离
这个可以解决把过多副作用都放到 componentDidMount 中的问题,解决
“复杂组件变得难以理解”
的问题。我们可以使用多个 useEffect 来处理不同的逻辑,以此实现关注点分离。
2. 通过跳过 Effect 进行性能优化
你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
6.2.6 useEffect 是如何实现的
**useEffect(effect, deps)**函数里的Effect
在 React 内部是一个链表结构,React 会在每次 DOM 更新后调用需要执行的Effect
。
6.3 useContext
6.3.1 React 跨组件数据传递的 props drilling 问题
父组件传数据给子组件:Props
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
我们想要的数据是“theme”,它要在 3 个组件之间通过 props 来传递 APP -> Toolbar -> ThemedButton;
最终我们会在 组件 ThemedButton 使用这个数据。
这样 组件 Toolbar 就做了一件没意义的事: 虽然没有使用“theme”,还是要传递这个值给子组件。
这个问题叫做props drilling(深度注入).
6.3.2 Context
Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
Context 机制:
- 在某个组件父组件树上创建一个 Context
- 所有子组件,就都能访问和修改这个 Context 了
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee",
},
dark: {
foreground: "#ffffff",
background: "#222222",
},
};
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext(themes.light);
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
6.3.3 useContext
Hooks 提供了一个useContext
Hook 函数,
它接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。
当前的 context 值由上层组件中距离当前组件最近的 MyContext.Provider
的 value prop 决定。
当组件上层最近的 MyContext.Provider
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 MyContext.Consumer。 useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 MyContext.Provider 来为下层组件提供 context。
将上面的 class 组件改为函数组件:
function App() {
return (
// 父组件依然需要提供 ThemeContext.Provider
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 子组件通过 useContext 获取 ThemeContext
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
6.3.4 useContext 是如何实现的
todo...
6.4 useCallback: 缓存回调函数
useCallback(fn, deps)
首先需要明确的是:
React 函数组件中,每一次 UI 的变化,都是通过重新执行整个函数来完成的.
所以,像下面这样,函数handleIncrement会在每次更新时重新创建一遍。
有两个弊端:
- 每次创建新的函数,增加不必要的开销
- 更重要的是:每次创建新函数会让接收事件处理函数的组件,需要重新渲染
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
// ...
return <button onClick={handleIncrement}>+</button>;
}
使用 useCallback 进行性能优化
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>;
}
6.5 useMemo:缓存计算的结果
useMemo(fn, deps);
fn 是产生所需数据的一个计算函数。
通常来说,fn 会使用 deps 中声明的一些变量来生成一个结果,用来渲染出最终的 UI。
如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。
如:列表中搜索用户名,显示用户信息
// 使用 userMemo 缓存计算的结果
const usersToShow = useMemo(() => {
if (!users) return null;
return users.data.filter((user) => {
// 缓存的是这里的计算结果🌟🌟
return user.first_name.includes(searchKey));
}
}, [users, searchKey]);
//...
userMemo 避免了因为其他更新而导致的重复计算。
useMemo与useCallback的关系:
一个是缓存结果,一个是缓存函数;
如果useMemo返回的是函数,那么,可以认为二者是等效的。
useMemo实现了useCallback:
const myEventHandler = useMemo(() => {
// 返回一个函数作为缓存结果
return () => {
// 在这里进行事件处理
};
}, [dep1, dep2]);
6.6 useRef:在多次渲染之间共享数据
在类组件中,我们可以定义类的成员变量,以便能在对象上通过成员属性去保存一些数据。
但是在函数组件中,是没有这样一个空间去保存数据的。
因此,使用useRef来保存数据。
我们可以把 useRef 看作是在函数组件之外创建的一个容器空间。
使用 useRef 定义一个 timer
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 区别于 useState 的地方。
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>
</>
);
}
6.6 自定义 Hook
自定义 Hook 更像是一种约定而不是功能。
自定义 Hook
可以在组件之间重用一些状态逻辑。
如果函数的名字以 “use” 开头并调用其他 Hook,我们就说这是一个自定义 Hook。
自定义 Hook的作用是不增加组件的情况下,像 高阶组件
和 render props
一样,解决函数组件之间重用一些状态的问题。
具体示例,请看官网示例,解释的比较清楚,这里不再赘述。
需要注意 ⚠️:
定义 Hook 必须以 “use” 开头
在两个组件中使用相同的 Hook 不会共享 state,只是重用了自定义 Hook 的代码而已
每次调用自定义 Hook,它都会获取独立的 state(像多次调用 useState 一样)
6.5 其他 Hook
还有一些使用频率较低的但是很有用的 Hook,查看Hook 索引。
比如, useReducer 可以让你通过 reducer 来管理组件本地的复杂 state
function Todos() {
const [todos, dispatch] = useReducer(todosReducer);
// ...
}
Hooks 的使用 还是看官方文档吧,这里没必要复述了。
7. Hooks 工作原理解析
todo...
8. 手动实现一个 useState 方法
todo...
分享一下不可错过的 Hooks 总结。
参考
useEffect 关于对象/数组的依赖,写的很透彻 Object & array dependencies in the React useEffect Hook