一文完全搞懂react hooks的原理和實現

本文來自《一文完全搞懂react hooks的原理和實現》,若是以爲不錯,歡迎給Github倉庫一個star。javascript

摘要

當使用 Hook 特性編寫組件的時候時候,總能感受到它的簡潔和方便。固然,「天下沒有免費的午飯」,它犧牲了可讀性而且存在內存泄漏風險(最後有提到)。但這並不妨礙探索它的魔力。html

在正式開始前,但願您讀過 Hook 的文檔或者在項目使用過它。但若是隻對函數編程感興趣,也可能有所收穫。java

爲了讓行文更流暢,我打算先拋出幾個問題,這些問題會在源碼實現的過程當中,逐步解決:react

  • 🤔️ useState 的實現原理
  • 🤔️ 爲何不能在循環、判斷內部使用 Hook
  • 🤔️ useEffect 的實現原理
  • 🤔️ useEffect 的應用場景
  • 🤔️ Class vs Hooks

⚠️ 代碼均由TypeScript來實現,文中所有 demos 均在 gist.github.com/dongyuanxin…git

useState 的實現原理

當調用 useState 的時候,會返回形如 (變量, 函數) 的一個元祖。而且 state 的初始值就是外部調用 useState 的時候,傳入的參數。github

理清楚了傳參和返回值,再來看下 useState 還作了些什麼。正以下面代碼所示,當點擊按鈕的時候,執行setNum,狀態 num 被更新,而且 UI 視圖更新。顯然,useState 返回的用於更改狀態的函數,自動調用了render方法來觸發視圖更新。編程

function App() {
  const [num, setNum] = useState < number > 0;

  return (
    <div> <div>num: {num}</div> <button onClick={() => setNum(num + 1)}>加 1</button> </div>
  );
}
複製代碼

有了上面的探索,藉助閉包,封裝一個 setState 以下:後端

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
}

let state: any;

function useState<T>(initialState: T): [T, (newState: T) => void] {
  state = state || initialState;

  function setState(newState: T) {
    state = newState;
    render();
  }

  return [state, setState];
}

render(); // 首次渲染
複製代碼

這是一個簡易能用的useState雛形了。它也解決了文章開始提到的「🤔️ useState 的實現原理」這個問題。但若是在函數內聲明多個 state,在當前代碼中,只有第一個 state 是生效的(請看state = state || initialState;))。數組

爲何不能在循環、判斷內部使用 Hook

先不要考慮題目說起的問題。思路仍是回到如何讓 useState 支持多個 state。《React hooks: not magic, just arrays》中說起,React Hook 看起來很是 Magic 的實現,本質上仍是經過 Array 來實現的。閉包

前面 useState 的簡單實現裏,初始的狀態是保存在一個全局變量中的。以此類推,多個狀態,應該是保存在一個專門的全局容器中。這個容器,就是一個樸實無華的 Array 對象。具體過程以下:

  • 第一次渲染時候,根據 useState 順序,逐個聲明 state 而且將其放入全局 Array 中。每次聲明 state,都要將 cursor 增長 1。
  • 更新 state,觸發再次渲染的時候。cursor 被重置爲 0。按照 useState 的聲明順序,依次拿出最新的 state 的值,視圖更新。

請看下面這張圖,每次使用 useState,都會向 STATE 容器中添加新的狀態。

實現的代碼以下:

import React from "react";
import ReactDOM from "react-dom";

const states: any[] = [];
let cursor: number = 0;

function useState<T>(initialState: T): [T, (newState: T) => void] {
  const currenCursor = cursor;
  states[currenCursor] = states[currenCursor] || initialState; // 檢查是否渲染過

  function setState(newState: T) {
    states[currenCursor] = newState;
    render();
  }

  ++cursor; // update: cursor
  return [states[currenCursor], setState];
}

function App() {
  const [num, setNum] = useState < number > 0;
  const [num2, setNum2] = useState < number > 1;

  return (
    <div>
      <div>num: {num}</div>
      <div>
        <button onClick={() => setNum(num + 1)}>加 1</button>
        <button onClick={() => setNum(num - 1)}>減 1</button>
      </div>
      <hr />
      <div>num2: {num2}</div>
      <div>
        <button onClick={() => setNum2(num2 * 2)}>擴大一倍</button>
        <button onClick={() => setNum2(num2 / 2)}>縮小一倍</button>
      </div>
    </div>
  );
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  cursor = 0; // 重置cursor
}

render(); // 首次渲染
複製代碼

此時,若是想在循環、判斷等不在函數組件頂部的地方使用 Hook,以下所示:

let tag = true;

function App() {
  const [num, setNum] = useState < number > 0;

  // 只有初次渲染,才執行
  if (tag) {
    const [unusedNum] = useState < number > 1;
    tag = false;
  }

  const [num2, setNum2] = useState < number > 2;

  return (
    <div> <div>num: {num}</div> <div> <button onClick={() => setNum(num + 1)}>加 1</button> <button onClick={() => setNum(num - 1)}>減 1</button> </div> <hr /> <div>num2: {num2}</div> <div> <button onClick={() => setNum2(num2 * 2)}>擴大一倍</button> <button onClick={() => setNum2(num2 / 2)}>縮小一倍</button> </div> </div>
  );
}
複製代碼

因爲在條件判斷的邏輯中,重置了tag=false,所以此後的渲染不會再進入條件判斷語句。看起來好像沒有問題?可是,因爲 useState 是基於 Array+Cursor 來實現的,第一次渲染時候,state 和 cursor 的對應關係以下表:

變量名 cursor
num 0
unusedNum 1
num2 2

當點擊事件觸發再次渲染,並不會進入條件判斷中的 useState。因此,cursor=2 的時候對應的變量是 num2。而其實 num2 對應的 cursor 應該是 3。就會致使setNum2並不起做用。

到此,解決了文章開頭提出的「🤔️ 爲何不能在循環、判斷內部使用 Hook」。在使用 Hook 的時候,請在函數組件頂部使用!

useEffect 的實現原理

在探索 useEffect 原理的時候,一直被一個問題困擾:useEffect 做用和用途是什麼?固然,用於函數的反作用這句話誰都會講。舉個例子吧:

function App() {
  const [num, setNum] = useState(0);

  useEffect(() => {
    // 模擬異步請求後端數據
    setTimeout(() => {
      setNum(num + 1);
    }, 1000);
  }, []);

  return <div>{!num ? "請求後端數據..." : `後端數據是 ${num}`}</div>;
}
複製代碼

這段代碼,雖然這樣組織可讀性更高,畢竟能夠將這個請求理解爲函數的反作用。但這並非必要的。徹底能夠不使用useEffect,直接使用setTimeout,而且它的回調函數中更新函數組件的 state。

在閱讀A Complete Guide to useEffect構建你本身的 Hooks以後,我才理解 useEffect 的存在的必要性和意義。

在 useEffect 的第二個參數中,咱們能夠指定一個數組,若是下次渲染時,數組中的元素沒變,那麼就不會觸發這個反作用(能夠類比 Class 類的關於 nextprops 和 prevProps 的生命週期)。好處顯然易見,相比於直接裸寫在函數組件頂層,useEffect 能根據須要,避免多餘的 render

下面是一個不包括銷燬反作用功能的 useEffect 的 TypeScript 實現:

// 仍是利用 Array + Cursor的思路
const allDeps: any[][] = [];
let effectCursor: number = 0;

function useEffect(callback: () => void, deps: any[]) {
  if (!allDeps[effectCursor]) {
    // 初次渲染:賦值 + 調用回調函數
    allDeps[effectCursor] = deps;
    ++effectCursor;
    callback();
    return;
  }

  const currenEffectCursor = effectCursor;
  const rawDeps = allDeps[currenEffectCursor];
  // 檢測依賴項是否發生變化,發生變化須要從新render
  const isChanged = rawDeps.some(
    (dep: any, index: number) => dep !== deps[index]
  );
  if (isChanged) {
    callback();
  }
  ++effectCursor;
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  effectCursor = 0; // 注意將 effectCursor 重置爲0
}
複製代碼

對於 useEffect 的實現,配合下面案例的使用會更容易理解。固然,你也能夠在這個 useEffect 中發起異步請求,並在接受數據後,調用 state 的更新函數,不會發生爆棧的狀況。

function App() {
  const [num, setNum] = useState < number > 0;
  const [num2] = useState < number > 1;

  // 屢次觸發
  // 每次點擊按鈕,都會觸發 setNum 函數
  // 反作用檢測到 num 變化,會自動調用回調函數
  useEffect(() => {
    console.log("num update: ", num);
  }, [num]);

  // 僅第一次觸發
  // 只會在compoentDidMount時,觸發一次
  // 反作用函數不會屢次執行
  useEffect(() => {
    console.log("num2 update: ", num2);
  }, [num2]);

  return (
    <div> <div>num: {num}</div> <div> <button onClick={() => setNum(num + 1)}>加 1</button> <button onClick={() => setNum(num - 1)}>減 1</button> </div> </div>
  );
}
複製代碼

⚠️ useEffect 第一個回調函數能夠返回一個用於銷燬反作用的函數,至關於 Class 組件的 unmount 生命週期。這裏爲了方便說明,沒有進行實現。

在這一小節中,嘗試解答了 「🤔️ useEffect 的實現原理」和 「🤔️ useEffect 的應用場景」這兩個問題。

Class VS Hooks

雖然 Hooks 看起來更酷炫,更簡潔。可是在實際開發中我更傾向於使用 Class 來聲明組件。兩種方法的對好比下:

Class Hooks
代碼邏輯清晰(構造函數、componentDidMount 等) 須要配合變量名和註釋
不容易內存泄漏 容易發生內存泄漏

總的來講,Hooks 對代碼編寫的要求較高,在沒有有效機制保證代碼可讀性、規避風險的狀況下,Class 依然是個人首選。關於內存泄漏,下面是一個例子(目前還沒找到方法規避這種向全局傳遞狀態更新函數的作法):

import React, { useState } from "react";
import ReactDOM from "react-dom";

let func: any;
setInterval(() => {
  typeof func === "function" && func(Date.now());
  console.log("interval");
}, 1000);

function App() {
  const [num, setNum] = useState < number > 0;
  if (typeof func !== "function") {
    func = setNum;
  }
  return <div>{num}</div>;
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root")); } render(); 複製代碼

參考連接

文章中多有看法不到當之處,歡迎討論和指正。

相關文章
相關標籤/搜索