理解 React Hooks

騰訊雲加社群發表於2019-03-04

歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~

本文由志航發表於雲+社群專欄

TL;DR

一句話總結 React Hooks 就是在 react 函式元件中,也可以使用類元件(classes components)的 state 和 元件生命週期,而不需要在 mixin、 函式元件、HOC元件和 render props 之間來回切換,使得函式元件的功能更加實在,更加方便我們在業務中實現業務邏輯程式碼的分離和元件的複用。

本文將從以下幾個方面介紹 hooks

Hooks 在解決什麼問題 Hooks 的 api 介紹 和如何使用 hooks Hooks 是怎麼實現的

?Hooks 在解決什麼問題

React 一直在解決一個問題,如何實現分離業務邏輯程式碼,實現元件內部相關業務邏輯的複用。

一般情況下,我們都是通過元件和自上而下傳遞的資料流將我們頁面上的大型UI組織成為獨立的小型UI,實現元件的重用。但是我們經常遇到很難侵入一個複雜的元件中實現重用,因為元件的邏輯是有狀態的,無法提取到函式元件當中。這在處理動畫和表單的時候,尤其常見,當我們在元件中連線外部的資料來源,然後希望在元件中執行更多其他的操作的時候,我們就會把元件搞得特別糟糕:

  • 難以重用和共享元件中的與狀態相關的邏輯,造成產生很多巨大的元件
  • 邏輯複雜的元件難以開發與維護,當我們的元件需要處理多個互不相關的 localstate 時,每個生命週期函式中可能會包含著各種互不相關的邏輯在裡面。
  • 複雜的模式,如渲染道具和高階元件。
  • 由於業務變動,函式元件不得不改為類元件。

這時候,Hooks就派上用場了。 Hooks 允許我們將元件內部的邏輯,組織成為一個可複用的隔離模組。

借用 @Sunil Pai 的兩張圖來說明這個問題:

img
image.png

img
image.png

從 React Hooks 中體驗出來的是 React 的哲學在元件內部的實現,以前我們只在元件和元件直接體現 React 的哲學,就是清晰明確的資料流和組成形式。既可以複用元件內的邏輯,也不會出現 HOC 帶來的層層巢狀,更加不會出現 Mixin 的弊端

?Hooks 的 api 介紹 和如何使用 hooks

@dan_abramov 在會議上給我們介紹了 hooks 的三個關鍵的api,分別是 State HooksEffect HooksCustom Hooks(自定義hooks)

?state Hooks (useState)

useState 這個方法可以為我們的函式元件帶來 local state,它接收一個用於初始 state 的值,返回一對變數。 讓函式元件擁有自己的元件。

首先如果我們需要用 classes component 實現一個點選按鈕 +1 元件應該怎麼寫呢?

import React from 'react';

class Example extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
        this.clickBtn = this.clickBtn.bind(this);
    }
    clickBtn = () => {
        this.setState({
            count: this.state.count + 1;
        });
    }
    return (
        <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={this.clickBtn}>
                Click me
            </button>
        </div>
    );
}
複製程式碼

那使用 useState 是怎麼樣的呢? 可以看見非常清晰明瞭。

// 一個簡單的點選計數
import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼

?Effect Hooks (useEffect)

Effect Hooks 用於處理一些帶有副作用的操作,下面通過監聽視窗寬度的變化程式碼為例,說明 effect hooks 的使用fangfa

import { useState } from 'react';

function windowWidth() {
    const [width, setWithd] = useState(window.innerWidth);
    useEffect(() => {
        const handleResize = ()=>{
            setWidth(window.innerWidth);
        }
        window.addEventListener('resize', handleResize);
    });
    return (
        <p> window width is {width}</p>
    )
}
複製程式碼

useEffect 可以傳入第二個操作來避免效能的損耗,如果第二個引數陣列中的成員變數沒有變化則會跳過此次改變。如何傳入一個空陣列 ,那麼該 effect 只會在元件 mount 和 unmount 時期執行。

import { useState } from 'react';

function windowWidth() {
    const [width, setWithd] = useState(window.innerWidth);
    useEffect(() => {
    const handleResize = ()=>{
        setWidth(window.innerWidth);
    }
    window.addEventListener('resize', handleResize);
    }, [width]); // width 沒有變化則不處理
    return (
        <p> window width is {width}</p>
    )
}
複製程式碼

useEffect 中還可以通過讓函式返回一個函式來進行一些取消相容之類的清理操作,比如取消訂閱等

import { useState } from 'react';

function windowWidth() {
  const [width, setWithd] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = ()=>{
        setWidth(window.innerWidth);
    }
    window.addEventListener('resize', handleResize);

    return () => {
        // 取消監聽視窗的寬度變化
        window.removeEventListener('resize');
    }
  });
  return (
      <p> window width is {width}</p>
  )
}
複製程式碼

如上所示,內建的 React Hooks 如 useState 和 useEffect 充當基本構建塊。 我們可以直接在元件中使用它們,或者我們可以將它們組合到自定義Hook中,例如useWindowWidth。使用自定義Hooks感覺就像使用React的內建API一樣。

?Custom Hooks 自定義元件

接著上面的監聽視窗大小的程式碼,我們接著講自定義 hooks, 證明 react hooks 是怎麼使到元件內的邏輯可複用的。

Talk is cheap, show me the code.

// 一個顯示目前視窗大小的元件
function responsiveComponent(){
   // custom hooks
   const width = useWindowWidth(); 
   return (
       <p>當前視窗的寬度是 {width}</p>
   )
}
複製程式碼

上面的程式碼只有幾行,非常清晰明瞭說明了他的作用就是監聽當前視窗的變化,這就是Hooks的目標 - 使元件真正具有宣告性,即使它們包含狀態和副作用。

我們來看看如何實現這個自定義Hook。我們使用React本地狀態來保持當前視窗寬度,並在視窗調整大小時使用副作用來設定該狀態

import { useState, useEffect} from 'react';
// custom hooks to listen window width change
function useWindowWidth(){
    const [width, setWidth] = useState(window.innerWidth);

    useEffect(() => {
        const handleResize = ()=>{
            setWidth(window.innerWidth);
        }
        window.addEventListener('resize', handleResize);
    }, [width]); // width 沒有變化則不處理

    return width;
}
複製程式碼

[線上編輯例子]

⚡ React Hooks 的規則

Hooks 是JavaScript函式,但它們強加了兩個額外的規則:

  • 只能在頂層呼叫Hooks。不要在迴圈,條件或巢狀函式中呼叫Hook。
  • 僅從React功能元件呼叫Hooks。不要從常規JavaScript函式中呼叫Hook。 (還有另一個地方可以呼叫Hooks——你自己的定製Hooks。)

? 其他 Hooks

這裡有一些不常用的內建Hook。例如,useContext允許您訂閱React上下文而不引入巢狀:

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}
複製程式碼

發現一個很有趣的倉庫,react-use, 包含了很多很有趣的自定義hooks

?hooks 是如何工作的

以下內容翻譯自 react-hooks-not-magic-just-arrays.

react hooks 其實只是一個陣列,並不是奇妙的魔法。

如何實現 useState() 方法

讓我們在這裡通過一個例子來演示狀態 hooks 的實現如何工作。

首先讓我們從一個元件開始:

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi");
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}
複製程式碼

hooks API背後的想法是你可以使用一個setter函式作為hook函式中的第二個陣列項返回,而setter將控制由hook管理的狀態。

那麼React與此有什麼關係呢?

讓我們瞭解這在React內部如何工作。 以下內容可在執行上下文中用於呈現特定元件。 這意味著此處儲存的資料位於正在渲染的元件之外。 此狀態不與其他元件共享,但它保留在可以隨後渲染特定元件的範圍內。

1)初始化

建立兩個空陣列:settersstate

將游標設定為 0

img
image.png

初始化:兩個空陣列,Cursor為0

2) 首次渲染

首次執行元件功能。

每次useState()呼叫,當在第一次執行時,將setter函式(繫結到游標位置)推送到setter陣列,然後將某個狀態推送到state陣列。

img
image.png

第一次渲染:作為游標增量寫入陣列的專案。

3) 後續渲染

每個後續渲染都會重置游標,並且只從每個陣列中讀取這些值。

img
image.png

後續渲染:從陣列中讀取的專案為游標增量

4) 事件處理

每個setter都有一個對它的游標位置的引用,因此通過觸發對任何setter的呼叫,它將改變狀態陣列中該位置的狀態值。

img
image.png

Setters“記住”他們的索引並根據它設定記憶體。

通過虛擬碼實現 useState 功能

這是一個演示實現的程式碼示例:

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

function createSetter(cursor) {
  return function setterWithCursor(newVal) {
    state[cursor] = newVal;
  };
}

// useState的虛擬碼實現
export function useState(initVal) {
  if (firstRun) {
    state.push(initVal);
    setters.push(createSetter(cursor));
    firstRun = false;
  }

  const setter = setters[cursor];
  const value = state[cursor];

  cursor++;
  return [value, setter];
}

// 模擬使用useState
function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
  const [lastName, setLastName] = useState("Yardley"); // cursor: 1

  return (
    <div>
      <Button onClick={() => setFirstName("Richard")}>Richard</Button>
      <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    </div>
  );
}

// 模擬Reacts渲染週期
function MyComponent() {
  cursor = 0; //  重置游標的位置
  return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // 首次渲染: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // 後續渲染: ['Rudi', 'Yardley']

// 點選'Fred' 按鈕 

console.log(state); // 點選後: ['Fred', 'Yardley']
複製程式碼

總結

Hooks 還處於早期階段,但是給我們複用元件的邏輯提供了一個很好的思路,大家可以在 react-16.7.0-alpha.0 中體驗。

相關閱讀 【每日課程推薦】機器學習實戰!快速入門線上廣告業務及CTR相應知識

此文已由作者授權騰訊雲+社群釋出,更多原文請點選

搜尋關注公眾號「雲加社群」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社群

相關文章