React Hooks原始碼深度解析

京東雲開發者發表於2023-03-16
作者:京東零售 鄭炳懿

前言

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的核心實現方式。

相關文章