Why React-Hooks:Hooks 是如何帮助我们升级工作模式的

222 阅读8分钟
本文由《深入浅出搞定React》课程整理而来

函数组件相比类组件来说,有着不少能够利好 React 组件开发的特性,而 React-Hooks 的出现正是为了强化函数组件的能力。现在,基于对 React-Hooks 编码层面的具体认知,想必你对“动机”的理解也已经上了一个台阶。这里我们就趁热打铁,针对“Why React-Hooks”这个问题,做一个加强版的总结。

相信有不少嗅觉敏锐的同学已经感觉到了——没错,这个环节就是手把手教你做“为什么需要 React-Hooks”这道面试题。以“Why xxx”开头的这种面试题,往往都没有标准答案,但会有一些关键的“点”,只要能答出关键的点,就足以证明你思考的方向是正确的,也就意味着这道题能给你加分。

这里,我梳理了以下 4 条答题思路:

  • 告别难以理解的 Class;
  • 解决业务逻辑难以拆分的问题;
  • 使状态逻辑复用变得简单可行;
  • 函数组件从设计思想上来看,更加契合 React 的理念。

关于思路 4,我在上个课时已经讲得透透的了,这里我主要是借着代码的东风,把 1、2、3 摊开来给你看一下。

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

坊间总有传言说 Class 是“难以理解”的,这个说法的背后是 this 和生命周期这两个痛点。

先来说说 this,在上个课时,你已经初步感受了一把 this 有多么难以捉摸。但那毕竟是个相对特殊的场景,更为我们所熟悉的,可能还是 React 自定义组件方法中的 this。看看下面这段代码:

class Example extends Component {
  state = {
    name: 'John',
    age: '20';
  };
  changeAge() {
    // 这里会报错
    this.setState({
      age: '100'
    });
  }
  render() {
    return <button onClick={this.changeAge}>
      {this.state.name}的年龄是{this.state.age}
    </button>

}

你先不用关心组件具体的逻辑,就看 changeAge 这个方法:它是 button 按钮的事件监听函数。当我点击 button 按钮时,希望它能够帮我修改状态,但事实是,点击发生后,程序会报错。原因很简单,changeAge 里并不能拿到组件实例的 this,至于为什么拿不到,我们将在第 15课时讲解其背后的原因,现在先不用关心。单就这个现象来说,略有一些 React 开发经验的同学应该都会非常熟悉。

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

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

对于第一点,大家都学过生命周期,都懂。下面着重说说这“不合理的逻辑规划方式”是如何被 Hooks 解决掉的

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

在过去,你是怎么组织自己的业务逻辑的呢?我想多数情况下应该都是先想清楚业务的需要是什么样的,然后将对应的业务逻辑拆到不同的生命周期函数里去——没错,逻辑曾经一度与生命周期耦合在一起。

在这样的前提下,生命周期函数常常做一些奇奇怪怪的事情:比如在 componentDidMount 里获取数据,在 componentDidUpdate 里根据数据的变化去更新 DOM 等。如果说你只用一个生命周期做一件事,那好像也还可以接受,但是往往在一个稍微成规模的 React 项目中,一个生命周期不止做一件事情。下面这段伪代码就很好地诠释了这一点:

componentDidMount() {
  // 1. 这里发起异步调用
  // 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM
  // 3. 这里设置一个订阅
  // 4. 这里随便干点别的什么 
  // ...
}

componentWillUnMount() {
  // 在这里卸载订阅
}

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

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

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

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

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

过去我们复用状态逻辑,靠的是 HOC(高阶组件)和 Render Props 这些组件设计模式,这是因为 React 在原生层面并没有为我们提供相关的途径。但这些设计模式并非万能,它们在实现逻辑复用的同时,也破坏着组件的结构,其中一个最常见的问题就是“嵌套地狱”现象。

Hooks 可以视作是 React 为解决状态逻辑复用这个问题所提供的一个原生途径。现在我们可以通过自定义 Hook,达到既不破坏组件结构、又能够实现逻辑复用的效果。

要理解上面这两段话,需要你对组件设计模式有基本的理解和应用。如果你读下来觉得一头雾水,也不必心慌。对于组件状态复用这个问题(包括 HOC、Render Props 和自定义 Hook),现在我对你的预期是“知道有这回事就可以了”。如果你实在着急,可以先通过文档中的相关内容简单了解一下。在专栏的第三模块,我会专门把这块知识提出来,放在一个更合适的上下文里给你掰开来讲。

保持清醒:Hooks 并非万能

尽管我们已经说了这么多 Hooks 的“好话”,尽管 React 团队已经用脚投票表明了对函数组件的积极态度,但我们还是要谨记这样一个基本的认知常识:事事无绝对,凡事皆有两面性。更何况 React 仅仅是推崇函数组件,并没有“拉踩”类组件,甚至还官宣了“类组件和函数组件将继续共存”这件事情。这些都在提醒我们,在认识到 Hooks 带来的利好的同时,还需要认识到它的局限性。

关于 Hooks 的局限性,目前社区鲜少有人讨论。这里我想结合团队开发过程当中遇到的一些瓶颈,和你分享实践中的几点感受:

  • Hooks 暂时还不能完全地为函数组件补齐类组件的能力:比如 getSnapshotBeforeUpdate、componentDidCatch 这些生命周期,目前都还是强依赖类组件的。官方虽然立了“会尽早把它们加进来”的 Flag,但是说真的,这个 Flag 真的立了蛮久了……(扶额)
  • “轻量”几乎是函数组件的基因,这可能会使它不能够很好地消化“复杂”:我们有时会在类组件中见到一些方法非常繁多的实例,如果用函数组件来解决相同的问题,业务逻辑的拆分和组织会是一个很大的挑战。我个人的感觉是,从头到尾都在“过于复杂”和“过度拆分”之间摇摆不定,哈哈。耦合和内聚的边界,有时候真的很难把握,函数组件给了我们一定程度的自由,却也对开发者的水平提出了更高的要求
  • Hooks 在使用层面有着严格的规则约束:这也是我们下个课时要重点讲的内容。对于如今的 React 开发者来说,如果不能牢记并践行 Hooks 的使用原则,如果对 Hooks 的关键原理没有扎实的把握,很容易把自己的 React 项目搞成大型车祸现场。

总结

在本课时,我们结合编码层面的认知,辩证地探讨了 Hooks 带来的利好与局限性。现在,你对于 React-Hooks 的基本形态和前世今生都已经有了透彻的了解,也真刀真枪地感受到了 Hooks 带来的利好。学习至此,相信你已经建立了对 React-Hooks 的学习自信。