React Hooks源碼深度解析

語言: CN / TW / HK

作者:京東零售 鄭炳懿

前言

React HooksReact16.8 引入的一個新特性,它允許函數組件中使用state和其他 React 特性,而不必使用類組件。Hooks是一個非常重要的概念,因為它們提供了更簡單、更易於理解的React開發體驗。

React Hooks的核心源碼主要包括兩個部分:React內部的Hook管理器和一系列預置的Hook函數

首先,讓我們看一下React內部的Hook管理器。這個管理器是React內部的一個重要機制,它負責管理組件中的所有Hook,並確保它們在組件渲染期間以正確的順序調用。

內部Hook管理器

示例:

const Hook = {
  queue: [],
  current: null,
};

function useState(initialState) {
  const state = Hook.current[Hook.queue.length];
  if (!state) {
    Hook.queue.push({
      state: typeof initialState === 'function' ? initialState() : initialState,
      setState(value) {
        this.state = value;
        render();
      },
    });
  }
  return [state.state, state.setState.bind(state)];
}

function useHook(callback) {
  Hook.current = {
    __proto__: Hook.current,
  };
  try {
    callback();
  } finally {
    Hook.current = Hook.current.__proto__;
  }
}

function render() {
  useHook(() => {
    const [count, setCount] = useState(0);
    console.log('count:', count);
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  });
}

render();

在這個示例中,Hook對象有兩個重要屬性:queuecurrentqueue存儲組件中所有Hook的狀態和更新函數,current存儲當前正在渲染的組件的Hook鏈表。useStateuseHook函數則分別負責創建新的Hook狀態和在組件中使用Hook

預置 Hook 函數

useState Hook

以下是useState Hook的實現示例:

function useState(initialState) {
  const hook = updateWorkInProgressHook();
  if (!hook.memoizedState) {
    hook.memoizedState = [
      typeof initialState === 'function' ? initialState() : initialState,
      action => {
        hook.queue.pending = true;
        hook.queue.dispatch = action;
        scheduleWork();
      },
    ];
  }
  return hook.memoizedState;
}

上述代碼實現了useState Hook,其主要作用是返回一個state和更新函數的數組,state 初始值為initialState

在這個實現中,updateWorkInProgressHook()函數用來獲取當前正在執行的函數組件的 fiber 對象並判斷是否存在對應的hook。它的實現如下:

function updateWorkInProgressHook() {
  const fiber = getWorkInProgressFiber();
  let hook = fiber.memoizedState;
  if (hook) {
    fiber.memoizedState = hook.next;
    hook.next = null;
  } else {
    hook = {
      memoizedState: null,
      queue: {
        pending: null,
        dispatch: null,
        last: null,
      },
      next: null,
    };
  }
  workInProgressHook = hook;
  return hook;
}

getWorkInProgressFiber()函數用來獲取當前正在執行的函數組件的fiber對象,workInProgressHook則用來存儲當前正在執行的hook對象。在函數組件中,每一個useState調用都會創建一個新的 hook 對象,並將其添加到fiber對象的hooks鏈表中。這個hooks鏈表是通過fiber對象的memoizedState屬性來維護的。

我們還需要注意到在useState Hook的實現中,每一個hook對象都包含了一個queue對象,用來存儲待更新的狀態以及更新函數。scheduleWork()函數則用來通知React調度器有任務需要執行。

React的源碼中,useState函數實際上是一個叫做useStateImpl的內部函數。

下面是useStateImpl的源碼:

function useStateImpl<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

可以看到,useStateImpl函數的作用就是獲取當前的dispatcher並調用它的useState方法,返回一個數組,第一個元素是狀態的值,第二個元素是一個dispatch函數,用來更新狀態。這裏的resolveDispatcher函數用來獲取當前的dispatcher,其實現如下:

function resolveDispatcher(): Dispatcher {
  const dispatcher = currentlyRenderingFiber?.dispatcher;
  if (dispatcher === undefined) {
    throw new Error('Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)');
  }
  return dispatcher;
}

resolveDispatcher函數首先嚐試獲取當前正在渲染的fiber對象的dispatcher屬性,如果獲取不到則説

明當前不在組件的渲染過程中,就會拋出一個錯誤。

最後,我們來看一下useState方法在具體的dispatcher實現中是如何實現的。我們以useReducer

dispatcher為例,它的實現如下:

export function useReducer<S, A>(
  reducer: (prevState: S, action: A) => S,
  initialState: S,
  initialAction?: A,
): [S, Dispatch<A>] {
  const [dispatch, currentState] = updateReducer<S, A>(
    reducer,
    // $FlowFixMe: Flow doesn't like mixed types
    [initialState, initialAction],
    // $FlowFixMe: Flow doesn't like mixed types
    reducer === basicStateReducer ? basicStateReducer : updateStateReducer,
  );
  return [currentState, dispatch];
}

可以看到,useReducer方法實際上是調用了一個叫做updateReducer的函數,返回了一個包含當前狀態和dispatch函數的數組。updateReducer的實現比較複雜,涉及到了很多細節,這裏不再展開介紹。

useEffect Hook

useEffectReact中常用的一個Hook函數,用於在組件中執行副作用操作,例如訪問遠程數據、添加/移除事件監聽器、手動操作DOM等等。useEffect的核心功能是在組件的渲染過程結束之後異步執行回調函數,它的實現方式涉及到 React 中的異步渲染機制。

以下是useEffect Hook的實現示例:

function useEffect(callback, dependencies) {
  // 通過調用 useLayoutEffect 或者 useEffect 方法來獲取當前的渲染批次
  const batch = useContext(BatchContext);

  // 根據當前的渲染批次判斷是否需要執行回調函數
  if (shouldFireEffect(batch, dependencies)) {
    callback();
  }

  // 在組件被卸載時清除當前 effect 的狀態信息
  return () => clearEffect(batch);
}

在這個示例中,useEffect接收兩個參數:回調函數和依賴項數組。當依賴項數組中的任何一個值發生變化時,

React會在下一次渲染時重新執行useEffect中傳入的回調函數。

useEffect函數的實現方式主要依賴於React中的異步渲染機制。當一個組件需要重新渲染時,React會將所有的state更新操作加入到一個隊列中,在當前渲染批次結束之後再異步執行這些更新操作,從而避免在同一個渲染批次中連續執行多次更新操作。

useEffect函數中,我們通過調用useContext(BatchContext)方法來獲取當前的渲染批次,並根據shouldFireEffect方法判斷是否需要執行回調函數。在回調函數執行完畢後,我們需要通過clearEffect方法來清除當前effect的狀態信息,避免對後續的渲染批次產生影響。

總結

總的來説,React Hooks的實現原理並不複雜,它主要依賴於React內部的fiber數據結構和調度系統,通過這些機制來實現對組件狀態的管理和更新。Hooks能夠讓我們在函數組件中使用狀態和其他React特性,使得函數組件的功能可以和類組件媲美。

除了useStateuseEffecthookReact還有useContext等常用的Hook。它們的實現原理也基本相似,都是利用fiber架構來實現狀態管理和生命週期鈎子等功能。

以上是hook簡單實現示例,它們並不是React中實際使用的代碼,但是可以幫助我們更好地理解hook的核心實現方式。