React Hooks 原始碼解析(3):useState

airingursb發表於2019-11-09

在寫本文之前,事先閱讀了網上了一些文章,關於 Hooks 的原始碼解析要麼過於淺顯、要麼就不細緻,所以本文著重講解原始碼,由淺入深,爭取一行程式碼也不放過。那本系列講解第一個 Hooks 便是 useState,我們將從 useState 的用法開始,再闡述規則、講解原理,再簡單實現,最後原始碼解析。另外,在本篇開頭,再補充一個 Hooks 的概述,前兩篇限於篇幅問題一直沒有寫一塊。

注:距離上篇文章已經過去了兩個月,這兩個月業務繁忙所以沒有什麼時間更新該系列的文章,但 react 這兩個月卻從 16.9 更新到了 16.11,review 了一下這幾次的更新都未涉及到 hooks,所以我也直接把原始碼筆記這塊更新到了 16.11。

1. React Hooks 概述

Hook 是 React 16.8 的新增特性,它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。其本質上就是一類特殊的函式,它們約定以 use 開頭,可以為 Function Component 注入一些功能,賦予 Function Component 一些 Class Component 所具備的能力。

例如,原本我們說 Function Component 無法儲存狀態,所以我們經常說 Stateless Function Component,但是現在我們藉助 useState 這個 hook 就可以讓 Function Component 像 Class Component 一樣具有狀態。前段時間 @types/react 也將 SFC 改成了 FC。

1.1 動機

在 React 官網的 Hook 簡介中列舉了推出 Hook 的原因:

  1. 在元件之間複用狀態邏輯很難
  2. 複雜元件變得難以理解
  3. 難以理解的 class

一,元件之間複用狀態邏輯很難。是我們系列第二篇中一直討論的問題,此處不再贅述。

二,複雜元件變得難以理解,即元件邏輯複雜。主要是針對 Class Component 來說,我們經常要在元件的各種生命週期中編寫程式碼,如在 componentDidMount 和 componentDidUpdate 中獲取資料,但是在 componentDidMount 中可能也包括很多其他的邏輯,使得元件越開發越臃腫,且邏輯明顯扎堆在各種生命週期函式中,使得 React 開發成為了“面向生命週期程式設計”。而 Hooks 的出現,將這種這種“面向生命週期程式設計”變成了“面向業務邏輯程式設計”,使得開發者不用再去關心本不該關心的生命週期。

三,難以理解的 class,表現為函數語言程式設計比 OOP 更加簡單。那麼再深入一些去考慮效能,Hook 會因為在渲染時建立函式而變慢嗎?答案是不會,在現在瀏覽器中閉包和類的原始效能只有在極端場景下又有有明顯的區別。反而,我們可以認為 Hook 的設計在某些方面會更加高效:

  1. Hook 避免了 class 需要的額外開支,像是建立類例項和在建構函式中繫結事件處理器的成本。
  2. 符合語言習慣的程式碼在使用 Hook 時不需要很深的元件樹巢狀。這個現象在使用高階元件、render props、和 context 的程式碼庫中非常普遍。元件樹小了,React 的工作量也隨之減少。

其實,React Hooks 帶來的好處不僅是更函式式、更新粒度更細、程式碼更清晰,還有以下三個優點:

  1. 多個狀態不會產生巢狀,寫法還是平鋪的:如 async/await 之於 callback hell 一樣,hooks 也解決了高階元件的巢狀地獄問題。雖然 renderProps 也可以通過 compose 解決這個問題,但使用略為繁瑣,而且因為強制封裝一個新物件而增加了實體數量。
  2. Hooks 可以引用其他 Hooks,自定義 Hooks 更加靈活。
  3. 更容易將元件的 UI 與狀態分離。

1.2 Hooks API

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue
  • useResponder

以上 Hooks API 都會在未來一一講解,此處不再贅述。本文先講解 useState。

1.3 自定義 Hooks

通過自定義 Hook,可以將元件邏輯提取到可重用的函式中。這裡安利一個網站:usehooks.com/,裡面收集了實用的自定義 Hooks,可以無縫接入專案中使用,充分體現了 Hooks 的可複用性之強、使用之簡單。

2. useState 的用法與規則

import React, { useState } from 'react'

const App: React.FC = () => {
    const [count, setCount] = useState<number>(0)
    const [name, setName] = useState<string>('airing')
    const [age, setAge] = useState<number>(18)

    return (
        <>
            <p>You clicked {count} times</p>
            <button onClick={() => {
                setCount(count + 1)
                setAge(age + 1)
            }}>
                Click me
            </button>
        </>
    )
}

export default App
複製程式碼

如果用過 redux 的話,這一幕一定非常眼熟。給定一個初始 state,然後通過 dispatch 一個 action,再經由 reducer 改變 state,再返回新的 state,觸發元件重新渲染。

它等價於下面這個 Class Component:


import React from 'react'

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      age: 18,
      name: 'airing'
    };
  }

  render() {
    return (
      <>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ 
            count: this.state.count + 1,
            age: this.state.age + 1
        })}>
          Click me
        </button>
      </>
    );
  }
}

複製程式碼

可以看到 Function Component 比 Class Component 簡潔需要,useState 的使用也非常簡單。但需要注意的是,Hooks 的使用必須要符合這條規則:確保 Hook 在每一次渲染中都按照同樣的順序被呼叫。因此最好每次只在最頂層使用 Hook,不要在迴圈、條件、巢狀函式中呼叫 Hooks,否則容易出錯。

那麼,為什麼我們必須要滿足這條規則?接下來,我們看一下 useState 的實現原理並自己親自動手實現一個 useState 便可一目瞭然。

3. useState 的原理與簡單實現

3.1 Demo 1: dispatch

第二節中我們發現 useState 的用法蠻像 Redux 的,那我們基於 Redux 的思想,自己動手實現一個 useState:

function useState(initialValue) {
    let state = initialValue
    function dispatch(newState) {
        state = newState
        render(<App />, document.getElementById('root'))
    }
    return [state, dispatch]
}
複製程式碼

我們將從 React 中引入的 useState 替換成自己實現的:

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

function useState(initialValue: any) {
    let state = initialValue
    function dispatch(newState: any) {
        state = newState
        render(<App />, document.getElementById('root'))
    }
    return [state, dispatch]
}

const App: React.FC = () => {
    const [count, setCount] = useState(0)
    const [name, setName] = useState('airing')
    const [age, setAge] = useState(18)

    return (
        <>
            <p>You clicked {count} times</p>
            <p>Your age is {age}</p>
            <p>Your name is {name}</p>
            <button onClick={() => {
                setCount(count + 1)
                setAge(age + 1)
            }}>
                Click me
            </button>
        </>
    )
}

export default App
複製程式碼

這個時候我們發現點選按鈕不會有任何響應,count 和 age 都沒有變化。因為我們實現的 useState 並不具備儲存功能,每次重新渲染上一次的 state 就重置了。這裡想到可以在外部用個變數來儲存。

3.2 Demo 2: 記憶 state

基於此,我們優化一下剛才實現的 useState:

let _state: any
function useState(initialValue: any) {
    _state = _state | initialValue
    function setState(newState: any) {
        _state = newState
        render(<App />, document.getElementById('root'))
    }
    return [_state, setState]
}
複製程式碼

雖然按鈕點選有變化了,但是效果不太對。如果我們刪掉 age 和 name 這兩個 useState 會發現效果是正常的。這是因為我們只用了單個變數去儲存,那自然只能儲存一個 useState 的值。那我們想到可以用備忘錄,即一個陣列,去儲存所有的 state,但同時我們需要維護好陣列的索引。

3.3 Demo 3: 備忘錄

基於此,我們再次優化一下剛才實現的 useState:


let memoizedState: any[] = [] // hooks 的值存放在這個陣列裡
let cursor = 0 // 當前 memoizedState 的索引

function useState(initialValue: any) {
    memoizedState[cursor] = memoizedState[cursor] || initialValue
    const currentCursor = cursor
    function setState(newState: any) {
        memoizedState[currentCursor] = newState
        cursor = 0
        render(<App />, document.getElementById('root'))
    }
    return [memoizedState[cursor++], setState] // 返回當前 state,並把 cursor 加 1
}

複製程式碼

我們點選三次按鈕之後,列印出 memoizedState 的資料如下:

React Hooks 原始碼解析(3):useState

開啟頁面初次渲染,每次 useState 執行時都會將對應的 setState 繫結到對應索引的位置,然後將初始 state 存入 memoizedState 中。

useState-1

在點選按鈕的時候,會觸發 setCount 和 setAge,每個 setState 都有其對應索引的引用,因此觸發對應的 setState 會改變對應位置的 state 的值。

useState-3

這裡是模擬實現 useState,所以每次呼叫 setState 都有一次重新渲染的過程。

重新渲染依舊是依次執行 useState,但是 memoizedState 中已經有了上一次是 state 值,因此初始化的值並不是傳入的初始值而是上一次的值。

useState-2

因此剛才在第二節中遺留問題的答案就很明顯了,為什麼 Hooks 需要確保 Hook 在每一次渲染中都按照同樣的順序被呼叫?因為 memoizedState 是按 Hooks 定義的順序來放置資料的,如果 Hooks 的順序變化,memoizedState 並不會感知到。因此最好每次只在最頂層使用 Hook,不要在迴圈、條件、巢狀函式中呼叫 Hooks。

最後,我們來看看 React 中是怎樣實現 useState 的。

4. useState 原始碼解析

4.1 入口

首先在入口檔案 packages/react/src/React.js 中我們找到 useState,其源自 packages/react/src/ReactHooks.js。

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
複製程式碼

resolveDispatcher() 返回的是 ReactCurrentDispatcher.current,所以 useState 其實就是 ReactCurrentDispatcher.current.useState。

那麼,ReactCurrentDispatcher 是什麼?

import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';

const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
}
複製程式碼

我們最終找到了 packages/react-reconciler/src/ReactFiberHooks.js,在這裡有 useState 具體實現。該檔案也包含了所有 React Hooks 的核心處理邏輯。

4.2 型別定義

4.2.1 Hook

在開始之前,我們先看看 ReactFiberHooks.js 中幾個型別的定義。首先是 Hooks:

export type Hook = {
  memoizedState: any, // 指向當前渲染節點 Fiber, 上一次完整更新之後的最終狀態值

  baseState: any, // 初始化 initialState, 已經每次 dispatch 之後 newState
  baseUpdate: Update<any, any> | null, // 當前需要更新的 Update ,每次更新完之後,會賦值上一個 update,方便 react 在渲染錯誤的邊緣,資料回溯
  queue: UpdateQueue<any, any> | null, // 快取的更新佇列,儲存多次更新行為

  next: Hook | null,  // link 到下一個 hooks,通過 next 串聯每一 hooks
};
複製程式碼

可以看到,Hooks 的資料結構和我們之前自己實現的基本一致,memoizedState 也是一個陣列,準確來說 React 的 Hooks 是一個單向連結串列,Hook.next 指向下一個 Hook。

4.2.2 Update & UpdateQueue

那麼 baseUpdate 和 queue 又是什麼呢?先看一下 Update 和 UpdateQueue 的型別定義:

type Update<S, A> = {
  expirationTime: ExpirationTime, // 當前更新的過期時間
  suspenseConfig: null | SuspenseConfig,
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null,
  next: Update<S, A> | null, // link 下一個 Update

  priority?: ReactPriorityLevel, // 優先順序
};

type UpdateQueue<S, A> = {
  last: Update<S, A> | null,
  dispatch: (A => mixed) | null,
    lastRenderedReducer: ((S, A) => S) | null,
      lastRenderedState: S | null,
};
複製程式碼

Update 稱作一個更新,在排程一次 React 更新時會用到。UpdateQueue 是 Update 的佇列,同時還帶有更新時的 dispatch。具體的 React Fiber 和 React 更新排程的流程本篇不會涉及,後續會有單獨的文章補充講解。

4.2.3 HooksDispatcherOnMount & HooksDispatcherOnUpdate

還有兩個 Dispatch 的型別定義需要關注一下,一個是首次載入時的 HooksDispatcherOnMount,另一個是更新時的 HooksDispatcherOnUpdate。


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: createResponderListener,
};

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: createResponderListener,
};
複製程式碼

4.3 首次渲染

4.3.1 renderWithHooks

React Fiber 會從 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork() 開始執行(React Fiber 的具體流程後續單獨成文補充講解),對於 Function Component,其走以下邏輯載入或更新元件:

case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
複製程式碼

在 updateFunctionComponent 中,對於 Hooks 的處理是:

nextChildren = renderWithHooks(
  current,
  workInProgress,
  Component,
  nextProps,
  context,
  renderExpirationTime,
);
複製程式碼

因此,我們發現 React Hooks 的渲染核心入口是 renderWithHooks。其他的渲染流程我們並不關心,本文我們著重來看看 renderWithHooks 及其之後的邏輯。

我們回到 ReactFiberHooks.js 來看看 renderWithHooks 具體做了什麼,去除容錯程式碼和 __DEV__ 的部分,renderWithHooks 程式碼如下:

export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
  nextCurrentHook = current !== null ? current.memoizedState : null;

  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;

  // remainingExpirationTime = NoWork;
  // componentUpdateQueue = null;

  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;
  // sideEffectTag = 0;

  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because nextCurrentHook === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)

  // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so nextCurrentHook would be null during updates and mounts.
  
  ReactCurrentDispatcher.current =
    nextCurrentHook === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  

  let children = Component(props, refOrContext);

  if (didScheduleRenderPhaseUpdate) {
    do {
      didScheduleRenderPhaseUpdate = false;
      numberOfReRenders += 1;

      // Start over from the beginning of the list
      nextCurrentHook = current !== null ? current.memoizedState : null;
      nextWorkInProgressHook = firstWorkInProgressHook;

      currentHook = null;
      workInProgressHook = null;
      componentUpdateQueue = null;

      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnUpdateInDEV
        : HooksDispatcherOnUpdate;

      children = Component(props, refOrContext);
    } while (didScheduleRenderPhaseUpdate);

    renderPhaseUpdates = null;
    numberOfReRenders = 0;
  }

  // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there's no re-entrancy.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const renderedWork: Fiber = (currentlyRenderingFiber: any);

  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  renderedWork.effectTag |= sideEffectTag;

  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null;
  nextCurrentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;
  nextWorkInProgressHook = null;

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;
  sideEffectTag = 0;

  // These were reset above
  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;

  return children;
}

複製程式碼

renderWithHooks 包括三個部分,首先是賦值 4.1 中提到的 ReactCurrentDispatcher.current,後續是做 didScheduleRenderPhaseUpdate 以及一些初始化的工作。核心是第一部分,我們來看看:

nextCurrentHook = current !== null ? current.memoizedState : null;
  
ReactCurrentDispatcher.current =
    nextCurrentHook === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
複製程式碼

如果當前 Fiber 為空,就認為是首次載入,ReactCurrentDispatcher.current.useState 將賦值成 HooksDispatcherOnMount.useState,否則賦值 HooksDispatcherOnUpdate.useState。根據 4.2 中的型別定義,即首次載入時,useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnMount.useState = mountState;更新時 useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnUpdate.useState = updateState。

4.3.2 mountState

首先看看 mountState 的實現:

// 第一次呼叫元件的 useState 時實際呼叫的方法
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 建立一個新的 Hook,並返回當前 workInProgressHook
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;

  // 新建一個佇列
  const queue = (hook.queue = {
    last: null, // 最後一次更新邏輯,  包括 {action,next} 即狀態值和下一次 Update
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),  // 最後一次渲染元件時的狀態
  });

  const dispatch: Dispatch<
    BasicStateAction<S>,
    > = (queue.dispatch = (dispatchAction.bind(
      null,
      // 繫結當前 fiber 和 queue.
      ((currentlyRenderingFiber: any): Fiber),
      queue,
    ): any));
  return [hook.memoizedState, dispatch];
}
複製程式碼

4.3.3 mountWorkInProgressHook

mountWorkInProgressHook 是建立一個新的 Hook 並返回當前 workInProgressHook,實現如下:

// 建立一個新的 hook,並返回當前 workInProgressHook
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  // 只有在第一次開啟頁面的時候,workInProgressHook 為空
  if (workInProgressHook === null) {
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 已經存在 workInProgressHook 就將新建立的這個 Hook 接在 workInProgressHook 的尾部。
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
複製程式碼

4.3.4 dispatchAction

我們注意到 mountState 還做了一件很關鍵的事情,繫結當前 fiber 和 queue 到 dispatchAction 上:

const dispatch: Dispatch<
    BasicStateAction<S>,
    > = (queue.dispatch = (dispatchAction.bind(
      null,
      // 繫結當前 fiber 和 queue
      ((currentlyRenderingFiber: any): Fiber),
      queue,
    ): any));
複製程式碼

那我們看一下 dispatchAction 是如何實現的:


function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // 此分支為 re-render 時的 Fiber 排程處理
    didScheduleRenderPhaseUpdate = true;
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime,
      suspenseConfig: null,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
    // 將本次更新週期裡的更新記錄快取進 renderPhaseUpdates 中
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    const currentTime = requestCurrentTime();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    // 儲存所有的更新行為,以便在 re-render 流程中計算最新的狀態值
    const update: Update<S, A> = {
      expirationTime,
      suspenseConfig,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      // ... 更新迴圈連結串列
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    // 省略特殊情況 Fiber NoWork 時的程式碼

    // 建立一個更新任務,執行 fiber 的渲染
    scheduleWork(fiber, expirationTime);
  }
}
複製程式碼

if 的第一個分支涉及 Fiber 的排程,我們此處僅是提及,本文不詳細講解 Fiber,只要知道 fiber === currentlyRenderingFiber 時是 re-render,即當前更新週期中又產生了新的週期即可。如果是 re-render,didScheduleRenderPhaseUpdate 置為 true,而在 renderWithHooks 中 如果 didScheduleRenderPhaseUpdate 為 true,就會迴圈計數 numberOfReRenders 來記錄 re-render 的次數;另外 nextWorkInProgressHook 也會有值。所以後續的程式碼中,有用 numberOfReRenders > 0 來判斷是否是 re-render 的,也有用 nextWorkInProgressHook 是否為空來判斷是否是 re-render 的。

同時,如果是 re-render,會把所有更新過程中產生的更新記錄在 renderPhaseUpdates 這個 Map 上,以每個 Hook 的 queue 為 key。

至於最後 scheduleWork 的具體工作,我們後續單獨成文來分析。

4.4 更新

4.4.1 updateState

我們看看更新過程中的 useState 時實際呼叫的方法 updateState:

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

// 第一次之後每一次執行 useState 時實際呼叫的方法
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
複製程式碼

可以發現,其實 updateState 最終呼叫的其實是 updateReducer。對於 useState 觸發的 update action 來說,basicStateReducer 就是直接返回 action 的值(如果 action 是函式還會幫忙呼叫一下)。因此,useState 只是 useReduer 的一個特殊情況而已,其傳入的 reducer 為 basicStateReducer,負責改變 state,而非 useReducer 那樣可以傳入自定義的 reducer。

4.4.2 updateReducer

那我們來看看 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;

  if (numberOfReRenders > 0) {
    // re-render:當前更新週期中產生了新的更新
    const dispatch: Dispatch<A> = (queue.dispatch: any);
    if (renderPhaseUpdates !== null) {
      // 所有更新過程中產生的更新記錄在 renderPhaseUpdates 這個 Map上,以每個 Hook 的 queue 為 key。
      const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        let newState = hook.memoizedState;
        let update = firstRenderPhaseUpdate;
        do {
          // 如果是 re-render,繼續執行這些更新直到當前渲染週期中沒有更新為止
          const action = update.action;
          newState = reducer(newState, action);
          update = update.next;
        } while (update !== null);
        
        if (!is(newState, hook.memoizedState)) {
          markWorkInProgressReceivedUpdate();
        }

        hook.memoizedState = newState;
        if (hook.baseUpdate === queue.last) {
          hook.baseState = newState;
        }

        queue.lastRenderedState = newState;

        return [newState, dispatch];
      }
    }
    return [hook.memoizedState, dispatch];
  }



  const last = queue.last;
  const baseUpdate = hook.baseUpdate;
  const baseState = hook.baseState;
  
  let first;
  if (baseUpdate !== null) {
    if (last !== null) {
      last.next = null;
    }
    first = baseUpdate.next;
  } else {
    first = last !== null ? last.next : null;
  }
  if (first !== null) {
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        if (!didSkip) {
          didSkip = true;
          newBaseUpdate = prevUpdate;
          newBaseState = newState;
        }
        if (updateExpirationTime > remainingExpirationTime) {
          remainingExpirationTime = updateExpirationTime;
        }
      } else {
        markRenderEventTimeAndConfig(
          updateExpirationTime,
          update.suspenseConfig,
        );

        // 迴圈連結串列,執行每一次更新
        if (update.eagerReducer === reducer) {
          newState = ((update.eagerState: any): S);
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
    } while (update !== null && update !== first);

    if (!didSkip) {
      newBaseUpdate = prevUpdate;
      newBaseState = newState;
    }

    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;

    queue.lastRenderedState = newState;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
複製程式碼

updateReducer 分為兩種情況:

  1. 非 re-render,即當前更新週期只有一個 Update。
  2. re-render,當前更新週期又產生了新的更新。

在 4.3.4 中我們提到 numberOfReRenders 記錄了 re-render 的次數,如果大於 0 說明當前更新週期中又產生了新的更新,那麼就繼續執行這些更新,根據 reducer 和 update.action 來建立新的 state,直到當前渲染週期中沒有更新為止,最後賦值給 Hook.memoizedState 以及 Hook.baseState。

注:其實單獨使用 useState 的話幾乎不會遇到 re-render 的場景,除非直接把 setState 寫在函式的頂部,但是這樣會導致無限 re-render,numberOfReRenders 會突破限制,在 4.3.4 dispatchAction 中讓程式報錯(4.3.4 隱去了 __DEV__ 與這部分容錯程式碼):

invariant(
    numberOfReRenders < RE_RENDER_LIMIT,
    'Too many re-renders. React limits the number of renders to prevent ' +
    'an infinite loop.',
  );
複製程式碼

那麼再來看一下非 re-render 的情況,除去 Fiber 相關的程式碼和特殊邏輯,重點在於 do-while 迴圈,這段程式碼負責迴圈連結串列,執行每一次更新:

do {
  // 迴圈連結串列,執行每一次更新
  if (update.eagerReducer === reducer) {
    newState = ((update.eagerState: any): S);
  } else {
    const action = update.action;
    newState = reducer(newState, action);
  }
  prevUpdate = update;
  update = update.next;
} while (update !== null && update !== first);
複製程式碼

還有一點需要注意,在這種情況下需要對每一個 update 判斷優先順序,如果不是當前整體更新優先順序內的更新會被跳過,第一個跳過的 update 會變成新的 Hook.baseUpdate。需要保證後續的更新要在 baseUpdate 更新之後的基礎上再次執行,因此結果可能會不一樣。這裡的具體邏輯後續會成文單獨解析。最後同樣需要賦值給 Hook.memoizedState 以及 Hook.baseState。

4.4.3 updateWorkInProgressHook

這裡補充一下,注意到第一行程式碼獲取 Hook 的方式就與 mountState 不同,updateWorkInProgressHook 是獲取當前正在工作中的 Hook。實現如下:

// 獲取當前正在工作中的 Hook,即 workInProgressHook
function updateWorkInProgressHook(): Hook {
  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
    nextCurrentHook = currentHook !== null ? currentHook.next : null;
  } else {
    // Clone from the current hook.
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      queue: currentHook.queue,
      baseUpdate: currentHook.baseUpdate,

      next: null,
    };

    if (workInProgressHook === null) {
      workInProgressHook = firstWorkInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
    nextCurrentHook = currentHook.next;
  }
  return workInProgressHook;
}

複製程式碼

這裡分為兩種情況,在 4.3.4 中我們提到如果 nextWorkInProgressHook 存在那麼就是 re-render,如果是 re-render 說明當前更新週期中還要繼續處理 workInProgressHook。

如果不是 re-render,就取下一個 Hook 為當前的 Hook,同時像 4.3.3 mountWorkInProgressHook 一樣,新建一個 Hook 並返回 workInProgressHook。

總之,updateWorkInProgressHook 獲取到了當前工作中的 workInProgressHook。

5 結語

直觀一點,我截了一個 Hook 在執行中的資料結構,如下圖所示:

2DFB1D17-C16F-41B1-B4E3-2BB77A336AF2

總結一下上文中解析的流程,如下圖所示:

useState 流程

如果對於 useState 的原始碼仍有所疑惑,可以自己寫個小 Demo 在關鍵函式打斷點除錯一下。

相關文章