Skip to content

聊聊 React Hooks #36

Open
Open
@myLightLin

Description

@myLightLin

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 的出现是为了更好更轻便的复用业务逻辑,抛弃类组件那种偏「重」的书写方式
  • 为此,提供了诸如 useStateuseEffect 的钩子,可以在函数组件里实现类组件诸如生命周期之类的逻辑
  • Hooks 底层依赖 顺序链表,因此在使用上,严格遵守顺序,不能嵌套或条件语句里使用。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions