React Hook源碼解析(一)

本文首發於公衆號:符合預期的CoyPanjavascript

寫在前面

React Hook已經正式發佈了一段時間了。我在項目中也進行過嘗試,一個很直觀的感覺:寫起來很爽。可是一直沒有深刻了解過其實現原理。本文將嘗試從源碼層面,瞭解React hooks的原理。本文所指的React版本爲:v16.12.0html

Hook概述

Hook 是 React 16.8 的新增特性,它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。Hook的誕生,是爲共享狀態邏輯提供更好的原生途徑。React官方文檔,已經對hook進行了十分全面的介紹:reactjs.org/docs/hooks-…java

useState實現原理

Hook中使用的最多的,可能就是useState這個api。React是如何實現這個api的呢?React如何保存Hook的state?爲何咱們每次調用useState的時候,均可以拿到最新的值?react

在寫做文本前,我對React內部實現的瞭解並不夠深刻,只是瞭解其Fiber實現及工做流程。不過我認爲這對理解hook的實現原理並不會有很大的影響。api

本文將專一於這兩個問題。咱們經過示例代碼,調試一下react的源碼。示例代碼以下:函數

const App = () => {
    const [name, setName] = useState('hello');
    const [password, setPassword] = useState('world');
    return <React.Fragment>
        <input value={name} onChange={ e => setName(e.target.value) } />
        <input value={password} onChange={ e => setPassword(e.target.value) } />
    </React.Fragment>
};
複製代碼

useState的入口代碼:源碼分析

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
複製代碼

這個dispatcher對象包含了全部的官方內置hook。this

readContext: ƒ (context, observedBits)
useCallback: ƒ (callback, deps)
useContext: ƒ (context, observedBits)
useEffect: ƒ (create, deps)
useImperativeHandle: ƒ (ref, create, deps)
useLayoutEffect: ƒ (create, deps)
useMemo: ƒ (create, deps)
useReducer: ƒ (reducer, initialArg, init)
useRef: ƒ (initialValue)
useState: ƒ (initialState)
useDebugValue: ƒ (value, formatterFn)
useResponder: ƒ (responder, props)
useDeferredValue: ƒ (value, config)
useTransition: ƒ (config)
複製代碼

先暫時無論這個dispatcher是怎麼來的,咱們接着看下useState的內部邏輯。spa

組件初次掛載時

React處理渲染帶有hook的組件的核心函數爲:renderWithHooks3d

咱們來看看再返回[name, setName]以前,都經歷了哪些步驟:

...
  // 組件掛載時,生成一個hook對象
  var hook = mountWorkInProgressHook(); 

	// 初始state綁在hook對象上
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
	
	// 記錄hook值的改變
  var queue = hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };

	// 生成 更改狀態的方法。
	// 這裏的 currentlyRenderingFiber$1是一個全局變量,表示當前正在渲染的Fiber節點。這個很重要,一下子再說。
  var dispatch = queue.dispatch = dispatchAction.bind(null,currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
	...
複製代碼

首先來看hook對象是如何生成:

function mountWorkInProgressHook() {
  
  // 初始化的hook對象
  var hook = {
    memoizedState: null, // 當前的state值
    baseState: null, 
    queue: null,
    baseUpdate: null,
    next: null
  };

  // workInProgressHook是一個全局變量,表示當前正在處理的hook
  if (workInProgressHook === null) {
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}
複製代碼

從上面的代碼能夠看到,hook實際上是以鏈表的形式存儲起來的。每個hook都有一個指向下一個hook的指針。若是咱們在組件代碼中聲明瞭多個hook,那這些hook對象之間是這樣排列的:

hookA.next = hookB;
hookB.next = hookC;
...
複製代碼

React會把hook對象掛到Fiber節點的memoizedState屬性上:

var renderedWork = currentlyRenderingFiber$1;
renderedWork.memoizedState = firstWorkInProgressHook;
複製代碼

在本文的例子中,組件掛載完畢後,其Fiber對象上的memoizedState屬性值以下:

setState

組件初次掛載完成了,咱們在頁面進行輸入時,hook是如何工做的呢?即,[name, setName]=useState('')中的setName都幹了什麼。

上文已經提到了,hook中的setName是這樣來的

var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
複製代碼

咱們調用setName的時候,事實上就是執行了這個dispatchAction

dispatchAction中,會進行render的調度處理,同時,會存儲這次更新的信息:

...
    // 記錄更新信息
		var _update2 = {
      expirationTime: expirationTime,
      suspenseConfig: suspenseConfig,
      action: action,
      eagerReducer: null,
      eagerState: null,
      next: null
    };

		...
    
		// 這裏的queue,是以前傳入的hook對象中的queue。這裏保留了一個引用,很重要。
    var last = queue.last;

		// 更新鏈表
    if (last === null) {
      // This is the first update. Create a circular list.
      _update2.next = _update2;
    } else {
      var first = last.next;

      if (first !== null) {
        // Still circular.
        _update2.next = first;
      }

      last.next = _update2;
    }
    queue.last = _update2;

		// 接下來,交給React去調度處理
    ...
複製代碼

通過React的調度,會帶上action(setName的傳參),再次進入hook組件核心渲染邏輯:renderWithHooks。此時,因爲並不是首次渲染組件,React會使用另一個掛載有全局hook函數的對象上的useState。在這個useState中,會使用一個叫作updateState的函數來計算最新的state值。而這個updateState的代碼很簡單:

function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}
複製代碼

先看這個basicStateReducer, 也很簡單:

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}
複製代碼

就是實現了一個reducer的功能,經過傳入的state的action,計算出最新的state。這裏的代碼告訴咱們,咱們其實能夠傳入一個函數做爲userState的參數,經過函數返回最新的state。

咱們繼續深刻updateReducer的代碼:

function updateReducer(reducer, initialArg, init) {
  
  // 經過fiber節點的memoizedState屬性拿到當前hook。
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  // 這裏是爲了處理當前render週期中,再次觸發render的問題,先暫時忽略這個條件分支。
  if (numberOfReRenders > 0) {
    ...
  } 

	// 最近的一次更新
  var last = queue.last; 

  var baseUpdate = hook.baseUpdate;
  var baseState = hook.baseState; // Find the first unprocessed update.

  var first;

  if (baseUpdate !== null) {
    if (last !== null) {
      // For the first update, the queue is a circular linked list where
      // `queue.last.next = queue.first`. Once the first update commits, and
      // the `baseUpdate` is no longer empty, we can unravel the list.
      last.next = null;
    }

    first = baseUpdate.next;
  } else {
    first = last !== null ? last.next : null;
  }

  if (first !== null) {
    var _newState = baseState;
    var newBaseState = null;
    var newBaseUpdate = null;
    var prevUpdate = baseUpdate;
    var _update = first;
    var didSkip = false;

    // 調度,獲取最新的state
    do {
      var updateExpirationTime = _update.expirationTime;

      if (updateExpirationTime < renderExpirationTime$1) {
        // react調度代碼,省略
        ...
      } else {
        ...
        if (_update.eagerReducer === reducer) {
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          _newState = _update.eagerState;
        } else {
          // 經過reducer計算出最新的state
          var _action = _update.action;
          _newState = reducer(_newState, _action);
        }
      }
      prevUpdate = _update;
      _update = _update.next;
    } while (_update !== null && _update !== first);

    ...

    hook.memoizedState = _newState;
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;
    queue.lastRenderedState = _newState;
    
  }
  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}
複製代碼
小結

咱們以示例代碼,簡單分析了一下hook的內部原理。回到咱們開始提出的問題,React如何保存Hook的state?爲何咱們每次調用useState的時候,均可以拿到最新的值?如今能夠簡單總結一下了:

在執行useState的時候,react會在組件的Fiber節點上,按照useState的前後順序,以鏈表的方式建立hook,而且將state和該state對應的更新函數返回。更新時,會順着鏈表,依次計算最新的state值返回。

用一個圖總結一下:

上面的圖,也很好的解釋了爲何不容許在條件分支中使用hook。useState執行一次,鏈表纔會前進到下一個節點。若是中途某個節點斷了,那麼state就對應不起來了,代碼天然就出Bug了。

寫在後面

本文從源碼角度,闡述了hook的大概實現原理。其中有一些分支邏輯以及hook中的反作用等將在後續的文章中進行解析。本人第一次寫源碼分析,有不恰當的地方請多指教。

相關文章
相關標籤/搜索