快速瞭解 React Hooks 原理

前端小智發表於2019-08-19

作者:Dave Ceddia

譯者:前端小智

來源:daveceddia


為了保證的可讀性,本文采用意譯而非直譯。

我們大部分 React 類元件可以儲存狀態,而函式元件不能? 並且類元件具有生命週期,而函式元件卻不能?

React 早期版本,類元件可以通過繼承PureComponent來優化一些不必要的渲染,相對於函式元件,React 官網沒有提供對應的方法來快取函式元件以減少一些不必要的渲染,直接 16.6 出來的 React.memo函式。

React 16.8 新出來的Hook可以讓React 函式元件具有狀態,並提供類似 componentDidMountcomponentDidUpdate等生命週期方法。

類被會替代嗎?

Hooks不會替換類,它們只是一個你可以使用的新工具。React 團隊表示他們沒有計劃在React中棄用類,所以如果你想繼續使用它們,可以繼續用。

我能體會那種總有新東西要學的感覺有多痛苦,不會就感覺我們們總是落後一樣。Hooks 可以當作一個很好的新特性來使用。當然沒有必要用 Hook 來重構原來的程式碼, React團隊也建議不要這樣做。

Go Go

來看看Hooks的例子,我們們先從最熟悉的開始:函式元件。

以下 OneTimeButton 是函式元件,所做的事情就是當我們點選的時候呼叫 sayHi 方法。

import React from 'react';
import { render } from 'react-dom';

function OneTimeButton(props) {
  return (
    <button onClick={props.onClick}>
        點我點我
    </button>
  )
}

function sayHi() {
  console.log('yo')
}

render(
  <OneTimeButton onClick={sayHi}/>,
  document.querySelector('#root')
)
複製程式碼

我們想讓這個元件做的是,跟蹤它是否被點選,如果被點選了,禁用按鈕,就像一次性開關一樣。

但它需要一個state,因為是一個函式,它不可能有狀態(React 16.8之前),所以需要重構成類。

函式元件轉換為類元件的過程中大概有5個階段:

  • 否認:也許它不需要是一個類,我們可以把 state 放到其它地方。

  • 實現: 廢話,必須把它變成一個class,不是嗎?

  • 接受:好吧,我會改的。

  • 努力加班重寫:首先 寫 class Thing extends React.Component,然後 實現 render等等 。

  • 最後:新增state。


class OneTimeButton extends React.Component {
  state = {
    clicked: false
  }

  handleClick = () => {
    this.props.onClick();

    // Ok, no more clicking.
    this.setState({ clicked: true });
  }

  render() {
    return (
      <button
        onClick={this.handleClick}
        disabled={this.state.clicked}
      >
        You Can Only Click Me Once
      </button>
    );
  }
}
複製程式碼

這是相當多的程式碼,元件的結構也發生了很大的變化, 我們需要多個小的功能,就需要改寫很多。

使用 Hook 輕鬆新增 State

接下來,使用新的 useState hook向普通函式元件新增狀態:

import React, { useState } from 'react'

function OneTimeButton(props) {
  const [clicked, setClicked] = useState(false)
  
  function doClick() {
    props.onClick();
    setClicked(true)
  }

  return (
    <button
      onClick={clicked ? undefined : doClick}
      disabled={clicked}
    >
      點我點我
    </button>
  )
}
複製程式碼

這段程式碼是如何工作的

這段程式碼的大部分看起來像我們一分鐘前寫的普通函式元件,除了useState

useState是一個hook。 它的名字以**“use”**開頭(這是Hooks的規則之一 - 它們的名字必須以“use”開頭)。

useState hook 的引數是 state 的初始值,返回一個包含兩個元素的陣列:當前state和一個用於更改state 的函式。

類元件有一個大的state物件,一個函式this.setState一次改變整個state物件。

函式元件根本沒有狀態,但useState hook允許我們在需要時新增很小的狀態塊。 因此,如果只需要一個布林值,我們就可以建立一些狀態來儲存它。

由於Hook以某種特殊方式建立這些狀態,並且在函式元件內也沒有像setState函式來更改狀態,因此 Hook 需要一個函式來更新每個狀態。 所以 useState 返回是一對對應關係:一個值,一個更新該值函式。 當然,值可以是任何東西 - 任何JS型別 - 數字,布林值,物件,陣列等。

現在,你應該有很多疑問,如:

  • 當元件重新渲染時,每次都不會重新建立新的狀態嗎? React如何知道舊狀態是什麼?

  • 為什麼hook 名稱必須以**“use”**開頭? 這看起來很可疑。

  • 如果這是一個命名規則,那是否意味著我可以自定義 Hook。

  • 如何儲存更復雜的狀態,很多場景不單單隻有一個狀態值這麼簡單。

Hooks 的魔力

將有狀態資訊儲存在看似無狀態的函式元件中,這是一個奇怪的悖論。這是第一個關於鉤子的問題,我們們必須弄清楚它們是如何工作的。

原作者得的第一個猜測是某種編譯器的在背後操眾。搜尋程式碼useWhatever並以某種方式用有狀態邏輯替換它。

然後再聽說了呼叫順序規則(它們每次必須以相同的順序呼叫),這讓我更加困惑。這就是它的工作原理。

React第一次渲染函式元件時,它同時會建立一個物件與之共存,該物件是該元件例項的定製物件,而不是全域性物件。只要元件存在於DOM中,這個元件的物件就會一直存在。

使用該物件,React可以跟蹤屬於元件的各種後設資料位。

請記住,React元件甚至函式元件都從未進行過自渲染。它們不直接返回HTML。元件依賴於React在適當的時候呼叫它們,它們返回的物件結構React可以轉換為DOM節點。

React有能力在呼叫每個元件之前做一些設定,這就是它設定這個狀態的時候。

其中做的一件事設定 Hooks 陣列。 它開始是空的, 每次呼叫一個hook時,React 都會向該陣列新增該 hook

為什麼順序很重要

假設我們們有以下這個元件:

function AudioPlayer() {
  const [volume, setVolume] = useState(80);
  const [position, setPosition] = useState(0);
  const [isPlaying, setPlaying] = useState(false);

  .....
}
複製程式碼

因為它呼叫useState 3次,React 會在第一次渲染時將這三個 hook 放入 Hooks 陣列中。

下次渲染時,同樣的3hooks以相同的順序被呼叫,所以React可以檢視它的陣列,並發現已經在位置0有一個useState hook ,所以React不會建立一個新狀態,而是返回現有狀態。

這就是React能夠在多個函式呼叫中建立和維護狀態的方式,即使變數本身每次都超出作用域。

多個useState 呼叫示例

讓我們們更詳細地看看這是如何實現的,第一次渲染:

  1. React 建立元件時,它還沒有呼叫函式。React 建立後設資料物件和Hooks的空陣列。假設這個物件有一個名為nextHook的屬性,它被放到索引為0的位置上,執行的第一個hook將佔用位置0

  2. React 呼叫你的元件(這意味著它知道儲存hooks的後設資料物件)。

  3. 呼叫useState,React建立一個新的狀態,將它放在hooks陣列的第0位,並返回[volume,setVolume]對,並將volume 設定為其初始值80,它還將nextHook索引遞增1。

  4. 再次呼叫useState,React檢視陣列的第1位,看到它是空的,並建立一個新的狀態。 然後它將nextHook索引遞增為2,並返回[position,setPosition]

  5. 第三次呼叫useState。 React看到位置2為空,同樣建立新狀態,將nextHook遞增到3,並返回[isPlaying,setPlaying]

現在,hooks 陣列中有3個hook,渲染完成。 下一次渲染會發生什麼?

  1. React需要重新渲染元件, 由於 React 之前已經看過這個元件,它已經有了後設資料關聯。

  2. ReactnextHook索引重置為0,並呼叫元件。

  3. 呼叫useState,React檢視索引0處的hooks陣列,並發現它已經在該槽中有一個hook。,所以無需重新建立一個,它將nextHook推進到索引1並返回[volume,setVolume],其中volume仍設定為80

  4. 再次呼叫useState。 這次,nextHook1,所以React檢查陣列的索引1。同樣,hook 已經存在,所以它遞增nextHook並返回[position,setPosition]

  5. 第三次呼叫useState,我想你知道現在發生了什麼。

就是這樣了,知道了原理,看起來也就不那麼神奇了, 但它確實依賴於一些規則,所以才有使用 Hooks 規則。

Hooks 的規則

自定義 hooks 函式只需要遵守規則 3 :它們的名稱必須以**“use”**為字首。

例如,我們可以從AudioPlayer元件中將3個狀態提取到自己的自定義鉤子中:

function AudioPlayer() {
  // Extract these 3 pieces of state:
  const [volume, setVolume] = useState(80);
  const [position, setPosition] = useState(0);
  const [isPlaying, setPlaying] = useState(false);

  // < beautiful audio player goes here >
}
複製程式碼

因此,我們們可以建立一個專門處理這些狀態的新函式,並使用一些額外的方法返回一個物件,以便更容易啟動和停止播放,例如:

function usePlayerState(lengthOfClip) {
  const [volume, setVolume] = useState(80);
  const [position, setPosition] = useState(0);
  const [isPlaying, setPlaying] = useState(false);

  const stop = () => {
    setPlaying(false);
    setPosition(0);
  }

  const start = () => {
    setPlaying(true);
  }

  return {
    volume,
    position,
    isPlaying,
    setVolume,
    setPosition,
    start,
    stop
  };
}
複製程式碼

像這樣提取狀態的一個好處是可以將相關的邏輯和行為組合在一起。可以提取一組狀態和相關事件處理程式以及其他更新邏輯,這不僅可以清理元件程式碼,還可以使這些邏輯和行為可重用。

另外,通過在自定義hooks中呼叫自定義hooks,可以將hooks組合在一起。hooks只是函式,當然,函式可以呼叫其他函式。

總結

Hooks 提供了一種新的方式來處理React中的問題,其中的思想是很有意思且新奇的。

React團隊整合了一組很棒的文件和一個常見問題解答,從是否需要重寫所有的類元件到鉤Hooks是否因為在渲染中建立函式而變慢? 以及兩者之間的所有東西,所以一定要看看。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

原文:daveceddia.com/intro-to-ho…

交流(歡迎加入群,群工作日都會發紅包,互動討論技術)

乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。

github.com/qq449245884…

我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的乾貨,在進階的路上,共勉!

關注公眾號,後臺回覆福利,即可看到福利,你懂的。

快速瞭解 React Hooks 原理

相關文章