React-Hooks初探


React-Hooks 设计动机

何谓类组件(Class Component)

  • 所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件

    何谓函数组件/无状态组件(Function Component/Stateless Component)

  • 函数组件顾名思义,就是以函数的形态存在的 React 组件。早期并没有 React-Hooks 的加持,函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。

    函数组件与类组件的对比:无关“优劣”,只谈“不同”

  • 类组件需要继承 class,函数组件不需要;
  • 类组件可以访问生命周期方法,函数组件不能;
  • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;
  • 类组件中可以定义并维护 state(状态),而函数组件不可以;

重新理解类组件:包裹在面向对象思想下的“重装战舰”

类组件是面向对象编程思想的一种表征。面向对象是一个老生常谈的概念了,当我们应用面向对象的时候,总是会有意或无意地做这样两件事情。

  • 封装:将一类属性和方法,“聚拢”到一个 Class 里去。
  • 继承:新的 Class 可以通过继承现有 Class,实现对某一类属性和方法的复用。

React 类组件,有一个问题——它提供了多少东西,你就需要学多少东西。假如背不住生命周期,你的组件逻辑顺序大概率会变成一团糟。“大而全”的背后,是不可忽视的学习成本。
再想这样一个场景:假如我现在只是需要打死一只蚊子,而不是打掉一个军队。这时候继续开动重装战舰,是不是正应了那句老话——“可以,但没有必要”。这也是类组件的一个不便,它太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。
更要命的是,由于开发者编写的逻辑在封装后是和组件粘在一起的,这就使得类组件内部的逻辑难以实现拆分和复用。如果你想要打破这个僵局,则需要进一步学习更加复杂的设计模式(比如高阶组件、Render Props 等),用更高的学习成本来交换一点点编码的灵活度。
这一切的一切,光是想想就让人头秃。所以说,类组件固然强大, 但它绝非万能。

深入理解函数组件:呼应 React 设计思想的“轻巧快艇”

相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。
类组件和函数组件之间,纵有千差万别,但最不能够被我们忽视掉的,是心智模式层面的差异,是面向对象和函数式编程这两套不同的设计思想之间的差异。
说得更具体一点,函数组件更加契合 React 框架的设计理念。何出此言?不要忘了这个赫赫有名的 React 公式:
这是图片

Hooks 的本质:一套能够使函数组件更强大、更灵活的“钩子”

React-Hooks 是什么?它是一套能够使函数组件更强大、更灵活的“钩子”。
前面我们已经说过,函数组件比起类组件“少”了很多东西,比如生命周期、对 state 的管理等。这就给函数组件的使用带来了非常多的局限性,导致我们并不能使用函数这种形式,写出一个真正的全功能的组件。
React-Hooks 的出现,就是为了帮助函数组件补齐这些(相对于类组件来说)缺失的能力。
如果说函数组件是一台轻巧的快艇,那么 React-Hooks 就是一个内容丰富的零部件箱。“重装战舰”所预置的那些设备,这个箱子里基本全都有,同时它还不强制你全都要,而是允许你自由地选择和使用你需要的那些能力,然后将这些能力以 Hook(钩子)的形式“钩”进你的组件里,从而定制出一个最适合你的“专属战舰”。

从核心 API 看 Hooks 的基本形态

useState():为函数组件引入状态

早期的函数组件相比于类组件,其一大劣势是缺乏定义和维护 state 的能力,而 state(状态)作为 React 组件的灵魂,必然是不可省略的。因此 React-Hooks 在诞生之初,就优先考虑了对 state 的支持。useState 正是这样一个能够为函数组件引入状态的 API。
有了 useState 后,我们就可以直接在函数组件里引入 state。

useState 快速上手

从用法上看,useState 返回的是一个数组,数组的第一个元素对应的是我们想要的那个 state 变量,第二个元素对应的是能够修改这个变量的 API。我们可以通过数组解构的语法,将这两个元素取出来,并且按照我们自己的想法命名。一个典型的调用示例如下:

1
const [state, setState] = useState(initialState);

在这个示例中,我们给自己期望的那个状态变量命名为 state,给修改 state 的 API 命名为 setState。useState 中传入的 initialState 正是 state 的初始值。后续我们可以通过调用 setState,来修改 state 的值,像这样:

1
setState(newState)

状态更新后会触发渲染层面的更新,这点和类组件是一致的。
这里需要向初学者强调的一点是:状态和修改状态的 API 名都是可以自定义的。比如在上文的 Demo 中,就分别将其自定义为 text 和 setText:

1
const [text, setText] = useState("初始文本");

“set + 具体变量名”这种命名形式,可以帮助我们快速地将 API 和它对应的状态建立逻辑联系。
当我们在函数组件中调用 React.useState 的时候,实际上是给这个组件关联了一个状态——注意,是“一个状态”而不是“一批状态”。这一点是相对于类组件中的 state 来说的。在类组件中,我们定义的 state 通常是一个这样的对象,如下所示:

1
2
3
4
5
  this.state {
text: "初始文本",
length: 10000,
author: ["xiuyan", "cuicui", "yisi"]
}

这个对象是“包容万物”的:整个组件的状态都在 state 对象内部做收敛,当我们需要某个具体状态的时候,会通过 this.state.xxx 这样的访问对象属性的形式来读取它。
而在 useState 这个钩子的使用背景下,state 就是单独的一个状态,它可以是任何你需要的 JS 类型。像这样:

1
2
3
4
5
6
7
// 定义为数组
const [author, setAuthor] = useState(["xiuyan", "cuicui", "yisi"]);

// 定义为数值
const [length, setLength] = useState(100);
// 定义为字符串
const [text, setText] = useState("初始文本")

你还可以定义为布尔值、对象等,都是没问题的。它就像类组件中 state 对象的某一个属性一样,对应着一个单独的状态,允许你存储任意类型的值。

useEffect():允许函数组件执行副作用操作

函数组件相比于类组件来说,最显著的差异就是 state 和生命周期的缺失。useState 为函数组件引入了 state,而 useEffect 则在一定程度上弥补了生命周期的缺席。
useEffect 能够为函数组件引入副作用。过去我们习惯放在 componentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期里来做的事,现在可以放在 useEffect 里来做,比如操作 DOM、订阅事件、调用外部 API 获取数据等。

Hooks 是如何帮助我们升级工作模式的

1. 告别难以理解的 Class:把握 Class 的两大“痛点”

为了解决 this 不符合预期的问题,各路前端也是各显神通,之前用 bind、现在推崇箭头函数。但不管什么招数,本质上都是在用实践层面的约束来解决设计层面的问题。好在现在有了 Hooks,一切都不一样了,我们可以在函数组件里放飞自我(毕竟函数组件是不用关心 this 的)哈哈,解放啦
至于生命周期,它带来的麻烦主要有以下两个方面:

  • 学习成本
  • 不合理的逻辑规划方式

2. Hooks 如何实现更好的逻辑拆分

生命周期函数常常做一些奇奇怪怪的事情:比如在 componentDidMount 里获取数据,在 componentDidUpdate 里根据数据的变化去更新 DOM 等。如果说你只用一个生命周期做一件事,那好像也还可以接受,但是往往在一个稍微成规模的 React 项目中,一个生命周期不止做一件事情。
像这样的生命周期函数,它的体积过于庞大,做的事情过于复杂,会给阅读和维护者带来很多麻烦。最重要的是,这些事情之间看上去毫无关联,逻辑就像是被“打散”进生命周期里了一样。比如,设置订阅和卸载订阅的逻辑,虽然它们在逻辑上是有强关联的,但是却只能被分散到不同的生命周期函数里去处理,这无论如何也不能算作是一个非常合理的设计。
而在 Hooks 的帮助下,我们完全可以把这些繁杂的操作按照逻辑上的关联拆分进不同的函数组件里:我们可以有专门管理订阅的函数组件、专门处理 DOM 的函数组件、专门获取数据的函数组件等。Hooks 能够帮助我们实现业务逻辑的聚合,避免复杂的组件和冗余的代码。

3. 状态复用:Hooks 将复杂的问题变简单

过去我们复用状态逻辑,靠的是 HOC(高阶组件)和 Render Props 这些组件设计模式,这是因为 React 在原生层面并没有为我们提供相关的途径。但这些设计模式并非万能,它们在实现逻辑复用的同时,也破坏着组件的结构,其中一个最常见的问题就是“嵌套地狱”现象。
Hooks 可以视作是 React 为解决状态逻辑复用这个问题所提供的一个原生途径。现在我们可以通过自定义 Hook,达到既不破坏组件结构、又能够实现逻辑复用的效果。

保持清醒:Hooks 并非万能

尽管我们已经说了这么多 Hooks 的“好话”,尽管 React 团队已经用脚投票表明了对函数组件的积极态度,但我们还是要谨记这样一个基本的认知常识:事事无绝对,凡事皆有两面性。更何况 React 仅仅是推崇函数组件,并没有“拉踩”类组件,甚至还官宣了“类组件和函数组件将继续共存”这件事情。这些都在提醒我们,在认识到 Hooks 带来的利好的同时,还需要认识到它的局限性。
关于 Hooks 的局限性,目前社区鲜少有人讨论。这里我想结合团队开发过程当中遇到的一些瓶颈,和你分享实践中的几点感受:

  • Hooks 暂时还不能完全地为函数组件补齐类组件的能力
  • “轻量”几乎是函数组件的基因,这可能会使它不能够很好地消化“复杂”
  • Hooks 在使用层面有着严格的规则约束

文章作者: 范昌锐
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 范昌锐 !
  目录