Description
React Hooks 是 React16.8 之后添加的特性,它是一组「钩子」,为函数式组件增加了不少能力,使得我们不必再写繁琐的类组件,能够用函数组件来精简地表达业务逻辑。那为什么需要 React Hooks 呢?这要从类组件和函数式组件说起。
类组件 VS 函数式组件
所谓类组件,是指如下所示的 React 代码:
class HelloWorld extends React.Component {
constructor() {
this.state = {text: 'hello world'}
}
componentDidMount() {
console.log('mounted')
}
componentDidUpdate() {
console.log('update')
}
render() {
return (
<div>
<p>{this.state.text}</p>
</div>
)
}
}
类组件是面向对象思想下的产物,所谓面向对象的特点,就是 封装
,继承
和 多态
。通常我们将一组属性和方法封装到一个 class 里,然后定义新的 class 去继承,这样就可以在获得原来的功能上继续实现新的功能。当我们编写类组件时,实际上就是继承了 React.Component
这个基类,里面就包含了许多生命周期的方法供我们使用。
所谓函数式组件,在 React Hooks 出现前,是指无状态组件,例如:
function HelloWorld(props) {
const {text} = props
return (
<div>
<p>{text}</p>
</div>
)
}
从代码量对比上看,我们就明显的发现,类组件显得太 "重" 了,相比之下,函数式组件就轻量
的多。类组件虽然 “重”,但是它功能齐全,通过 this 访问各种状态和方法,还可以有多个生命周期钩子可以实现业务逻辑,反观函数式组件,它是“无状态的”,数据只能通过 props 由上级组件传入。我们来简单对比下这两种组件的差异:
类组件 | 函数组件 | |
---|---|---|
状态 | 有状态 | 无状态 |
this | 有 | 无 |
生命周期方法 | 有 | 无 |
继承 | 需要 | 不需要 |
平时写一个类组件,业务逻辑其实是很分散的,比如你在 componentDidMount
定义了一个计时器 timer ,你就要在 componentUnMount
里做清理 timer 的逻辑,类组件固然功能齐全,但业务逻辑强绑定,难以实现拆分和复用,如果要实现复用,就得动用一些高级技巧,比如 render props
,高阶组件
等等,这些都增大了开发者的学习成本。所以 React 团队在思考一种新的开发模式,给函数式组件加上状态管理,一段逻辑可以拆分为一个函数,函数之间可以实现复用,这样就把分散的逻辑聚合起来了,形式上也更灵活,于是 React Hooks 出现了。
React Hooks 介绍
React 一直推崇函数式编程
的理念,有个经典公式 UI = render(data)
,渲染函数接收一组数据,然后吐出 UI。这个过程里,函数发挥着重要作用。
React Hooks 是一组 API 钩子,它把类组件提供的一些功能,移植到函数组件里来,让函数组件也可以承担一些逻辑开发。常用的 hooks 有:
useState
useEffect
useCallback
- ...
这些具体的 API 使用,官方文档说得很清楚,照着文档仔细读一遍,很快就能掌握应用到业务中。我们来看 React Hooks 解决了哪些问题:
- 繁重的 class 类编写
- 业务逻辑拆分、复用问题
- 更方便的状态管理
- 从设计理念上,函数式编程更符合 React 理念
React Hooks 也不是万能的,任何设计都是权衡的艺术,你说类组件繁重,但它功能齐全;你说函数组件轻量,那就意味着一些复杂逻辑函数组件可能胜任不了。React Hooks 还是有它的局限性:
- 目前还不能完全覆盖类组件的所有生命周期函数,有一些比如
getSnapshotBeforeUpdate
还是得依靠类组件 - 如何定义拆分业务逻辑,实现函数复用,对开发者水平是一个挑战
- hooks 有严格的规则约束,在既定规则下编程。
React Hook 原理
React 官方文档强调,React Hooks 只能在函数组件使用,并且不要在循环,条件或嵌套中使用 Hook。为什么 React Hooks 的执行顺序如此重要?因为它底层使用的数据存储结构是 顺序链表
。
React Hooks 的调用链路分为 初始渲染
和 更新阶段
。初始渲染阶段的流程如下:
- 定义 useState
- 通过 resolveDispatcher 获取 dispatcher
- 调用 dispatcher.useState
- 调用 mountState
- 返回目标数组 [state, setState]
这其中,关键步骤在 mountState,这个函数主要是初始化 hooks ,这一步调用了 mountWorkInProgressHook 方法,这个函数新建了一个 hooks 对象,这个对象有个 next 指针,把其它 hooks 对象串联起来了。
以上是初始化阶段,接下来看更新阶段:
- 从 useState 开始
- 通过 resolveDispatcher 获取 dispatcher
- 调用 dispatcher.useState
- 调用 updateState
- 调用 updateReducer
- 返回目标数组 [state, setState]
在更新阶段,updateState 会去依次遍历之前构建好的链表,然后把值更新到链表对应的 hooks 位置。
现在可以来回答为什么不能在 if 语句里调用 hook。假如代码是下面这样:
let flag = false
const [name, setName] = useState('张三')
if (!flag) {
const [age, setAge] = useState(20)
flag = true
}
const [sex, setSex] = useState(0)
初始化 flag 为 false,所以有三个 hooks 存在链表里,它们都绑定了各自的变量和 set××× 方法:
name - setName
|
age - setAge
|
sex - setSex
等到二次渲染时,此时 if 已经是 false 了,所以只会有两个 hooks :
name - setName
|
sex - setSex
这个时候不确定性就出现了,假如此时你要更改年龄,调用了 setAge。React Hooks 是按链表顺序查找,它会认为你要修改第二个 hooks ,但此时第二个 hooks 已经由 age 变成 sex 了,所以就会出现更新错位的情况。
总结
- React Hooks 的出现是为了更好更轻便的复用业务逻辑,抛弃类组件那种偏「重」的书写方式
- 为此,提供了诸如
useState
,useEffect
的钩子,可以在函数组件里实现类组件诸如生命周期之类的逻辑 - Hooks 底层依赖
顺序链表
,因此在使用上,严格遵守顺序,不能嵌套或条件语句里使用。