詳解React Hooks是如何工作的

1. React Hooks VS 純函數

React Hook 說白瞭就是 React V18.6 新增的一些 API,API的本質就是提供某種功能的函數接口。因此,React Hooks 就是一些函數,但是 React Hooks 不是純函數。

什麼是純函數呢?就是此函數在相同的輸入值時,需產生相同的輸出,並且此函數不能影響到外面的數據。
簡單理解就是函數裡面不能用到在外面定義的變量,因為如果用到瞭外面定義的變量,當外面的變量改變時會影響函數內部的計算,函數也會影響到外面的變量。

對於 React Hooks 提供的函數 API,恰恰就不是純函數。
來看一個 useState 的使用語句 const [count, setCount] = useState(0),使用 useState 函數得到的結果並不是全都一樣的,因為如果 useState(0) 每次得到的結果都是一樣的,那 count 值就永遠不會改變瞭,那 count 所在的頁面就永遠不會改變,和我們看到的結果就不一樣瞭。由此可知,React Hooks 都不是純函數,也就是說 Hooks 用到瞭函數外的變量。

那麼是什麼特性讓 React Hooks 一定不能是純函數呢?實際上是 React 框架和函數組件本身決定的。我們知道,React 頁面渲染的原理就是通過每次 render 得到新的虛擬 DOM ,然後進行 DOM Diff 來渲染頁面。而 React 的函數組件是通過執行整個函數得到一個虛擬 DOM。因此在每次頁面渲染 render 時,在函數組件內部的所有語句都會重新執行一次。如果在函數組件內部使用的 React Hooks 是純函數的話,就不會在每次渲染後得到不同的虛擬 DOM 瞭。

React 規定: 所有 React 組件都必須是純函數,並禁止修改其自身 props 。

因此在 React V16.8 之前 React Hooks 還沒出來的時候,函數組件因為是純函數,隻能返回一個固定的虛擬 DOM,不能包含狀態,也不支持生命周期方法。因此,當時僅僅是支持函數組件,但函數組件相比於類組件限制太多,函數組件無法取代類組件,也沒類組件好用。

React 希望組件是簡單的而不是復雜的,React 認為組件的最佳寫法應該是函數,而不是類。因此 React 就新增瞭 React Hooks,Hook 就是鉤子的意思,是 React 提供給函數組件在需要外部功能和數據狀態時將其 “鉤” 進去,從而完善函數組件,使其能完全代替類組件。

React 的函數組件隻能是純函數,那麼每次事件發生時重新 render 函數組件時得到不同的虛擬 DOM 的事就完全交給瞭 React Hooks,那麼 React Hooks 是如何做到的呢?下面就手動實現一個 useState,useState 的具體細節肯定不是這樣的,但原理和思路是一樣的。

2. 簡單 myUseState

React.useState 的第一次執行是將初始值賦予給一個 _state,之後的每次重新 render 時就是讀取 _state 的值。[state, setState] 中的 setState 做的事就是改變 _state 的值,然後重新渲染頁面。
根據這個原理實現 myUseState 函數如下:

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

let _state

function myUseState(initialValue){
  if(_state === undefined){
    _state = initialValue
  }
  const setState = (newValue)=>{
    _state = newValue
    render()
  }
  return [_state, setState]
}

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

function App(){
  const [n, setN] = myUseState(0)
  return (
    <div>
      n: {n}
      <button onClick={() => setN(n+1)}>+1</button>
    </div>
  )
}

ReactDOM.render(<App/>,document.getElementById('root'));

3. 改進 myUseState

上述實現的 myUseState 存在 bug,當在函數組件內用到兩次 myUseState 時就會出現問題瞭,二者共用一個 _state 會出現混亂。
因此需要將上述實現進行改進,改進的思路就是將 _state 定義為一個數據或者是對象,由於我們在函數使用時隻傳瞭一個數值,無法確定鍵值,因此隻能使用數據。改進如下:

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

let _state = []
let index = 0

function myUseState(initialValue){
  const currentIndex = index
  if(_state[currentIndex] === undefined){
    _state[currentIndex] = initialValue
  }
  const setState = (newValue)=>{
    _state[currentIndex] = newValue
    render()
  }
  index++
  return [_state[currentIndex], setState]
}

function render(){
  index = 0
  ReactDOM.render(<App/>,document.getElementById('root'));
}

function App(){
  const [n, setN] = myUseState(0)
  const [m, setM] = myUseState(0)
  return (
    <div>
      n: {n}
      <button onClick={() => setN(n+1)}>+1</button>
      <br/>
      m: {m}
      <button onClick={() => setM(m+1)}>+1</button>
    </div>
  )
}

ReactDOM.render(<App/>,document.getElementById('root'));

4. 實現原理引發的 Hooks 規則

上述實現的 myUseState 肯定不是 React.useState 的具體實現代碼,但實現原理是一致的。myUseState 函數封裝瞭函數組件內的數據狀態,並對該狀態進行管理,以暴露出相關的操作接口的方式提供給函數組件使用。
這樣一來,函數組件就和其數據狀態分離瞭,函數組件隻負責返回虛擬 DOM 本身就可以瞭,對於數據狀態的管理完全交給其 “鉤” 住的 React.useState Hook 就可以瞭。

從上述的實現思路可以發現,React Hooks 的實現其實是基於 全局變量 和 閉包 原理實現的特殊函數。

但是,正是因為這樣的實現方式,限制瞭 React Hooks 的使用必須是 隻在頂層調用Hook,意思就是說 不要在循環,條件或嵌套函數中調用 Hook,如果在 if 條件句中使用瞭 Hook, 導致組件每次渲染生成時 React.useState 語句的執行次數不對,就會打亂 index 的計數,從而導致數據維護的錯誤。

上述的實現原理依賴於 index 的正確計數,因此 React 依賴於調用 Hooks 的順序,

以上就是詳解React Hooks是如何工作的的詳細內容,更多關於詳解React Hooks的資料請關註WalkonNet其它相關文章!

推薦閱讀: