[译] React 的今天和明天(图文版) —— 第二部分

5,731 阅读36分钟

因为这个演讲 Dan 的 Demo 部分比较多,建议如果时间充裕,可以观看视频。希望看本文视频的同学,可以查看我的这篇文章:React Conf 2018 专题 —— React Today and Tomorrow Part II 视频中英双语字幕。第一部分 Sophie Alpert 的演讲图文版地址:[译] React 的今天和明天(图文版) —— 第一部分

React 的今天和明天 —— 第二部分

嗨。我的名字是 Dan。我在 React Team 工作,这是我第一次参加 React 大会。 (掌声)

React 当前面临的问题

刚才 Sophie 讲述了这三个问题,我想大多数的开发者在 React 开发过程中都会遇到这些问题。当然,我们可以逐一来解决这些问题。我们可以尝试独立地去解决这些问题。但是实际上解决其中一个问题可能会使其他问题更加严重。

2018-11-12 11 58 06

比如我们尝试解决“包装地狱”问题,可以将更多的逻辑放到组件里面,但是我们的组件会变得更大,而且更难以重构。另一个方面,如果我们为了方便重用,尝试将组件拆分为更小的片段,那么组件树的嵌套会更多了,而且最终又会以“包装地狱” 收场。最后,无论那种情况,使用 class 都会让人产生困惑。

因此我们认为造成这种情况是因为这不是三个独立的问题。我们认为,这是同一个问题的三个症状。问题在于 React 没有原生提供一个比 class 组件更简单、更小型、更轻量级的方式来添加 state 或生命周期。

2018-11-12 11 59 14

而且一旦你使用了 class组件,你没有办法在不造成“包装地狱”的情况下,进一步拆分它。事实上,这并不是一个新问题。如果你已经使用了 React 几年,你也许还记得在 React 刚出来的时候,事实上已经包含了一个针对该问题的解决方案。嗯,这个解决方案就是 mixins。Mixins 能够让你在 class 之间复用方法,并且可以减少嵌套。

2018-11-13 12 15 16

所以我们要在 React 里面重新把 mixins 添加回来吗? (对 ... 不...)对了,不,不,我们不会添加 mixins。我的意思是之前使用mixins 的代码并不是无法使用了。但是我们不再推荐在 React 里使用 mixins。如果你好奇我们这么做的原因,可以在 React Blog 里面查看我们之前写的一篇文章,题目是《 Mixins 是有害的 》。在文章中,我们从实验结果发现 mixins 带来的问题远比它解决的问题多。因此,我们不推荐大家使用 mixins。

我们有一个提案

那么也许我们解决不了这个问题了,因为这是 React 组件模型固有的问题。也许我们不得不选择接受现实。(笑声) 或者也许有另外一种书写组件的方法可以避免这些问题。

2018-11-13 12 17 50

这也就是今天我将要分享的内容。

2018-11-13 12 18 30

但是在开始分享我们在 React 上做出的改动和新特性之前,我想先讲讲一年前我们建立的 RFC 流程,RFC 表示 request for comments,它意味着无论是我们还是其他人想要对 React 做出大量变化或者添加新特性时,都需要撰写一个提案,提案里面需要包含动机的详情和该提案如何工作的详细设计。

这正是我们要做的事情。我们非常兴奋地宣布:我们已经准备好了一个提案来解决这三个问题。

2018-11-13 12 23 03

重要的是,本提案没有不向下兼容的变化,也没有弃用任何功能。本提案是严格添加性的、可选择的而且增加了一些新的 API 来帮助我们解决这些问题。并且我们希望听到你们对本提案的反馈,这也是为什么我们在今天发布本提案的原因。

2018-11-13 9 25 08

我们想过很多发布本提案的方式,也许我们可以写好提案后,提出一个 RFC 然后放在那里。但是既然我们总是要召开 React 大会,我们决定在本次大会上发布这个提案。

Demo 环节

那么,接下来进入 Demo 环节。(掌声)

2018-11-13 9 39 36

我的屏幕已经投在了显示器上。对不起,有点技术故障。呃,有谁会用这个投影仪,来帮帮我。(笑声) 呃,我能复制我的桌面吗?请。(我能) 是啊。(笑声)好的,但是屏幕上没有显示,我什么都看不到。 (笑声)这就是我现在的问题。 (掌声)好的,灾难过去了。(笑声)好的,嗯,让我来稍微调整下文字大小。你们能看清吗? (可以的。) 好的。

一个熟悉的 class 组件例子

那么,我们来看,这里是一个普通的 React 组件,这是一个 Row 组件,这里有一些样式,然后渲染出一个人名。

import React from 'react';
import Row from './Row';

export default function Greeting(props) {
  return (
    <section>
      <Row label="Name">
        {props.name}
      </Row>
    </section>
  );
}

我们想要做的是让这个名字可编辑。那么平时我们在 React 里通常是怎么做的呢?我们需要在这里添加一个 input,需要将这些内容放到class 里面返回,添加一些本地 state,让 state 来驱动 input。这也是我准备做的事情。这也是现今大家通常做的事情。

我要导出 default class Greeting 继承 React.Component。我在这里只会使用稳定的 JavaScript语法。接下来是 constructor(props), super (props)。在这里把 state 里的 name 初始化为 Mary。接下来我要声明一个 render 函数,复制一下这段代码然后粘贴到这里。对不起。好的。

demo1

我希望这里不再仅仅渲染 name,我希望这里可以渲染一个 input。我把这里替换为一个 input,然后 input 的值设置为 this.state.name。然后在 input 输入发生变化时,调用 this.handleNameChange,这是我的change 事件的回调函数。我把它声明在这里,当名字发生变化时,像我们通常做的那样调用 setState 方法。然后将 name 设置为 e.target.value。对吧。

demo2

如果我编辑 ... (页面上报了 TypeError 的错误) 好吧,所以我应该去绑定 ... (笑声) 对不起,我需要在这里绑定 event 事件。 好的,现在这样我们就可以编辑它了,运行正常。

demo3

这个 class 组件我们应该非常熟悉了。你如果使用 React 开发可能会遇到很多类似的代码。

import React from 'react';
import Row from './Row';

export default class Greeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Mary'
    }
    this.handleNameChange = this.handleNameChange.bind(this);
  }

  handleNameChange(e) {
    this.setState({
      name: e.target.value
    })
  }
  render() {
    return (
      <section>
        <Row label="Name">
          <input
            value={this.state.name}
            onChange={this.handleNameChange}
          />
        </Row>
      </section>
    );
  }

}

该功能可以用 function 组件实现吗

但让我们后退一步,如果想要使用 state 时,能不能不必须使用 class 组件呢?我不确定该怎么做。但是我就准备跟据我的已知来进行,我需要渲染一个 input。我在这里放入一个 input。这个 inputvalue 的值为当前的 name 的值,所以我就传入 name 值。我不知道从哪里获取 name。它不是从 props 里面来,嗯,我就在这里声明,我不知道它的值,之后我再填写这一块。

呃,这里应该也有一个 change 回调函数,我在这里声明 onChange 函数 handleNameChange。我在这里添加一个函数来处理事件。在这里我想要通知 React 设置 name 值到某处,但又一次地,我不确定在 function 组件里如何实现这个功能。因此我就直接调用一个叫做 setName 的方法。使用当前的 input 的值。我把它声明在这里。

import React from 'react';
import Row from './Row';

export default function Greeting(props) {
  const name = ???
  const setName = ???

  function handleNameChange(e) {
    setName(e.target.value);
  }

  return (
    <section>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
    </section>
  );
}

好吧,由于这两件事情是密切相关的,对吧。其中一个是 state 里 name 变量的当前值,而另一个是一个函数,该函数让我们去设置 state 里的 name 变量。由于这两件事情非常相关,我将它们合并到一起作为一对值。


- const name = ???
- const setName = ???
+ const [name, setName] = ???

我们从某处一同获取到它们的值。所以问题是我从哪里获取到它们?答案是从 React 本地状态里面获取。 那么我如何在 function 组件里面获取到 React 到本地状态呢?嗯,我直接使用 useState 会怎样。把初始到状态传给 useState 函数来指定它的初始值。

import React, { useState } from 'react';
import Row from './Row';

export default function Greeting(props) {
  const [name, setName] = useState('Mary');

  function handleNameChange(e) {
    setName(e.target.value);
  }

  return (
    <section>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
    </section>
  );
}

我们来看一下程序运行是否正常。是的,运行正常。

demo4

(掌声和欢呼声)

那么我们来比较一下这两种方式。在左侧是我们熟悉的 class 组件。这里 state 必须是一个对象。嗯,我们绑定一些事件处理函数以便调用。在事件处理函数里面使用了 this.setState 方法。当我们调用 setState 方法时,实际上并没有直接将值设置到 state 里面,state 作为参数合并到 state 对象里。而当我想要获取 state 时,我们需要调用 this.state.something

import React from 'react';
import Row from './Row';

export default class Greeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Mary'
    }
    this.handleNameChange = this.handleNameChange.bind(this);
  }

  handleNameChange(e) {
    this.setState({
      name: e.target.value
    })
  }
  render() {
    return (
      <section>
        <Row label="Name">
          <input
            value={this.state.name}
            onChange={this.handleNameChange}
          />
        </Row>
      </section>
    );
  }

}

那么我们再来看右侧的例子:我们不需要使用 this.state.something 来获取 state。因为 state 里的 name 变量在函数里已经可用。它就是一个变量。同样的,当我们需要设置 state 时,我们不需要使用 this.something。因为函数也可以让我们在其作用域内设置 name 的值。那么 useState 到底是什么呢? useState 是一个 Hook。Hook 是一个 React 提供的函数,它可以让你在 function 组件中“钩”连 到一些 React 特性。而 useState 是我们今天讲到的第一个 hook,后面还有一些更多的 hook。我们随后会看到它们。

import React, { useState } from 'react';
import Row from './Row';

export default function Greeting(props) {
  const [name, setName] = useState('Mary');

  function handleNameChange(e) {
    setName(e.target.value);
  }

  return (
    <section>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
    </section>
  );
}

使用 class 和 hook 两种方式实现增加姓氏编辑区域

好的,让我们回到我们熟悉的 class 例子。我们接下来想要添加第二个区域。比如,添加一个姓氏的区域。那么我们通常的做法是在 state 添加一个新 key。我把这行复制然后粘贴到这里。这里改成 surname。在这里渲染,这里是 surnamehandleSurnameChange。我再来复制这个事件处理函数,把这里改成 surname。别忘了绑定这个函数。好的,Mary Poppins 显示出来了,我们可以看到程序运行正常。

import React from 'react';
import Row from './Row';

export default class Greeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Mary',
      surname: 'Poppins',
    }
    this.handleNameChange = this.handleNameChange.bind(this);
    this.handleSurnameChange = this.handleSurnameChange.bind(this);
  }

  handleNameChange(e) {
    this.setState({
      name: e.target.value
    })
  }

  handleSurnameChange(e) {
    this.setState({
      surname: e.target.value
    })
  }

  render() {
    return (
      <section>
        <Row label="Name">
          <input
            value={this.state.name}
            onChange={this.handleNameChange}
          />
        </Row>
        <Row label="Surname">
          <input
            value={this.state.surname}
            onChange={this.handleSurnameChange}
          />
        </Row>
      </section>
    );
  }

}

那么我们如何使用 hook 来实现相同的功能呢?我们需要做的一件事情是把我们的 state 改为一个对象。可以看到,使用 hook 的 state 并不强制其类型必须为对象。它可以是任何原生的 JavaScript 类型。我们可以在需要的时候把它变为对象,但是我们不用必须这么做。

从概念上讲,surname 和name 关系不大。所以我们需要做的是,再次调用 useState hook 来声明第二个 state 变量。在这里我声明 surname,当然我可以给它起任何名字,因为它就是我程序里的一个变量。再来设置 setSurname。调用 useState,传入 state 初始变量 'Poppins'。我再一次复制和粘贴这个 Row 片段。值改为 surname,onchange 事件改为 handleSurnameChange。当用户编辑surname 时,不是 sir name,我们希望能够修改 surname 的值。

import React, { useState } from 'react';
import Row from './Row';

export default function Greeting(props) {
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <section>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
      <Row label="Surname">
        <input
          value={surname}
          onChange={handleSurnameChange}
        />
      </Row>
    </section>
  );
}

我们来看看能否正常运行。耶,运行正常。 (掌声)

所以我们可以看到,我们可以在组件里使用多次 hook。 我们来更详细地比较这两种方式。在左侧我们熟悉的 class 组件里的 state 总是一个对象,具有多个字段,需要调用 setState 函数将其中的某些值合并进 state 对象中。当我们需要获取它时,需要调用 this.state.something。在右侧使用 hook 的例子中,我们使用了两次 hook,声明了两个变量:name 和 surname。而且每当我们调用 setNamesetSurname 时,React 会接到需要重新渲染该组件的通知,就和调用 setState 一样。所以下一次 React 渲染组件会将当前的 namesurname 传递给组件。而且我们可以直接使用这些 state 变量,不需要调用 this.state.something

用 class 和 hook 两种方式使用 React context

好的。我们再回到我们的 class 组件的例子。有没我们知道的其他的 React 特性呢?那么另外一个你可能希望在组件里面做的事情就是读取 context。有可能你对 context 还不熟悉,它就像一种为了子树准备的全局变量。 Context 在需要获取当前主题或者当前用户正在使用的语言很有用。尤其是所有组件都需要读取一些相同变量时,使用 context 可以有效避免总是通过 props 传值。

让我们导入 ThemeContextLocaleContext,这两个 context 我已经在另一个文件里定义好了。可能你们最熟悉的用来消费 context,尤其是消费多个 context 的 API 就是 render prop API。就像这样写。我往下滚动到这里。我们使用 ThemeContext Consumer 获得主题。在我的例子里,主题就是个简单的样式。我把这段代码复制,将其全部放入render prop 内部。将 className 赋值为 theme。好的,非常老旧的样式。(笑声)

我也想展示当前的语言,因此我将要使用 LocaleContext Consumer。我们再来渲染另一行,把这行代码复制粘贴到这里,改成 language。 Language。在这里渲染。好的,我们能够看到 context 运行了。

import React from 'react';
import Row from './Row';
import { ThemeContext, LocaleContext } from './context';

export default class Greeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Mary',
      surname: 'Poppins',
    }
    this.handleNameChange = this.handleNameChange.bind(this);
    this.handleSurnameChange = this.handleSurnameChange.bind(this);
  }

  handleNameChange(e) {
    this.setState({
      name: e.target.value
    })
  }

  handleSurnameChange(e) {
    this.setState({
      surname: e.target.value
    })
  }

  render() {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <section className={theme}>
            <Row label="Name">
              <input
                value={this.state.name}
                onChange={this.handleNameChange}
              />
            </Row>
            <Row label="Surname">
              <input
                value={this.state.surname}
                onChange={this.handleSurnameChange}
              />
            </Row>
            <LocaleContext.Consumer>
              {locale => (
                <Row label="Language">
                  {locale}
                </Row>
              )}
            </LocaleContext.Consumer>
          </section>
        )}
      </ThemeContext.Consumer>

    );
  }

}

这也许是最普通的消费 context 情况了。实际上,我们在 React 16.6 版本上增加了一个更加方便的 API 来获取它。呃,但是这就是你们常见的多 context 的情形。那么我们看一下如何使用 hook 实现相同的功能。

就像我们所说,state 是 React 的基础特性,因此我们可以使用 useState 来获取 state。那么如果我们想要使用 context,首先需要导入我的 context。这里导入 ThemeContextLocaleContext。现在如果我想在我组件里使用 context,我可以使用 useContext。可以使用 ThemeContext 获取当前的主题,使用 LocaleContext 获取当前的语言。这里 useContext 不只是读取了 context,它也订阅了该组件,当 context 发生变化,组件随之更新。但现在 useContext 就给出了 ThemeContext 的当前值 theme,所以我可以将其赋给 className。接下来我们添加一个兄弟节点,把label 改为 Language, 把 locale 放到这里。 (掌声)

import React, { useState, useContext } from 'react';
import Row from './Row';
import { ThemeContext, LocaleContext } from './context';

export default function Greeting(props) {
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);


  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <section className={theme}>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
      <Row label="Surname">
        <input
          value={surname}
          onChange={handleSurnameChange}
        />
      </Row>
      <Row label="Language">
        {locale}
      </Row>
    </section>
  );
}

那么,让我们比较这两个方法。左边的例子是传统的 render prop API 的使用方式。非常清楚地显示了它正在做什么。但是它还包含了一点点的嵌套,而且嵌套问题不只会在使用 context 的情况下出现,使用任何一种类型的render prop API 都会遇到。

我们使用 hook 也能实现相同的功能。但是代码会更扁平。那么我们来看一下,我们使用了两个 useContext,从中我们得到了 themelocale。然后我们可以使用它们了。你可能想问 React 是如何知道的,例如,我在这调用了两个 useState,那么 React 是如何知道哪一个 state 和调用的哪一个 useState 是相对应的呢?答案是 React 依赖于这些调用的顺序,这可能有一点不太寻常。

为了让 hook 正确地运行,在使用 hook 时,我们需要遵循一条规则:不能在条件判断里面调用 hook,它必须在你的组件的顶层。举个例子,我做一些类似于 if props 条件的判断,然后我在条件里面调用 useState hook。我们开发了一个 Linter 插件,此时会提示 'This is not the correct way to use hooks'。

虽然这是一个不同寻常的限制,但是这对 hook 正常运行十分重要,同时可以使事情变得更明确,我认为你们会喜欢它的,我等会儿会向你们展示它。

如何使用 class 和 hook 两种方式处理副作用

那么,让我们回头看看我们的 class。你使用 class 想要做到的另一件事可能就是生命周期函数。而最普遍的使用生命周期函数的案例就是处理一些副作用,比如发送请求,或者是调用某些浏览器 API 来监测 DOM 变化。但是你不能在渲染阶段去做这些类似的事情,因为此时 DOM 可能还没有渲染完成。因此,在 React 中处理副作用的方法是声明如 componentDidMount 的生命周期方法。

那么比如说,嗯,让我向你们展示一下这个。那么,你看到在屏幕的顶部,页签上显示的标题是 React App。这里实际上有一个让我们更新这个标题的浏览器 API。现在我们想要这个页签的标题变成这个人的名字,并且能够随着我输入的值而改变。

现在我要初始化它。嗯,有一个浏览器 API 可以做这件事,那就是 document.title,等于this.state.name 加空格加 this.state.surname。现在我们可以看见这里显示出了 Mary Poppins。但是如果我编辑姓名,页签上的标题没有自动地更新,因为我还没有实现 componentDitUpdate 方法。为了让该副作用和我渲染保持一致,我在这里声明 componentDitUpdate,然后复制这段代码并粘贴到这里。现在标题显示的是 Mary Poppins,如果我开始编辑输入框,页签标题也随之更新了。这就是我们如何在一个 class 里处理副作用的例子。


+  componentDidMount() {
+    document.title = this.state.name + ' ' + this.state.surname;
+  }

+  componentDidUpdate() {
+    document.title = this.state.name + ' ' + this.state.surname;
+ }

那么我们要如何用 hook 实现相同的功能呢?处理副作用的能力是 React 组件的另一个核心特性。所以如果我们想要使用副作用,我们需要从 React 里导入一个 useEffect。然后我们要告诉 React 在 React 清除组件之后 对 DOM 做什么。所以我们在 useEffect里面传递一个函数作为参数,在函数里处理副作用,在这里代码改为 document.title = name + ' ' + surname

-  import React, { useState, useContext } from 'react';
+ import React, { useState, useContext, useEffect } from 'react';

+  useEffect(() => {
+    document.title = name + ' ' + surname;
+  })

可以看到,页面标题显示为 Mary Poppins。如果我开始编辑它,页面标题也会随之更新。

所以,userEffect 默认会在初始渲染和每一次更新之后执行。所以通过默认的,页面标题与这里渲染的内容保持一致。如果出于性能考虑或者有特殊的逻辑,可以选择不采用这种默认行为。在我之后,Ryan 的演讲将会涉及到一些关于这个方面的内容。

那么让我们来比较这两个方法。在左边这个class 里,我们将逻辑分开到不同名称的生命周期方法中。这也是我们为什么会有 componentDidMountcomponentDitUpdate 的原因,它们在不同的时间上被触发。我们有时候会在它们之间重复一些逻辑。虽然可以把这些逻辑放进一个函数里,但是我们仍然不得不在两个地方调用它,而且要记得保持一致。

而使用 effect hook,默认具有一致性,而且可以选择不使用该默认行为。需要注意的是,在 class 中我们需要访问 this.state, 所以需要一个特殊的 API 来实现。但是在这个 effect 例子中,实际上不需要一个特殊的 API 去访问这个 state 变量。因为它已经在这个函数的作用域里,在上文中已经声明。这就是 effect 被声明在组件内部的原因。而且这样我们也可以访问 state 变量和 context,并且可以为它们赋值。

订阅的两种实现

那么,让我们回头看看熟悉的 class 的例子。嗯,其他你可能需要在 class 里使用生命周期方法实现的就是订阅功能。你可能想要去订阅一些浏览器 API,它会提供给你一些值,例如窗口的大小。你需要组件随着这个 state 值的改变更新。那么我们在 class 里实现这个功能的方法是,比如说我们想要,嗯,我们想要监测窗口的宽度。

我将 width 放进 state 里。使用 window.innerWidth 浏览器 API 来初始化。然后我想要渲染它。嗯,让我们复制并且粘贴这段代码。这里改为 width。我将在这个地方渲染它。这里改为 this.state.width。这就是窗口的宽度了,而不是 Mary Poppins 的宽度。(大笑)我将添加一个,嗯,我将要添加一个事件监听,所以我们需要真真切切地监听这个 width 的改变。所以设置 window.addEventListener。我将监听 resize 事件, handleResize。然后我需要声明这个事件。在这里我们更新这个 width 状态,设置为 window.innerWidth。然后我们需要去绑定它。

然后,嗯,然后我也需要取消订阅。所以我不想因为保留这些订阅造成内存泄漏。我想要取消这个事件的订阅。我们在一个 class 里处理的方式是创建另一个叫做 componentWillUnmount 的生命周期方法。然后我将这段逻辑代码复制并且粘贴到这里,将这里改为 removeEventListener。我们设置了一个事件监听,并且我们移除了这个事件监听。我们可以通过拖动窗口来验证。你看到这个 width 正在变化。运行正常。

import React from 'react';
import Row from './Row';
import { ThemeContext, LocaleContext } from './context';

export default class Greeting extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        name: 'Mary',
        surname: 'Poppins',
+       width: window.innerWidth,
      }
      this.handleNameChange = this.handleNameChange.bind(this);
      this.handleSurnameChange = this.handleSurnameChange.bind(this);
+     this.handleResize = this.handleResize.bind(this);
  }

    componentDidMount() {
        document.title = this.state.name + ' ' + this.state.surname;
+       window.addEventListener('resize', handleResize);
    }

    componentDidUpdate() {
      document.title = this.state.name + ' ' + this.state.surname;
    }

+   componentWillUnmount() {
+     window.removeEventListener('resize', handleResize);
+   }

+   handleResize() {
+     this.setState({
+       width: window.innerWidth
+     });
+   }

    handleNameChange(e) {
      this.setState({
        name: e.target.value
      })
    }

    handleSurnameChange(e) {
      this.setState({
        surname: e.target.value
      })
    }

  render() {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <section className={theme}>
            <Row label="Name">
              <input
                value={this.state.name}
                onChange={this.handleNameChange}
              />
            </Row>
            <Row label="Surname">
              <input
                value={this.state.surname}
                onChange={this.handleSurnameChange}
              />
            </Row>
            <LocaleContext.Consumer>
              {locale => (
                <Row label="Language">
                  {locale}
                </Row>
              )}
            </LocaleContext.Consumer>
+           <Row label="Width">
+              {this.state.width}
+           </Row>
          </section>
        )}
      </ThemeContext.Consumer>

    );
  }

}

那么让我们看看如何可以,我们如何用 hook 实现这个功能。从概念上来说,监听窗口宽度与设置文档标题无关。这就是为什么我们没有把它放入这个 useEffect 里的原因。它们在概念上是完全独立的副作用,就像我们可以使用多次的 useState 用来声明多个 state 变量,我们可以使用多次 useEffect 来处理不同的副作用。

这里我想要订阅 window.addEventListener ,resize,handleResize。然后我需要保存当前 width 的状态。所以,我将声明另一组 state 变量。所以这里声明 width 和 setWidth。我们通过 useState 设置他们的初始值为 window.innerWidth。现在我把 handleResize 函数声明在这里。因为它没有在其他地方被调用。然后用 setWidth 来设置当前的 width。嗯,我需要去渲染它。所以我复制并粘贴这个 Row。这里改为 width。

最后我需要在这个 effect 之后去清除它。所以我需要指定如何清除。从概念上说,清除也是这个 effect 的一部分。所以这个 effect 有一个清除的地方。这个顺序,你可以指定如何清除订阅的方法是,effect 可以选择返回一个函数。如果它返回一个函数,那么 React 将在 effect 之后调用这个函数进行清除操作。所以这就是我们取消订阅的地方。好的,让我们验证一下它能否正常运行吧。耶!(掌声)

import React, { useState, useContext, useEffect } from 'react';
import Row from './Row';
import { ThemeContext, LocaleContext } from './context';

export default function Greeting(props) {
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);

  useEffect(() => {
    document.title = name + ' ' + surname;
  })

+ const [width, setWidth] = useState(window.innerWidth);
+ useEffect(() => {
+   const handleResize = () => setWidth(window.innerWidth);
+   window.addEventListener('resize', handleResize);
+   return () => {
+     window.removeEventListener('resize', handleResize);
+   };
+ })

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <section className={theme}>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
      <Row label="Surname">
        <input
          value={surname}
          onChange={handleSurnameChange}
        />
      </Row>
      <Row label="Language">
        {locale}
      </Row>
+     <Row label="Width">
+       {width}
+     </Row>
    </section>
  );
}

那么让我们比较这两个方法。在左边,我们使用了一个熟悉的 class 组件,嗯,在这没有令人惊喜的东西。我们有一些副作用,一些相关的逻辑是分开的:我们可以看到文档的标题在这里被设置,但是它在这也被设置了。并且我们在这订阅 effect,抱歉,在这订阅这个事件,但是我们在这里取消订阅。所以这些事情需要相互保持同步。而且这个方法包含了两个不相关的方法,在这不相关的两行。因此,我在未来有点难以单独测试它们。但是它看起来非常熟悉,这点也不错。

那么这段代码看起来可能会就不那么熟悉了。但让我们来看一看这里发生了什么。嗯,在 hook 中,我们分离代码不是基于生命周期函数的名字,而是基于这段代码要做什么。所以我们可以看到这个有一个 effect,我们用来更新文档的标题这是一件这个组件能做的事。这里有另一个 effect,它订阅了 window 的 resize 事件,并且当 window 的大小发生改变时,state 随之更新。然后,嗯,这个 effect 有一个清除阶段,它的作用是移除这个 effect 时,React 取消事件监听从而避免内存泄漏。如果你一直仔细观察,你可能注意到由于 effect 在每次渲染之后运行,我们会重新订阅。有一个方法可以优化这个问题。默认是一致的,这很重要。如果你,例如在这使用一些 prop,我需要去重新订阅一个不同的 id ,该 id 来自 props 或类似的地方。但是这儿有一个方法去优化它,并且可以选择不用这个行为。Ryan 在下一个演讲中将会提到如何去实现它。

Custom Hook

好的,我在这里还想要演示另外一件事。现在组件已经非常庞大了,这也没有太大的问题。我们考虑到在 function 组件中你们有可能做更多的事情,组件会变得更大,但也完全没有问题。嗯,但是你有可能想要复用其他组件里面到一些逻辑,或者是想要将公用的逻辑抽取出来,或者是想要分别测试。有趣的是, hook 调用实际上就是函数调用。而且组件就是函数。那么我们平时是如何在两个函数之间共享逻辑呢。我们会将公用逻辑提取到另外一个函数里面。这也是我将要做的事情。我把这段代码复制粘贴到这里。我要新建一个叫做 useWindowWidth 的函数。然后把它粘贴到这里。我们需要组件里面的宽度,以便能够将其 渲染。因为我需要在这个函数里面返回当前宽度。然后我们回到上面的代码,这样修改: const width = useWindowWidth。 (掌声和欢呼声)

import React, { useState, useContext, useEffect } from 'react';
import Row from './Row';
import { ThemeContext, LocaleContext } from './context';

export default function Greeting(props) {
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);
+ const width = useWindowWidth();

  useEffect(() => {
    document.title = name + ' ' + surname;
  })

- const [width, setWidth] = useState(window.innerWidth);
- useEffect(() => {
-   const handleResize = () => setWidth(window.innerWidth);
-   window.addEventListener('resize', handleResize);
-   return () => {
-     window.removeEventListener('resize', handleResize);
-   };
- })

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <section className={theme}>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
      <Row label="Surname">
        <input
          value={surname}
          onChange={handleSurnameChange}
        />
      </Row>
      <Row label="Language">
        {locale}
      </Row>
      <Row label="Width">
        {width}
      </Row>
    </section>
  );
}

+function useWindowWidth() {
+  const [width, setWidth] = useState(window.innerWidth);
+  useEffect(() => {
+    const handleResize = () => setWidth(window.innerWidth);
+    window.addEventListener('resize', handleResize);
+    return () => {
+     window.removeEventListener('resize', handleResize);
+    };
+  })
+  return width;
+}

那么这个函数是什么呢?我们并没有做什么特别的事情,我们仅仅是将逻辑提取到了一个函数里面。呃,但是这里有一个约定。我们把这种函数叫做 custom hook。按照约定,custom hook 的名字需要以 use 开头。这么约定主要有两个原因。

我们会读你的函数名或修改函数名称。但是这是一个重要的约定,因为首先以 use 开头来命名 custom hook,可以让我们自动检测是否违反了我之前说过的第一条规则:不能在条件判断里面使用 hook。因此如果我们无法得知哪些函数是 hook,那么我们就无法做到自动检测。

另一个原因是,如果你查看组件的代码,你可能会想要知道某个函数里面是否含有 state。因此这样的约定很重要,好的,以 use 开头的函数表示这个函数是有状态的。

在这里 width 变量给了我们当前的宽度并且订阅了其更新。如果我们想,我们可以更进一步。在这个例子里面也许并不必要,但是我想要给你一个思路。嗯,我们也许设置文档的标题的功能会更加复杂,你希望能够把它的逻辑提取出来并单独测试。那么我把这段代码复制过来粘贴到这里。我可以写一个新的 custom hook。我把这个 hook 命名为useDocumentTitle。由于name 和 surname 在上下文作用域里没有意义。我希望调用标题,标题就是一个参数,由于 custom hook 就是 JavaScript 函数,因此他们可以传递参数,返回值或者不返回。这里我把 title 设置为参数。然后在组件里面,使用 useDocumentTitle,参数为 name 加上 surname


import React, { useState, useContext, useEffect } from 'react';
import Row from './Row';
import { ThemeContext, LocaleContext } from './context';

export default function Greeting(props) {
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);
  const width = useWindowWidth();
+ useDocumentTitle(name + ' ' + surname);

- useEffect(() => {
-   document.title = name + ' ' + surname;
- })

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <section className={theme}>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
      <Row label="Surname">
        <input
          value={surname}
          onChange={handleSurnameChange}
        />
      </Row>
      <Row label="Language">
        {locale}
      </Row>
      <Row label="Width">
        {width}
      </Row>
    </section>
  );
}

+function useDocumentTitle(title) {
+  useEffect(() => {
+    document.title = title;
+  })
+}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  })
  return width;
}

事实上,我可以更进一步。在这个例子中是完全没有必要的,但是同样的道理,也许我们的输入框会更加的复杂,也许我们需要追踪输入框的聚焦或失焦事件,或者输入框是否被校验过、提交过等等。也许我们还有更多的逻辑想要从组件中抽离。嗯,而且想要减少重复代码。这里已经有了重复的代码,这两段事件处理函数几乎一样。

那么我们如果,呃,我把他们删除一段,然后提取另一段。我要创建另一个新 hook,把它命名为 useFormInput。这个 hook 是我的 change 处理函数。现在我把这个声明复制粘贴到这里。这里定义了输入框的状态。这里不再是 namesetName。我把这里改为更通用的 valuesetValue。我把初始值作为参数。这里改为 handleChange,这里改为 setValue。那么我们该如何做在我们组件里面使用输入框呢?我们需要获取当前的 value 和 change 处理函数。这是我们需要赋给输入框的。所以我们就在 hook 里面返回他们。嗯,返回 value 和 onChange handleChange 函数。我们回到组件里面,这里改为 name 等于 useFormInput,参数 Mary。这里 name 变为了一个对象,包括 valueonChange 函数。这里 surname 等于 useFormInput,初始化参数 Poppins。这里改为 name.valuesurname.value。因为这两个值才是我们需要的字符串。接下来我把这里删除,然后将其改为 spread 属性。有人在笑。[笑声] 好的。我们来验证一下,是的,运行正常。

import React, { useState, useContext, useEffect } from 'react';
import Row from './Row';
import { ThemeContext, LocaleContext } from './context';

export default function Greeting(props) {
- const [name, setName] = useState('Mary');
- const [surname, setSurname] = useState('Poppins');
+ const name = useFormInput('Mary');
+ const surname = useFormInput('Poppins');
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);
  const width = useWindowWidth();
- useDocumentTitle(name+ ' ' + surname);
+ useDocumentTitle(name.value + ' ' + surname.value);

- function handleNameChange(e) {
-   setName(e.target.value);
- }

- function handleSurameChange(e) {
-   setSurname(e.target.value);
- }

  return (
    <section className={theme}>
      <Row label="Name">
-       <input
-         value={name}
-         onChange={handleNameChange}
-       />
+       <input {...name} />
      </Row>
      <Row label="Surname">
-       <input
-         value={surname}
-         onChange={handleSurnameChange}
-       />
+       <input {...surname} />
      </Row>
      <Row label="Language">
        {locale}
      </Row>
      <Row label="Width">
        {width}
      </Row>
    </section>
  );
}

+function useFormInput(initialValue) {
+  const [value, setValue] = useState(initialValue);
+  function handleChange(e) {
+    setValue(e.target.value);
+  }
+  return {
+    value,
+    onChange: handleChange
+  };
+}

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  })
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  })
  return width;
}

每次我们调用 hook,其状态都是完全独立的。这是因为我们依赖调用 hook 的顺序,而不是通过名称或其他方式来实现的。所以你可以多次调用相同的 hook。每次调用都会获取其自身的本地状态。

我们最后一次来比较这两种方式。嗯,在左侧我们熟悉的class 组件例子里,在一个对象里面有一些 state,绑定了一些方法,有一些逻辑分散到不同的声明周期方法里面,这些逻辑是一串事件处理函数。嗯,我们用了来自 context 的内容来渲染内容。嗯,这种情况我们相当熟悉了。

2018-11-21 1 32 40

在右侧窗格里面,和我们常见的 React 组件不同。但是它是有意义的。即使你并不知道这些函数是如何实现的。你可以看到,这个函数就是用来组织输入框的,这个函数用了 context 来获取主题和本地语言,这个函数使用了窗口宽度和文档标题,然后渲染了一连串的内容。如果我们想了解更多,我们可以滚动窗口到下面,可以看到,这就是输入框如何运行的代码,这里是如何设置文档标题的代码,而这里是如何设置并订阅窗口宽度的代码。或许这里是一个 npm 包,实际上你没有必要了解它是如何实现的。我们可以将它在组件里面调用,或者在组件之间复制粘贴它们。 Hook 提供了 custom hook,为用户提供了灵活的创建自己的抽象函数的功能,custom hook 不会让你的 React 组建树变得庞大,而且可以避免“包装地狱”。 (掌声)

而且重要的是,这两个例子并不是独立的两个应用。实际上,这两个例子是在同一个应用里面。我把这个窗口打开的目的就是想要展示 class 可以和 hook 并肩工作。而 hook 代表这我们对 React 未来的期许,嗯,但是我们并不想做出不向下兼容的改变。我们还需要保证 class 可以正常运行。

2018-11-21 1 33 37

Hook 提案

我们回到幻灯片上来。好的,这张幻灯片就是你们可以发 tweet 的片子。 (笑声)

2018-11-18 10 24 17

今天我们向你们展示了 Hook 提案。Hook 让我们可以在不使用 class 的情况下使用 React 的众多特性。而且我们没有弃用 class,但是我们给你们提供了一个不去写 class 的新选择。我们打算尽快完成使用 hook 来替代 class 的全部用例。目前还有一部分缺失,但是我们正在处理这部分内容。而且 hook 能够让大家复用有状态的逻辑,并将其从组件中提取出来,分别测试,在不同组件之间复用,并且可以避免引入“包装地狱”。

重要的是,hook 不是一个破坏性的改动,完全向后兼容,是严格添加性的。你可以从这个 url 查找到我们关于 hook 的文档。嗯,我们希望听到你们的反馈,React 社区希望了解到你们对 hook 的想法,嗯,无论你们喜欢与否。而且我们发现如果不让大家实际使用 hook,就会很难收到反意见。所以我们将 hook 构建发布到了 React 16.7 alpha 版本上。这个不是一个主要版本,是一个小版本。但是在这个 alpha 版本,你可以尝试使用 hook。而且我们在 Facebook 的生产环境已经测试了一个月,因此我们认为不会有大的缺陷。但是 hook 的 API 可以根据你们的反馈意见进行调整。而且我不建议你们把整个应用使用 hook 来重写。因为首先,hook 目前还在提案阶段。第二个原因,我个人认为,使用 hook 的思维方式需要一个思想上的改变,也许刚开始你们尝试把 class 组件转为 hook 写法会比较困惑。但是我推荐大家尝试在新的代码里使用 hook,并且让我们知道你们是怎么想的。那么,谢谢大家。 (掌声)

2018-11-18 10 43 11

在我们看来,hook 代表着 React 的未来。但我认为这也代表着我们推进 React 发展的方式。那就是我们不进行大的重写。嗯,我们希望我们更喜欢的新模式可以和旧模式并存,这样我们就可以进行渐进迁移并接受这些新模式,就像你们逐渐接受 React 本身一样。

Hook 一直就在那里

这也差不多是我演讲的结尾了。但是最后,我想讲讲一些我个人的观点。我从四年前学习 React。我遇到的第一个问题就是为什么要使用 JSX。

嗯,我第二个问题是 React 的 Logo 到底有什么含义。React 项目没有起名叫“原子”(Atom),它并不是一个物理引擎。嗯,有一个解释是,React 是基于反应的(reactions),原子也参与了化学反应(chemical reactions),因此 React 的 Logo 用了原子的形象。

2018-11-18 10 12 48

但是 React 没有官方承认过这种说法。嗯,我发现了一个对我来说更有意义的解释。我是这样思考的,我们知道物质是由原子组成的。我们学过物质的外观和行为是由原和其内部的属性决定的。而 React 在我看来是类似的,你可以使用 React 来构建用户界面,将其拆分为叫做组件的独立单元。用户界面的外观和行为是由这些组件及其内部的属性决定的。

具有讽刺意味的是,“原子”(Atom)一词,字面上的意思是不可分割的。当科学家们首次发现原子的时候,他们认为原子是我们发现的最小的物质。但是之后他们就发现了电子,电子是原子内部更小的微粒。后来证明实际上电子更能描述原子运行的原理。

我对 hook 也有类似的感觉。我感觉 hook 不是一个新特性。我感觉 hook 提供了使用我们已知的 React 特性的能力,如 state 、context 和生命周期。而且我感觉 hook 就像 React 的一个更直观的表现。Hook 在组件内部真正解释了组件是如何工作的。我感觉 hook 一直在我们的视线里面隐藏了四年。事实上,如果看看 React 的 Logo,可以看到电子的轨道,而 hook 好像一直就在那里。谢谢。(掌声)


传送门

如果发现译文和字幕存在错误或其他需要改进的地方,欢迎到本项目的 GitHub 仓库 对英文字幕或译文进行修改并 PR,谢谢大家。当然后本视频还有后面 Ryan 给我带来的第三段题目为 90% Cleaner React with Hooks 的演讲,欢迎有兴趣的小伙伴一起参与英文字幕校对和翻译工作。