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

清秋發表於2019-03-04

因為這個演講 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。好的,非常老舊的樣式。(笑聲)

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

我也想展示當前的語言,因此我將要使用 LocaleContext Consumer。我們再來渲染另一行,把這行程式碼複製貼上到這裡,改成 language。 Language。在這裡渲染。好的,我們能夠看到 context 執行了。

[譯] React 的今天和明天(圖文版) —— 第二部分
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 放到這裡。 (掌聲)

[譯] React 的今天和明天(圖文版) —— 第二部分
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 的演講,歡迎有興趣的小夥伴一起參與英文字幕校對和翻譯工作。

相關文章