# 1. 前言

众所周知, React 是一个用于构建用户界面的 JavaScript 库,它具有以下两个特点:

  • 声明式:以声明式编写 UI,当数据变动时 React 能高效更新并渲染合适的组件。
  • 组件化:构建管理自身状态的封装组件,然后对其组合以构成复杂的 UI

声明式的特点使得开发者不用去手动处理繁琐的 DOM 操作, React 会将声明式的代码转换为命令式的 DOM 操作,并且把数据层面的描述映射到用户可见的 UI 变化中去,从而达到数据驱动视图的目的。

而组件化的特点使得逻辑复用更为便捷,我们可以将一个页面分割成若干个小的组件,组件的状态由各个组件在其内部自行闭环,各个组件间可以自由组合,开发者就像「搭积木」一样,通过对若干个小组件的拼装,最终形成一个完整的页面。

那这两个特点又跟 React Hooks 有什么关系呢?

别着急,接下来,我们就结合这两个特点来看一下 React Hooks 产生的背后的原因,我们为什么需要 Hooks

# 2. 组件化

我们先从组件化这个特点来看,从 React 一出生,就为我们提供了两种编写组件的方式:类组件函数组件。我们也知道, React Hooks 的诞生的一个巨大作用是使得函数组件更为强大,那么在 React Hooks 诞生之前,相比较与类组件,函数组件差在哪里,或者说,相比较于类组件,函数组件有哪些不足,为什么需要搞一个 Hook 出来来增强函数组件。接下来,我们就来比较一下在 React Hooks 诞生之前的类组件和函数组件。

# 2.1 何谓类组件

所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。以下是一个典型的类组件:

class DemoClass extends React.Component {
  // 初始化类组件的 state

  state = {
    text: "",
  };

  // 编写生命周期方法 didMount

  componentDidMount() {
    // 省略业务逻辑
  }

  // 编写自定义的实例方法

  changeText = (newText) => {
    // 更新 state

    this.setState({
      text: newText,
    });
  };

  // 编写生命周期方法 render

  render() {
    return (
      <div className="demoClass">
        <p>{this.state.text}</p>

        <button onClick={this.changeText}>点我修改</button>
      </div>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 2.2 何谓函数组件

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

function DemoFunction(props) {
  const { text } = props;

  return (
    <div className="demoFunction">
      <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
    </div>
  );
}
1
2
3
4
5
6
7
8
9

# 2.3 函数组件与类组件的对比

我们先基于上面的两个 Demo ,从形态上对两种组件做区分。它们之间肉眼可见的区别就包括但不限于:

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

从以上列出的这几点对比里面,频繁出现了「类组件可以 xxx,函数组件不可以 xxx」,那这是否就意味着类组件比函数组件更好,我们应当只使用类组件而摒弃函数组件呢?

如果你有这样的理解,我只能说你片面了,但这也不能怪你,在 React Hooks 出现之前,类组件的能力边界确实强于函数组件,但我们也不能说类组件就 yyds ,函数组件啥也不是,我们应当辩证的看待事物:类组件能力强大,但它肯定也有不足,函数组件能力有限,但也有可取的优点。接下来,我们就来重新审视一下这两种类型的组件。

# 2.4 重新理解类组件

我们再次审视一下上文中这个典型的类组件:

class DemoClass extends React.Component {
  // 初始化类组件的 state

  state = {
    text: "",
  };

  // 编写生命周期方法 didMount

  componentDidMount() {
    // 省略业务逻辑
  }

  // 编写自定义的实例方法

  changeText = (newText) => {
    // 更新 state

    this.setState({
      text: newText,
    });
  };

  // 编写生命周期方法 render

  render() {
    return (
      <div className="demoClass">
        <p>{this.state.text}</p>

        <button onClick={this.changeText}>点我修改</button>
      </div>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

可以看到, React 类组件内部预置了相当多的「现成的东西」等着你去调度/定制,如:状态 state 和生命周期。不管你用不用得到,或者会不会用,反正我都给你,至于你用好用坏,这是你的事,跟我 React 无关。这样真的好吗?我就想打个蚊子,你却给我了辆坦克,虽然打蚊子是绰绰有余,但是万一操作不当,把自己炸了怎么办,由此看来,**多未必就好!**尤其是繁多的生命周期函数,一旦对某个声明周期函数理解的不够透彻,就极容易使得视图渲染陷入死循环,给调试和排查问题带来了极大的困难,此时你可能会说,这是你开发者自己的问题啊,你自己没有理解清楚,用错了,怪人家 React 什么事。对,你这样说也没错,但是作为一个优秀的框架,在实现强大功能的同时,也应当极尽可能的降低使用者的开发负担和心智负担,难道不是吗?

由此看来,类组件的一个不便就是:它太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。

更要命的是,由于开发者编写的逻辑在封装后是和组件强耦合在一起的,这就使得**类组件内部的逻辑难以实现拆分和复用。**如果你想要打破这个僵局,则需要进一步学习更加复杂的设计模式(比如高阶组件、 Render Props 等),用更高的学习成本来交换一点点编码的灵活度。

所以说,类组件固然强大, 但它绝非万能

# 2.5 重新理解函数组件

我们再次审视一下上文中这个典型的函数组件:

function DemoFunction(props) {
  const { text } = props;

  return (
    <div className="demoFunction">
      <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
    </div>
  );
}
1
2
3
4
5
6
7
8
9

相比于类组件,函数组件肉眼可见的轻量、灵活、易于组织和维护、较低的学习成本等。

令人无奈的是,函数组件虽然又轻量又灵活,但是在 React Hooks 诞生之前,函数组件确实没有维护自身状态的能力。

# 2.6 小结

所以从组件化的角度来看,相比较于类组件的大而全,函数组件具有轻量、灵活、易于组织和维护、较低的学习成本等优点,但有个致命的缺点就是函数组件内部无法维护状态,总给人一种「我知道你非常好,我也非常想用你,但就是用不了 😭😭😭」的尴尬处境。当然, React 团队也看到了这一点,那如果能够通过某种方法给函数组件赋予状态维护,访问生命周期等能力,那岂不是更好?所以, React Hooks 应该安排起来了!

# 3. 声明式

了解完组件化后,我们再从声明式角度来看看,在使用 React 进行开发时,我们只需写声明式的书写视图和逻辑代码,无需关心 DOM 操作, React 会将声明式的代码转换为命令式的 DOM 操作,并且把数据层面的描述映射到用户可见的 UI 变化中去,从而达到数据驱动视图的目的。提到数据驱动视图,那必须得引出这个赫赫有名的 React 公式:

UI = Render(state)
1

通俗的讲, React 框架的设计理念就是希望能够达到一种「吃进去数据,吐出来 UI」的效果,而能够与这一设计理念契合的自然就属函数组件了,想想看,函数组件是不是就是这个样子:

function FunctionComponent(数据) {
  return <UI></UI>;
}
1
2
3

既然函数组件是与 React 设计理念非常契合的一种编码方式,那没有理由不推崇函数组件。

# 4. React Hooks 给我带来了什么

通过以上我们知道了类组件虽然大而全,但是在某些场景下会显得特别笨重,有种「坦克打蚊子」的感觉,而且稍不留神就会误伤到自己。而函数组件轻巧灵活,学习成本低,更契合 React 框架的设计理念,但却给我们一种「只能远观,不能亵玩」的尴尬。

幸运的是, React 团队给我们带来了 React Hooks 这一神兵利器,有了 React Hooks 的加持,函数组件不再像以前那样鸡肋,有了更广阔的用武之地。

接下来我们就来看一下, React Hooks 给我带来了什么?或者说为我们解决了哪些痛点?

# 4.1 告别类组件的 this 痛点

在编写类组件的时候,你肯定感受到了类组件中的组件实例 this 有多么难以捉摸。看看下面这段代码:

class Example extends Component {

  state = {

    name: '修言',

    age: '99';

  };

  changeAge() {

    // 这里会报错

    this.setState({

      age: '100'

    });

  }

  render() {

    return <button onClick={this.changeAge}>{this.state.name}的年龄是{this.state.age}</button>

  }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

上述代码中 changeAge 这个方法是 button 按钮的事件监听函数。当点击 button 按钮时,希望它能够修改状态,但事实是,点击发生后,程序会报错。原因很简单, changeAge 里并不能拿到组件实例的 this ,为了解决 this 不符合预期的问题,我们也是想尽了各种办法,之前用 bind 、现在推崇箭头函数。但不管什么招数,本质上都是在用实践层面的约束来解决设计层面的问题。好在现在有了 Hooks ,我们可以将上述类组件改写成函数组件,在函数组件里可以不用关心这些烦人的 this 了。

# 4.2 逻辑拆分

以前在编写稍微大型一点的类组件的时候,代码逻辑曾经一度与生命周期耦合在一起。比如在 componentDidMount 里获取数据,在 componentDidUpdate 里根据数据的变化去更新 DOM 等。如果说你只用一个生命周期做一件事,那好像也还可以接受,但是往往在一个稍微大型的组件中,一个生命周期里不止做一件事情。例如:

componentDidMount() {

  // 1. 这里发起异步调用

  // 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM

  // 3. 这里设置一个订阅

  // 4. 这里随便干点别的什么

  // ...

}

componentWillUnMount() {

  // 在这里卸载订阅

}

componentDidUpdate() {

  // 1. 在这里根据 DidMount 获取到的异步数据更新 DOM

  // 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM(和 DidMount 的第2步一样)

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

像这样的生命周期函数,它的体积过于庞大,做的事情过于复杂,会给阅读和维护者带来很多麻烦。最重要的是,这些事情之间看上去毫无关联,逻辑就像是被“打散”进生命周期里了一样。比如,设置订阅和卸载订阅的逻辑,虽然它们在逻辑上是有强关联的,但是却只能被分散到不同的生命周期函数里去处理,这无论如何也不能算作是一个非常合理的设计。

而在 Hooks 的帮助下,我们完全可以把这些繁杂的操作按照逻辑上的关联拆分进不同的函数组件里:我们可以有专门管理订阅的函数组件、专门处理 DOM 的函数组件、专门获取数据的函数组件等。Hooks 能够帮助我们实现业务逻辑的聚合,避免复杂的组件和冗余的代码

# 4.3 逻辑复用

在没有 Hooks 以前,我们为了更好的逻辑复用,通常需要编写高阶组件或 Render Props ,这是因为 React 在原生层面并没有为我们提供相关的途径。但是自从有了 Hooks 以后,我们可以通过自定义 Hook ,达到既不破坏组件结构、又能够实现逻辑复用的效果。在社区中也衍生出了一些自定义 Hook 库,如阿里开源的 ahooks (opens new window)等,并且 Hooks 这一理念,也被借鉴到了 Vue3 中,在 Vue3 中我们也可以编写一些自定义的 「Vue Hooks」来达到逻辑复用的目的。

# 5. React Hooks 的局限性

React Hooks 虽然给我们带来了这么多的好处,但是它就没有一丝不足吗?其实不然,我们还是得辩证的看待事物,虽然它有非常诱人的好处,但它的局限性也不容忽视。

  • Hooks 暂时还不能完全地为函数组件补齐类组件的能力:比如getSnapshotBeforeUpdatecomponentDidCatch 这些生命周期,目前都还是强依赖类组件的。
  • Hooks 在使用层面有着严格的规则约束:只能在 React 函数中调用 Hook;不要在循环、条件或嵌套函数中调用 Hook
  • 函数组件给了我们一定程度的自由,却也对开发者的水平提出了更高的要求:对于大型的函数组件,业务逻辑的拆分和组织会是一个很大的挑战。

# 6. 总结

类组件虽然大而全,但是在某些场景下会显得特别笨重,有种「坦克打蚊子」的感觉,而且稍不留神就会误伤到自己。而函数组件轻巧灵活,学习成本低,更契合 React 框架的设计理念,但却给我们一种「只能远观,不能亵玩」的尴尬。因此,为了增强函数组件的能力, React Hooks 应运而生, React Hooks 为我们解决了从前开发中的一些痛点,如类组件的实例 this 指向、业务逻辑拆分和复用等,但也对开发者的水平提出了更高的要求。