什么是 React Hooks?

一句话总结 React Hooks 就是:
在 react 函数组件中,也可以使用类组件(classes components)的 state 和组件生命周期,而不需要在 mixin、函数组件、HOC组件和 render props 之间来回切换,使得函数组件的功能更加实在,更加方便我们在业务中实现业务逻辑代码的分离和组件的复用

Hooks 在解决什么问题?

一般情况下,我们都是通过组件和自上而下传递的数据流将我们页面上的大型 UI 组织成为独立的小型 UI,实现组件的复用。但是我们经常遇到一个复杂的组件很难实现复用,因为组件的逻辑是有状态的,无法提取到函数组件当中,这在处理动画和表单的时候,尤其常见。当我们在组件中连接外部的数据源,然后希望在组件中执行更多其他的操作的时候,我们就会把组件搞得特别糟糕:

  • 难以复用和共享组件中与状态相关的逻辑,导致产生很多巨大的组件
  • 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 localstate 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面
  • 复杂的模式,如渲染属性和高阶组件
  • 由于业务变动,函数组件不得不改为类组件

那之前,官方推荐怎么解决这个问题呢?答案是:渲染属性(Render Props)和高阶组件(Higher-Order Components),这里就不作更多介绍了

这时候,Hooks就派上用场了,Hooks 允许我们将组件内部的逻辑,组织成为一个可复用的隔离模块

React Hooks 的规则

Hooks 是 JavaScript 函数,但它们强加了两个额外的规则:

  • 只能在顶层调用Hooks,不要在循环,条件或嵌套函数中调用Hook
  • 仅从 React 功能组件调用 Hooks,不要从常规的 JavaScript 函数中调用 Hook(除了自己的定制 Hooks)

state Hooks (useState)

首先看个例子:

浅析 React Hooks - 图1

useState 是 react 自带的一个 hook 函数,它的作用就是用来声明状态变量。useState 这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第 [0]项是当前当前的状态值,第 [1]项是可以改变状态值的方法函数
当用户点击按钮时,我们调用 setCount 函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给 react了,react 将会重新渲染我们的 Example 组件,并且使用的是更新后的新的状态,即count=1

假如一个组件有多个状态值怎么办?

首先,useState 是可以多次调用的,所以我们完全可以这样写:

浅析 React Hooks - 图2

其次,useState 接收的初始值没有规定一定要是 string/number/boolean 这种简单数据类型,它完全可以接收对象或者数组作为参数

唯一需要注意的点是,之前我们的 this.setState 做的是合并状态后返回一个新状态,可以只修改 state 中的局部变量,而不需要将整个修改后的 state 传进去,而 useState 是直接替换老状态后返回新状态,我们修改 state 必须将整个修改后的 state 传进去,因为它会直接覆盖之前的 state,而不是合并之前的 state 对象,例如:

  1. import React, { useState } from 'react';
  2. export function Count() {
  3. const [data, setData] = useState({
  4. count: 0,
  5. name: 'cjg',
  6. age: 18,
  7. });
  8. const handleClick = () => {
  9. const { count } = data;
  10. // 这里必须将完整的state对象传进去
  11. setData({
  12. ...data,
  13. count: count + 1,
  14. })
  15. };
  16. return (<button onClick={handleClick}></button>)
  17. }


从 ExampleWithManyStates 函数我们可以看到,useState 无论调用多少次,相互之间是独立的,其实我们看 hooks 的“形态”,有点类似之前被官方否定掉的 Mixins 这种方案,都是提供一种“插拔式的功能注入”的能力。而 mixins 之所以被否定,是因为 Mixins 机制是让多个 Mixins 共享一个对象的数据空间,这样就很难确保不同Mixins 依赖的状态不发生冲突
而现在我们的 hook,
一方面它是直接用在 function 当中,而不是 class;另一方面每一个 hook 都是相互独立的,不同组件调用同一个 hook 也能保证各自状态的独立性**,这就是两者的本质区别了

react 是怎么保证多个 useState 的相互独立的?

还是看上面给出的 ExampleWithManyStates 例子,我们调用了三次 useState,每次我们传的参数只是一个值,我们根本没有告诉 react 这些值对应的 key 是哪个,那 react 是怎么保证这三个 useState 找到它对应的 state 呢?
答案是,react 是根据 useState 出现的顺序来定的。例如:

浅析 React Hooks - 图3

假如我们改一下:

浅析 React Hooks - 图4

这样一来,

浅析 React Hooks - 图5

鉴于此,react 规定我们必须把 hooks 写在函数的最外层,不能写在 if…else 等条件语句当中,来确保 hooks 的执行顺序一致
**

Effect Hooks (useEffect)

我们给之前的例子增加一个新功能:

浅析 React Hooks - 图6

我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起 ajax 请求获取数据,添加一些监听的注册和取消注册,手动修改 dom 等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate 和 componentWillUnmount。而现在的 useEffect 就相当于这些生命周期函数钩子的集合体,它以一抵三
同时,由于前文所说 hooks 可以反复多次使用,相互独立,所以我们合理的做法是:给每一个副作用一个单独的useEffect 钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰

useEffect 做了什么?

首先,我们声明了一个状态变量 count,将它的初始值设为0。然后我们告诉 react,我们的这个组件有一个副作用。我们给 useEffecthook 传了一个匿名函数,这个匿名函数就是我们的副作用。在这个例子里,我们的副作用是调用browser API 来修改文档标题。当 react 要渲染我们的组件时,它会先记住我们用到的副作用。等 react 更新了DOM之后,它再依次执行我们定义的副作用函数

这里要注意几点:
第一,react 首次渲染和之后的每次渲染都会调用一遍传给 useEffect 的函数。而之前我们要用两个生命周期函数来分别表示首次渲染(componentDidMount),和之后的更新导致的重新渲染(componentDidUpdate)
第二,useEffect 中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而之前的componentDidMount 或 componentDidUpdate 中的代码则是同步执行的。这种安排对大多数副作用来说都是合理的,但有的情况除外,比如我们有时候需要先根据 DOM 计算出某个元素的尺寸再重新渲染,这时候我们希望这次重新渲染是同步发生的,也就是说它会在浏览器真的去绘制这个页面前发生

useEffect 怎么解绑一些副作用?

这种场景很常见,当我们在 componentDidMount 里添加了一个注册,我们得马上在 componentWillUnmount中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了
怎么清除呢?让我们传给 useEffect 的副作用函数返回一个新的函数即可,这个新的函数将会在组件下一次重新渲染之后执行,例如:

浅析 React Hooks - 图7

这里有一个点需要重视:这种解绑的模式跟 componentWillUnmount 不一样,componentWillUnmount 只会在组件被销毁前执行一次而已,而 useEffect 里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍

怎么跳过一些不必要的副作用函数?

每次重新渲染都要执行一遍这些副作用函数,显然是不经济的,怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可,用第二个参数来告诉 react 只有当这个参数的值发生改变时,才执行我们传的副作用函数(第一个参数):

浅析 React Hooks - 图8

当我们第二个参数传一个空数组 [] 时,其实就相当于只在首次渲染的时候执行,也就是 componentDidMount 加componentWillUnmount 的模式。不过这种用法可能带来bug,建议少用

怎么写自定义的Effect Hooks?

通过自己去写一个 Effect Hooks,这样我们就能将可复用的逻辑抽离出来,变成一个个可以随意插拔的“插销”,哪个组件要用就插进哪个组件里,例如:

浅析 React Hooks - 图9

这时候就可以使用这个 hook:

浅析 React Hooks - 图10

参考:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
https://segmentfault.com/a/1190000016953180