淺析React Hooks原理


React Hooks原理

React Hooks 簡介

React Hooks 是 React 16.8 以及之后版本的產物,React Hooks 就是一堆鈎子函數,不同的鈎子函數提供了不同的功能,React 通過這些鈎子函數對函數型組件進行增強。Hooks 允許你在不編寫 class 的情況下使用狀態(state)和其他 React 特性。 你還可以構建自己的 Hooks, 跨組件共享可重用的有狀態邏輯。

踐行代數效應

React 核心成員 Sebastian Markbåge (Hooks 的發明者)曾說:我們在 React 中做的就是踐行代數效應。

代數效應是函數式編程的一個概念,它所解決的一部分問題只在純函數式編程中才存在,是為了分離一些副作用。代數效應實際上是一個比較領先的理念(寫這篇博客為止),這種感覺就像你在寫回調去解決異步問題,突然有一天有一個人告訴你有一個叫 async/await 的東西!
有人看到這可能就會說了,我不關心什么破代數效應,不要跟我講大道理,直接給老子上代碼就行了≦(._.)≧
代數效應看起來像是很高深的原理,下面我們用一些虛構的偽代碼來解釋一下什么是代數效應。

假設我們有一個 getName 是根據 id 獲取用戶信息的函數,還有一個 makeFriends 函數用來處理 getName 返回的用戶信息

function getName(id) {
  // ... 獲取用戶數據
}

function makeFriends(id1, id2) {
  const user1 = getName(id1)
  const user2 = getName(id2)
  return `${user1.name}和${user2.name}變成了好朋友`
}

現在我們將 getName 變成異步函數

async function getName(id) {
  // ... 異步操作
}

async function makeFriends(id1, id2) {
  const user1 = await getName(id1)
  const user2 = await getName(id2)
  return `${user1.name}和${user2.name}變成了好朋友`
}

可以看到,由於 getName 函數變成了異步函數,導致 makeFriends 也需要變成異步函數去獲取 getName 返回的數據,getName 變成異步函數破壞了 makeFriends 函數的同步特性。但是其實以正常編程思想,我才不想關注 getName 是怎樣的實現的,我只在乎 getName 返回的數據。

這其實就是 getName 變成異步函數導致的副作用。
我們嘗試虛構一個類似 try...catch 的語法 —— try...handle 和兩個操作符 perform、resume 去分離一下這樣的副作用

function getName(id) {
  const user = perform axios.get(id)
  return user
}

function makeFriends(id1, id2) {
  const user1 = getName(id1)
  const user2 = getName(id2)
  return `${user1.name}和${user2.name}變成了好朋友`
}

try {
  makeFriends('9527', '9528')
} handle (user) {
  if (user) {
    resume with user
  } else {
    resume with {
      name: '毛小星'
    }
  }
}

當 makeFriends 執行到 getName 方法的時候,會執行 perform 后面的語句,perform 跳出當時的執行棧,try...handle 會捕獲 perform 執行的結果,這就是一個效應。這種語法看起來非常像 try...catch,但是一旦 catch 到了 Error,那么當前執行的這條邏輯就完成了,當前的調用棧就銷毀了,那么我們能不能使用一種語法“回到”之前的執行邏輯中去呢?try...handle 讓這種場景成為了可能,handle 捕獲了 perform 的執行結果后,依然會捕獲下一個 perform。
上面這段偽代碼就是代數效應的思想,是不是很簡單,其實代數效應就是將副作用從函數中分離,讓函數變得更加純粹一些,這也是函數式編程的核心思想。

代數效應在 React 中的實踐

React 16.8 中的 Hooks 就是在踐行代數效應,像 useState、useReducer、useRef 等,我們不需要關注函數式組件中的 state 在 Hooks 中是如果保存的,React 會為我們處理。你可以把 useState 看做成是一個 perform State(),這個效應就被 React 處理了,這樣我們就直接使用 useState 返回的 state,編寫我們的業務邏輯即可。
下面我們看看 useState 的基本使用

useState 的基本使用

在 16.8 之前的版本,函數型組件基本只負責展示數據,不負責狀態的保存。useState 的出現就可以讓函數型組件保存狀態了,下面我們來看看 useState 是怎樣的使用的

  1. useState 可以接收一個初始的值
  2. 返回值為數組,數組中存儲狀態值和更改狀態值的方法
  3. useState 方法可以被調用多次,用來保存不同狀態值
  4. 參數可以是一個函數,函數返回什么,初始狀態就是什么,函數只會被調用一次,用在初始值是動態值的情況
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'

const userMap = new Map([
  ['9527', { name: '毛小星' }],
  ['9528', { name: '楊秘書' }],
])

const Friend = () => {
  const [count, setCount] = useState(() => 0)
  const [id1] = useState('9527')
  const [id2] = useState('9528')
  const [content, setContent] = useState('')
  
  const getName = (_id) => {
    return userMap.get(_id)
  }

  const makeFriend = (_id1, _id2) => {
    const user1 = getName(_id1)
    const user2 = getName(_id2)

    const result = `${user1.name} 和 ${user2.name} 變成了好朋友`

    setContent(result)
  }

  useEffect(() => {
    makeFriend(id1, id2)
  }, [id1, id2, count])

  return (
    <div>
      <p>{count && content}</p>
      <Button type="primary" onClick={() => setCount((_count) => _count + 1)}>increment</Button>
    </div>
  )
}

export default Friend

上面這段代碼,是不是和 try...handle 非常像,有點那個味了,是不是?沒錯,useState 和 useEffect 的組合就是 React 踐行代數效應的最好示例。

useState 原理

上面我們在講代數效應的時候,我們說不用關心 useState 里面做了什么,我們只需要使用 useState 返回給我們的 state 即可。誒,但是我就是個好奇寶寶,我就想知道 useState 內部到底是怎樣實現的。

Hooks 架子

下面我們自己實現一個簡易版的 useState,來了解一下 Hooks 內部的基本原理。
我們先來准備一些基礎代碼,也就是我們要寫的 useState 的架子,寫代碼之前,我們先來梳理一下我們需要干什么。

  • 首先我們要模擬 useState ,那么我們肯定要先聲明一個 useState 函數
  • 然后我們需要一個組件來測試我們實現的 hook,我們暫且叫它 App
  • 我們都知道 React 16.8 之后采用了新的 Fiber 架構,Fiber 也就是一個對象用來存儲組件的信息,一般組件都會被存儲在 stateNode 這個屬性上,而 Hooks 的 state 會用鏈表結構被存儲在 Fiber 的 memoizedState 這個屬性上。
  • React 設計最精妙之處就在於它的調度,我們需要一個調度函數 schedule
  • 組件是需要區分生命周期的,首次渲染和更新階段是不一樣的,我們使用一個 isMount 字段去標識
  • 最后我們需要一個 workInprogressHook 來處理最近的一個 hook

下面我們就通過上面的思路來把 hooks 的框架搭出來

let isMount = true // 是否渲染
let workInprogressHook = null // 當前處理 hook

// Fiber對象
const fiber = {
  stateNode: App,
  memoizedState: null, // 用鏈表去存儲 hook 
}

function useState (initialState) {
 // todo 實現 useState
}


// 調度
function schedule() {
  // 初始化 當前處理 hook
  workInprogressHook = fiber.memoizedState
  const app = fiber.stateNode()
  isMount = false
  return app
}

// 測試組件
// 為了簡化流程,我們忽略 DOM 更新
function App () {
  const [num, setNum] = useState(0)
  const [count, setCount] = useState(0)

  return {
    onClick() {
      setNum(num => num + 1)
    },
    updateCount() {
      setCount(count => count + 1)
    }
  }
}

// 將調度掛載到 window 對象上,方便測試點擊效果
window.app = schedule()

初始狀態的 useState

上面我們在 useState 函數中留了一個 todo項,在實現 useState 函數之前,我們先來思考一個問題,我們應該怎樣存儲 useState 生成的狀態呢,通常我們調用 useState 是像下面這樣的

const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
const [count3, setCount3] = useState(0)

在 React 源碼中,React 是通過鏈表結構來存儲這些 hook 的,我們要把所有的 state 通過鏈表的形式存儲,並且我們要將 workInprogressHook 指向當前 hook 方便我們處理,下面我們來試着實現 useState

function useState(initialState) {
  let hook // 當前 hook 節點

  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  if (isMount) {
    hook = {
      memoizedState: initialState,
      next: null,
    }

    // 創建 hook 鏈表
    // 如果沒有初始化的 hook 則初始化 hook 節點,並將當前處理節點(workInprogressHook)指向當前 hook
    // 如果不是初始化的話,則將 當前處理節點(workInprogressHook)的下一個節點指向 hook
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook
    } else {
      workInprogressHook.next = hook
    }

    workInprogressHook = hook
  }

  // todo 實現更新邏輯
}

更新 state

在完善 useState 的更新邏輯,我們先來想想,既然 state 是需要用鏈表來存儲的,那么update 函數也得需要對應一個鏈表來存儲啊,我們來看看為什么需要鏈表來存儲

const [count, setCount] = useState(0)

return (
  <p onClick={() => {
    setCount(num => num + 1)
    setCount(num => num + 1)
    setCount(num => num + 1)
  }}>
    {num}
  </p>
)

可以看到更新函數 setCount 可能不是只調用一次,在 React 中,這些 update 函數被環狀鏈表組合在了一起。這時我們就需要在 hook 上增加一個 queue 屬性來存儲 update 函數

hook = {
  memoizedState: initialState,
  next: null,
  // 保存改變的狀態
  // 隊列是因為 有可能有多個更新函數
  // setCount(num => num + 1)
  // setCount(num => num + 1)
  // setCount(num => num + 1)
  queue: {
    pending: null,
  }
}

在 React 源碼中,更新階段會調用 dispatchAction.bind(null, hook.queue) 這個函數來更新 state,我們先來看看是怎樣實現的

function dispatchAction(queue, action) {
  // 更新節點
  const update = {
    action,
    next: null,
  }

  // 構建更新鏈表 環狀鏈表
  // queue.pending === null 還沒有觸發更新,創建第一個更新
  if (queue.pending === null) {
    // u0 -> u0 -> u0
    update.next = update
  } else {
    // u0 -> u0
    // u1 -> u0 -> u1
    update.next = queue.pending.next
    queue.pending.next = update
  }
  queue.pending = update

  // 觸發更新
  schedule()
}

環狀鏈表的操作可能不太容易理解,下面我們來詳細講解下。

  • 首先,當產生第一個 update 的時候(我們叫它 u0),此時queue.pending === null。update.next = update;即u0.next = u0,他會和自己首尾相連形成單向環狀鏈表。然后queue.pending = update;即queue.pending = u0
queue.pending = u0 ---> u0
                ^       |
                |       |
                ---------
  • 當產生第二個update(我們叫他u1),update.next = queue.pending.next;,此時queue.pending.next === u0, 即u1.next = u0。queue.pending.next = update;,即u0.next = u1。然后queue.pending = update;即queue.pending = u1
queue.pending = u1 ---> u0   
                ^       |
                |       |
                ---------

這樣做的好處就是,當我們需要遍歷 update 時,queue.pending.next指向第一個插入的update,方便我們去操作 update 函數。邏輯還是比較清晰明了的,如果上面看不懂的話,需要去好好補一下數據結構了哦。

完善 useState

在 dispatchAction 中,我們將 update 構建成環狀鏈表后, 接着我們就可以繼續實現 useState 中的更新邏輯,當我們需要更新 state 時,我們就需要遍歷環狀鏈表,將新的狀態更新到 update 函數中去,當遍歷完,我們將鏈表清空,最后我們將新的 state 和 update 函數返回即可。

function useState(initialState) {
  let hook // 當前 hook 節點

  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  if (isMount) {
    ... mount 階段
  } else {
    // 如果是 update 的情況,則將 hook 指向 workInprogressHook
    // workInprogressHook 指向 hook 鏈表的下一個節點
    hook = workInprogressHook
    workInprogressHook = workInprogressHook.next
  }

  // 處理更新 遍歷更新函數的環狀鏈表
  // 獲取初始狀態
  let baseState = hook.memoizedState

  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next

    do {
      const action = firstUpdate.action
      // 處理更新狀態
      baseState = action(baseState)
      firstUpdate = firstUpdate.next
    } while (firstUpdate !== hook.queue.pending.next) // 遍歷完環狀鏈表

    // 清空鏈表
    hook.queue.pending = null
  }

  hook.memoizedState = baseState

  return [baseState, dispatchAction.bind(null, hook.queue)]
}

淺析 Hooks 源碼

上面我們實現一個簡單的 useState,我們使用 isMount 來判斷更新時機,但是 React 中沒有這么 low,React 中使用了不同的 hash 值來標識不同的 hooks 的狀態

本篇博客 React 源碼為 16.12.0 版本

// 利用 hash 來存儲不同狀態的方法
// mount 階段
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
};

// update 階段
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useResponder: createDeprecatedResponderListener,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
};

Redux 的作者 Dan Abramov 在加入 React 團隊中也是將 Redux 的思想帶入了 React 中,useState 和 useReducer 這兩個 hook 就是他的代表作,而且從本質來說,useState 不過就是預置了 reducer 的 useReducer,下面的源碼會印證這點。

mount 階段的 useState 和 useReducer

在 mount 階段,useState 會調用 mountState, 而 useReducer 則會調用 mountReducer
下面我們來看看這兩個方法

// \react\packages\react-reconciler\src\ReactFiberHooks.js

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 創建hook對象
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

能看到 mountState 和 mountReducer 的區別就是 queue 中 lastRenderedReducer 字段

const queue = (hook.queue = {
  // 與極簡實現中的同名字段意義相同,保存update對象
  pending: null,
  // 保存dispatchAction.bind()的值
  dispatch: null,
  // 上一次render時使用的reducer
  lastRenderedReducer: reducer,
  // 上一次render時的state
  lastRenderedState: (initialState: any),
});

mountReducer 的 lastRenderedReducer 接收的就是傳入你自定義的 reducer;而 mountState 接收的 lastRenderedReducer 是一個預置的 basicStateReducer。
下面我們來看看 basicStateReducer 的實現

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}

這也直接證明了 useState 即 reducer 為 basicStateReducer 的 useReducer。

update 階段的 useState 和 useReducer

在 update 階段 updateState 則是直接調用了 updateReducer 方法,更加證明了 useState 就是特殊的 useReducer

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

下面我們來看看 updateReducer 是怎樣實現的

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 獲取當前hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  
  queue.lastRenderedReducer = reducer;

  // ...計算 newState 的過程

  hook.memoizedState = newState;
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

源碼這個部分比較長,我只保留了一些主干代碼,大致流程就是重新計算新的 state,然后將新的 state 返回。

調用更新函數

我們在使用 setCount((count) => count + 1) 這樣的更新函數更新 state 的時候,會觸發 dispatchAction 函數,這個時候當前的函數組件對應的 Fiber 和 對應的更新方法(hook.queue)就通過調用 dispatchAction.bind 傳入了方法內

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {

  const currentTime = requestCurrentTimeForUpdate();
  const suspenseConfig = requestCurrentSuspenseConfig();
  const expirationTime = computeExpirationForFiber(
    currentTime,
    fiber,
    suspenseConfig,
  );

  const update: Update<S, A> = {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // 構建 update 環狀鏈表
  // Append the update to the end of the list.
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.
    didScheduleRenderPhaseUpdate = true;
    update.expirationTime = renderExpirationTime;
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  } else {
    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // 只保留核心代碼
      // ...優化調度渲染
      const currentState: S = (queue.lastRenderedState: any);
      const eagerState = lastRenderedReducer(currentState, action);
      update.eagerReducer = lastRenderedReducer;
      update.eagerState = eagerState;
      if (is(eagerState, currentState)) {
        // Fast path. We can bail out without scheduling React to re-render.
        // It's still possible that we'll need to rebase this update later,
        // if the component re-renders for a different reason and by that
        // time the reducer has changed.
        return;
      }
    }

    // 調度
    scheduleWork(fiber, expirationTime);
  }
}

dispatchAction 函數我只留了一些主干代碼,總結一下:將 update 加入 queue.pending,構建環狀鏈表,在優化渲染后,開啟調度。

if...else... 是 React 的一些優化手段,if 內:

if (
  fiber === currentlyRenderingFiber ||
  (alternate !== null && alternate === currentlyRenderingFiber)
)

這是需要 render 階段觸發的更新,所以需要給當前的更新放到一個延遲隊列中,在渲染階段,再重新啟用 workInProgress 去觸發更新

而下面的 else...if

else if (
    fiber.expirationTime === NoWork &&
    (alternate === null || alternate.expirationTime === NoWork)
  )

fiber.expirationTime 保存的是 fiber 對象的 update的優先級,fiber.expirationTime === NoWork 則意味着 fiber 對象上不存在 update。
通過源碼的學習,我們已經知道了 update 計算 state 是在 hook 的聲明階段,在調用階段還通過內置的 reducer 重新計算 state,如果調用階段的 state 和聲明階段的 state 是相等的,那么就完全不需要重新開啟一次新的調度了。

到此我們就了解了 hooks 的理念,其實 React 就是在踐行函數式編程,如果你覺得這篇“人類高質量文章”寫的不錯就點個贊吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM