【react】react hook運行原理解析

聲明:本文的研究的源碼是react@16.3.1javascript

hook相關術語

hook

react在頂級命名空間上暴露給開發者的API,好比下面的代碼片斷:html

import React , { useState, useReducer, useEffect } from 'react'
複製代碼

咱們會把useState,useReduceruseEffect等等稱之爲「hook」。確切來講,hook是一個javascript函數。java

請注意,當咱們在下文中提到「hook」這個術語,咱們已經明確地跟「hook對象」這個術語區分開來了。react

react內置瞭如下的hook:算法

/* react/packages/react-reconciler/src/ReactFiberHooks.new.js */
export type HookType =
  | 'useState'
  | 'useReducer'
  | 'useContext'
  | 'useRef'
  | 'useEffect'
  | 'useLayoutEffect'
  | 'useCallback'
  | 'useMemo'
  | 'useImperativeHandle'
  | 'useDebugValue'
  | 'useDeferredValue'
  | 'useTransition'
  | 'useMutableSource'
  | 'useOpaqueIdentifier';
複製代碼

hook對象

/* react/packages/react-reconciler/src/ReactFiberHooks.new.js */
export type Hook = {
  memoizedState: any, 
  baseState: any, 
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null
};
複製代碼

從數據類型的角度來講,hook對象是一個「純javascript對象(plain javascript object)」。從數據結構的角度來看,它是一個單向鏈表(linked list)(下文簡稱爲「hook鏈」)。next字段的值能夠佐證這一點。redux

下面簡單地解釋一下各個字段的含義:數組

  • memoizedState。經過遍歷完hook.queue循環單向鏈表所計算出來的最新值。這個值會在commit階段被渲染到屏幕上。
  • baseState。咱們調用hook時候傳入的初始值。它是計算新值的基準。
  • baseQueue。
  • queue。參見下面的queue對象

update對象

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type Update<S, A> = {
  // TODO: Temporary field. Will remove this by storing a map of
  // transition -> start time on the root.
  eventTime: number,
  lane: Lane,
  suspenseConfig: null | SuspenseConfig,
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null,
  next: Update<S, A>,
  priority?: ReactPriorityLevel,
};
複製代碼

咱們只須要關注跟hook原理相關的字段便可,因此update對象的類型能夠簡化爲:markdown

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type Update<S, A> = {
  action: A,
  next: Update<S, A>
};
複製代碼
  • action。專用於useState,useReducer這兩個hook的術語。由於這兩個hook是借用redux概念的產物,因此,在這兩個hook的內部實現源碼中,使用了redux的諸多術語:「dispatch」,「reducer」,「state」,「action」等。可是此處的ation跟redux的action不是徹底同樣的。假若有一下代碼:
const [count,setState] = useState(0);
const [useInfo,dispatch] = useReducer(reducer,{name:'鯊叔',age:0})
複製代碼

從源碼的角度來講,咱們調用setState(1)setState(count=> count+1)dispatch({foo:'bar'})傳入的參數就是「action」。對於redux的action,咱們約定俗成爲{type:string,payload:any}這種類型,可是update對象中的action卻能夠爲任意的數據類型。好比說,上面的1,count=> count+1{foo:'bar'}都是update對象的action。數據結構

一併須要提到的是「dispatch方法」這個術語。從源碼的角度來看,useState/useReducer這兩個hook調用所返回數組的第二個元素其實都是內部dispatchAction函數實例的一個引用。咱們以React.useState()爲例子,不妨看看它的源碼:閉包

// React.useState()在mount階段的實現
  function mountState(initialState) {
    // 這裏省略了不少代碼
    // ......
    var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
    return [hook.memoizedState, dispatch];
  }
  
  // React.useState()在update階段的實現
   function updateState(initialState) {
    return updateReducer(basicStateReducer);
  }
  
  function updateReducer(reducer, initialArg, init) {
    var hook = updateWorkInProgressHook();
    var queue = hook.queue;
    // 這裏省略了不少代碼
    // ......
    var dispatch = queue.dispatch;
    return [hook.memoizedState, dispatch];
  }
複製代碼

能夠看得出,咱們開發者拿到的只是dispatch方法的一個引用。因此,下文會把useState/useReducer這兩個hook調用所返回數組的第二個元素統稱爲「dispatch方法」。調用dispatch方法會致使function component從新渲染。

  • next。指向下一個update對象的指針。從這裏咱們就能夠判斷update對象是一個單向鏈表。至於它何時變成【循環】單向鏈表,後面會講到。

queue對象

// react/packages/react-reconciler/src/ReactFiberHooks.new.js 
type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};
複製代碼
  • pending。咱們調用dispatch方法,主要是作了兩件事:1)生成一個由update對象組成的循環單向鏈表; 2)觸發react的調度流程。而pending就是這個循環單向鏈表的頭指針。
  • dispatch。返回給開發者的用於觸發組件re-render的函數實例引用。
  • lastRenderedReducer。 上一次update階段使用的reducer。
  • lastRenderedState。 使用lastRenderedReducer計算出來並已經渲染到屏幕的state。

currentlyRenderingFiber

這是一個全局變量,存在於function component的生命週期裏面。顧名思義,這是一個fiber節點。每個react component都有一個與之對應的fiber節點。按照狀態劃分,fiber節點有兩種:「work-in-progress fiber」和「finished-work fiber」。前者表明的是當前render階段正在更新react component,然後者表明的是當前屏幕顯示的react component。這兩種fiber節點經過alternate字段來實現【循環引用】對方。有源碼註釋爲證:

// react/packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {
  // ......
  // 此前省略了不少代碼
  
  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null,
  
  // 此後省略了不少代碼
  // ......

};
複製代碼

這裏的currentlyRenderingFiber是屬於「work-in-progress fiber」。可是爲了不歧義,內部源碼採用了當前這個命名:

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

// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
複製代碼

對這個全局變量的賦值行爲是發生在function component被調用以前。有源碼爲證:

export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any {
	currentlyRenderingFiber = workInProgress;
    // 此間省略了不少代碼
    // ......
    let children = Component(props, secondArg);
    // 此後省略了不少代碼
    // ......
}
複製代碼

沒錯,這裏的Component就是咱們日常所說,所寫的function component。能夠看出,一開始currentlyRenderingFiber是爲null的,在function component調用以前,它被賦值爲該function component所對應的fiber節點了。

currentHook

這是一個全局變量,對應於舊的hook鏈(舊的hook鏈的產生於hook的mount階段)上已經遍歷過的那個hook對象。當前正在遍歷的hook對象存放在updateWorkInProgressHook()方法中的局部變量nextCurrentHook上。

workInProgressHook

mount階段和update階段都存在。mount階段,這是一個全新的javascript對象;update階段,它是經過對舊hook對象進行淺拷貝獲得的新的,對應與當前被調用的hook的hook對象。不管是mount階段仍是update階段,它都是指向當前hook鏈中的最後一個被處理過(mount階段,對應於最後一個被建立的hook對象;update階段,對應於最後一個被拷貝的hook對象)的hook對象。

hook的mount階段

等同於組件的首次掛載階段,更確切來講是function component的第一次被調用(由於function component本質上是一個函數)。通常而言,是由ReactDOM.render()來觸發的。

hook的update階段

等同於組件的更新階段,更確切地說是function component的第二次,第三次......第n次的被調用。通常而言,存在兩種狀況使得hook進入update階段。第一種是,父組件的更新致使function component的被動更新;第二種是,在function component內部手動調用hook的dispatch方法而致使的更新。

小結

從數據類型的角度來講,上面所提到的「xxx對象」從數據結構的角度來看,它們又是相應的數據結構。下面,咱們把上面所提到的數據結構串聯到一塊以後就是mount階段,hook所涉及的數據結構:

幾個事實

1. mount階段調用的hook與update階段調用的hook不是同一個hook

import React, { useState } from 'react';

function Counter(){
	const [count,setState] = useState();
    
    return <div>Now the count is {count}<div>
}
複製代碼

就那上面的代碼,拿useSate這個hook做說明。Counter函數會被反覆調用,第一次調用的時候,對useState這個hook來講,就是它的「mount階段」。此後的每一次Counter函數調用,就是useState的「update階段」。

一個比較顛覆咱們認知的事實是,第一次調用的useState居然跟隨後調用的useState不是同一個函數。這恐怕是不少人都沒有想到的。useState只是一個引用,mount階段指向mount階段的「mountState」函數,update階段指向update階段的「updateState」函數,這就是這個事實背後的實現細節。具體來看源碼。react package暴露給開發者的useState,實際上是對應下面的實現:

// react/packages/react/src/ReactHooks.js

export function useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
複製代碼

而resolveDispatcher()函數的實現是這樣的:

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}
複製代碼

ReactCurrentDispatcher.current初始值是爲null的:

// react/src/ReactCurrentDispatcher.js

/** * Keeps track of the current dispatcher. */
const ReactCurrentDispatcher = {
  /** * @internal * @type {ReactComponent} */
  current: (null: null | Dispatcher),
};
複製代碼

那麼它是在何時被賦值了呢?賦了什麼值呢?答案是在function Component被調用以前。在renderWithHooks()這個函數裏面,有這樣的代碼:

export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any {
	// 這裏省略了不少代碼....
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
        
  let children = Component(props, secondArg);
  // 這裏省略了不少代碼.....
  return children;
}
複製代碼

這裏,current是一個fiber節點。從這個判斷能夠看出,function component沒有對應的fiber節點或者該fiber節點上沒有hook鏈表的時候,就是hook的mount階段。mount階段,Dispatcher.current指向的是HooksDispatcherOnMount;不然,就是updte階段。update階段,Dispatcher.current指向的是HooksDispatcherOnUpdate。

最後,咱們分別定位到HooksDispatcherOnMount和HooksDispatcherOnUpdate對象上,真相就一目瞭然:

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

const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState, // 目光請聚焦到這一行
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useOpaqueIdentifier: mountOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState, // 目光請聚焦到這一行
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useOpaqueIdentifier: updateOpaqueIdentifier,
  unstable_isNewReconciler: enableNewReconciler,
};
複製代碼

能夠看到,useState在mount階段對應的是「mountState」這個函數;在update階段對應的是「updateState」這個函數。再次強調,這裏只是拿useState這個hook舉例說明,其餘hook也是同樣的道理,在這裏就不贅言了。

2. useState()實際上是簡化版的useReducer()

說這句的意思就是,相比於useReducer,useState這個hook只是在API參數上不同而已。在內部實現裏面,useState也是走的是useReducer那一套機制。具體來講,useState也有本身的reducer,在源碼中,它叫basicStateReducer。請看,mount階段的useState的實現:

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

function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  // ......
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // .....
}
複製代碼

能夠看到,useState()也是有對應的reducer的,它就掛載在lastRenderedReducer這個字段上。那basicStateReducer長什麼樣呢?

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

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

能夠看到,這個basicStateReducer跟咱們本身寫的(redux式的)reducer是具備相同的函數簽名的:(state,action) => state,它也是一個真正的reducer。也就是說,在mount階段,useReducer使用的reducer是開發者傳入的reducer,而useState使用的是react幫咱們對action進行封裝而造成的basicStateReducer

上面是mount階段的useState,下面咱們來看看update階段的useState是怎樣跟useReducer產生關聯的。上面已經講過,useState在update階段引用的是updateState,那咱們來瞧瞧它的源碼實現:

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

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

沒錯,updateState()調用的就是updateReducer(),而useReducer在update階段引用的也是updateReducer函數!到這裏,對於這個事實算是論證完畢。

3. useReducer()的第一個參數「reducer」是能夠變的

廢話很少說,咱們來看看updateReducer函數的源碼實現(我對源碼進行了精簡):

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

function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新列表的表尾
  const last = queue.pending;

  // 獲取最先的那個update對象,時刻記住,這是循環鏈表
  first = last !== null ? last.next : null;

  if (first !== null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // 執行每一次更新,去更新狀態
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的狀態和修改狀態的方法
  return [hook.memoizedState, dispatch];
}
複製代碼

能夠看到,update階段useReducer傳進來的reducer是被用於最新值的計算的。也就是說,在update階段,咱們能夠根據必定的條件來切換reducer的。雖然,實際開發中,咱們不會這麼幹,可是,從源碼來看,咱們確實是能夠這麼幹的。

也許,你會問,useState能夠一樣這麼幹嘛?答案是:「不能」。由於useState所用到的reducer不是咱們能左右的。在內部源碼中,這個reducer固定爲basicStateReducer。

hook運做的基本原理

hook的整個生命週期能夠劃分爲三個階段:

  • mount階段
  • 觸發更新階段
  • update階段

經過了解這三個階段hook都幹了些什麼,那麼咱們就基本上就能夠掌握hook的運做基本原理了。

mount階段

簡單來講,在mount階段,咱們每調用一次hook(不區分類型。舉個例子說,我連續調用了三次useState(),那麼我就會說這是調用了三次hook),實際上會發生下面的三件事情:

  1. 建立一個新的hook對象;
  2. 構建hook單鏈表;
  3. 補充hook對象的信息。

第一步和第二步:【建立一個新的hook對象】和【構建hook單鏈表】

咱們每調用一次hook,react在內部都會調用mountWorkInProgressHook()方法。而【hook對象的建立】和【鏈表的構建】就是發生在這個方法裏面。由於它們的實現都是放在同一個方法裏面,這裏就放在一塊講了:

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
複製代碼

顯而易見,變量hook裝的就是初始的hook對象。因此,【建立一個新的hook對象】這一步算是講完了。

下面,咱們來看看第二步-【構建hook單鏈表】。它的代碼比較簡單,就是上面的mountWorkInProgressHook方法裏面的最後幾行代碼:

if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
複製代碼

術語章節已經講過,workInProgressHook是一個全局變量,它指向的是最後一個被生成的hook對象。若是workInProgressHook爲null,那就表明着根本就沒有生成過hook對象,對應於當前這個hook對象是第一個hook對象,則它會成爲表頭,被頭指針【currentlyRenderingFiber.memoizedState】所指向;不然,當前建立的hook對象被append到鏈表的尾部。這裏,react內部的實現採用了比較巧妙的實現。它新建了一個指針(workInProgressHook),每一輪構建完hook鏈表後都讓它指向表尾。那麼,下一次追加hook對象的時候,咱們只須要把新hook對象追加到workInProgressHook對象的後面就行。實際上,上面的代碼能夠拆解爲這樣:

if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState  = hook;
    workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook.next = hook; // 爲何對workInProgressHook.next的賦值可以起到append鏈表的做用呢?這裏須要用到【引用傳遞】的知識來理解。
    workInProgressHook = hook;
  }
複製代碼

這種實現方法的好處是:不要經過遍歷鏈表來找到最後一個元素,以便其後插入新元素(時間複雜度爲O(n))。而是直接插入到workInProgressHook這個元素後面就好(時間複雜度爲O(1))。咱們要知道,常規的鏈表尾部插入是這樣的:

if (currentlyRenderingFiber.memoizedState === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState  = hook;
  } else {
   let currrent = currentlyRenderingFiber.memoizedState;
   while(currrent.next !== null){
   	currrent = currrent.next
   }
   currrent.next = hook;
  }
複製代碼

從時間複雜度的角度來講就是把鏈表插入算法的時間複雜度從O(n)降到O(1)。好,上面稍微展開了一點。到這裏,咱們已經看到react源碼中,是如何實現了第一步和第二步的。

第三步:補充hook對象的信息

咱們在第一步建立的hook對象有不少字段,它們的值都是初始值null。那麼在第三部,咱們就是對這些字段的值進行填充。這些操做的實現代碼都是在mountState函數的裏面:

function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  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];
}
複製代碼

對memoizedState和baseState的填充:

if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
複製代碼

對queue字段的填充:

const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
複製代碼

對next字段的填充實際上是發生在第二步【構建hook單鏈表】,這裏就不贅述了。

以上就是hook對象填充字段信息的過程。不過,值得指出的是,hook對象的queue對象也是在這裏初始化並填充內容的。好比dispatch字段,lastRenderedReducer和lastRenderedState字段等。着重須要提到dispatch方法:

const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
複製代碼

從上面的代碼能夠看到,咱們拿到的dispatch方法實質上是一個引用而已。它指向的是dispatchAction這個方法經過函數柯里化所返回的函數實例。函數柯里化的本質是【閉包】。經過對currentlyRenderingFiberqueue變量的閉包,react能確保咱們調用dispatch方法的時候訪問到的是與之對應的queue對象和currentlyRenderingFiber。

好的,以上就是hook在mount階段所發生的事情。

觸發更新階段

當用戶調用dispatch方法的時候,那麼咱們就會進入【觸發更新階段】。react的源碼中並無這個概念,這是我爲了幫助理解hook的運行原理而提出的。

要想知道觸發更新階段發生了什麼,咱們只須要查看dispatchAction方法的實現就好。可是,dispatchAction方法實現源碼中,參雜了不少跟調度和開發環境相關的代碼。這裏爲了方便聚焦於hook相關的原理,我對源碼進行了精簡:

function dispatchAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A,) {
    const update: Update<S, A> = {
      eventTime,
      lane,
      suspenseConfig,
      action,
      eagerReducer: null,
      eagerState: null,
      next: (null: any),
    };
  
    // 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;
  
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
複製代碼

觸發更新階段,主要發生瞭如下三件事情:

  1. 建立update對象:
const update: Update<S, A> = {
      eventTime,
      lane,
      suspenseConfig,
      action,
      eagerReducer: null,
      eagerState: null,
      next: (null: any),
    };
複製代碼

這裏,咱們只須要關注action和next字段就好。從這裏能夠看出,咱們傳給dispatch方法的任何參數,都是action。

  1. 構建updateQueue循環單向鏈表:
// 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;
複製代碼

從上面的代碼中,能夠看到:

  • updateQueue是一個循環單向鏈表;
  • 鏈表元素插入的順序等同於dispatch方法調用的順序。也就是說最後生成的update對象處於鏈尾。
  • queue.pending這個指針永遠指向鏈尾元素。
  1. 真正地觸發更新

不管是以前的class component時代,仍是如今的function component時代,咱們調用相應的setState()方法或者dispatch()方法的時候,其本質都是向react去請求更新當前組件而已。爲何這麼說呢?由於,從react接收到用戶的更新請求到真正的DOM更新,這中間隔着「千山萬水」。之前,這個「千山萬水」是react的「批量更新策略」,如今,這個「千山萬水」是新加入的「調度層」。

無論怎樣,對於function component,咱們內心得有個概念就是:假如react決定要更新當前組件的話,那麼它的調用棧必定會進入一個叫「renderWithHooks」的函數。就是在這個函數裏面,react纔會調用咱們的function component(再次強調,function component是一個函數)。調用function component,則必定會調用hook。這就意味着hook會進入update階段。

那麼,hook在update階段發生了什麼呢?下面,咱們來看看。

update階段

hook在update階段作了什麼,主要是看updateReducer()這個方法的實現。因爲updateReducer方法實現中,包含了很多調度相關的代碼,如下是我作了精簡的版本:

function updateReducer(reducer,initialArg,init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 拿到更新鏈表的表尾元素
  const last = queue.pending;

  // 獲取最先插入的那個update對象,時刻記住,這是循環鏈表:最後一個的next指向的是第一個元素
  first = last !== null ? last.next : null;

  if (first !== null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // 執行每一次更新,去更新狀態
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = hook.baseState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的狀態和修改狀態的方法
  return [hook.memoizedState, dispatch];
}
複製代碼

hook的update階段,主要發生瞭如下兩件事情:

  1. 遍歷舊的hook鏈,經過對每個hook對象的淺拷貝來生成新的hook對象,並依次構建新的hook鏈。
  2. 遍歷每一個hook對象上的由update對象組成queue循環單鏈表,計算出最新值,更新到hook對象上,並返回給開發者。

updateReducer()方法的源碼,咱們能夠看到,咱們調用了updateWorkInProgressHook()方法來獲得了一個hook對象。就是在updateWorkInProgressHook()方法裏面,實現了咱們所說的第一件事情。下面,咱們來看看updateWorkInProgressHook()的源碼(一樣進行了精簡):

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  invariant(
    nextCurrentHook !== null,
    'Rendered more hooks than during the previous render.',
  );
  currentHook = nextCurrentHook;

  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list.
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    // Append to the end of the list.
    workInProgressHook = workInProgressHook.next = newHook;
  }

  return workInProgressHook;
}
複製代碼

上面在講currentlyRenderingFiber的時候講到,當前已經顯示在屏幕上的component所對應的fiber節點是保存在currentlyRenderingFiber.alternate字段上的。那麼,舊的hook鏈的頭指針無疑就是currentlyRenderingFiber.alternate.memoizedState。而nextCurrentHook變量指向的就是當前準備拷貝的標本對象,currentHook變量指向的是當前舊的hook鏈上已經被拷貝過的那個標本對象。結合這三個語義,咱們不難理解這段代碼:

let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
複製代碼

用一句話來總結就是:若是當前是第一次hook調用,那麼拷貝的標本對象就是舊的hook鏈的第一個元素;不然,拷貝的標本對象就是當前已經拷貝過的那個標本對象的下一個。

下面這一段代碼就是hook對象的淺拷貝:

currentHook = nextCurrentHook;

  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null,
  };
複製代碼

從上面的淺拷貝,咱們能夠想到,hook的mount階段和update階段都是共用同一個queue鏈表。

再往下走,即便新鏈表的構建,幾乎跟mount階段hook鏈表的構建一摸同樣,在這裏就不贅述了:

if (workInProgressHook === null) {
    // This is the first hook in the list.
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    // Append to the end of the list.
    workInProgressHook = workInProgressHook.next = newHook;
  }
複製代碼

到這裏,咱們經過解析updateWorkInProgressHook()的源碼把第一件事情算是講完了。下面,咱們接着來說第二件事情-【遍歷每一個hook對象上的由update對象組成queue循環單鏈表,計算出最新值,更新到hook對象上,並返回給開發者】。相關源碼就在上面給出的精簡版的updateReducer()方法的源碼中。咱們再次把它摳出來,放大講講:

const queue = hook.queue;

  // 拿到queu循環鏈表的表尾元素
  const last = queue.pending;

  // 獲取最先插入的那個update對象,時刻記住,這是循環鏈表:最後一個元素的next指針指向的是第一個元素
  first = last !== null ? last.next : null;

  if (first !== null) {
    let newState = hook.baseState;
    let update = first;
    do {
      // 執行每一次更新,去更新狀態
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);

    hook.memoizedState = hook.baseState = newState;
  }
  const dispatch = queue.dispatch;
  // 返回最新的狀態和修改狀態的方法
  return [hook.memoizedState, dispatch];
複製代碼

首先,拿到queue循環鏈表的第一個元素;

其次,從它開始遍歷整個鏈表(結束條件是:回到鏈表的頭元素),從鏈表元素,也便是update對象上面拿到action,遵循newState = reducer(newState, action);的計算公式,循環結束的時候,也就是最終值被計算出來的時候;

最後,把新值更新到hook對象上,而後返回出去給用戶。

從第二件事件裏面,咱們能夠理解,爲何hook在update階段被調用的時候,咱們傳入的initialValue是被忽略的,以及hook的最新值是如何更新獲得的。最新值是掛載在hook對象上的,而hook對象又是掛載在fiber節點上。當component進入commit階段後,最新值會被flush到屏幕上。hook也所以完成了當前的update階段。

爲何hook的順序如此重要?

在hook的官方文檔:Rules of Hooks中,提到了使用hook的兩大戒律:

  • Only Call Hooks at the Top Level
  • Only Call Hooks from React Functions(component)

若是單純去死記硬背,而不去探究其中的原因,那麼咱們的記憶就不會牢固。如今,咱們既然深究到源碼層級,咱們就去探究一下提出這戒律後面的依據是什麼。

首先,咱們先來解讀一下這兩大戒律究竟是在講什麼。關於第一條,官方文檔已經很明確地指出,之因此讓咱們在函數做用域的頂部,而不是在循環語句,條件語句和嵌套函數裏面去調用hook,目的只有一個:

By following this rule, you ensure that Hooks are called in the same order each time a component renders.

是的,目的就是保證mount階段,第一次update階段,第二update階段......第n次update階段之間,全部的hook的調用順序都是一致的。至於爲何,咱們稍後解釋。

而第二條戒律,說的是隻能在React的function component裏面去調用react hook。這條戒律是顯而易見的啦。你們都知道react hook對標的是class component的相關feature(狀態更新,生命週期函數)的,它確定要跟組件的渲染相掛鉤的,而普通的javascript函數是沒有跟react界面渲染相關聯的。其實這條戒律更準確來講,應該是這樣的:要確保react hook【最終】是在react function component的做用域下面去調用。也就是說,你能夠像俄羅斯套娃那樣,在遵循第一條戒律的前提下去對react hook層層包裹(這些層層嵌套的函數就是custom hook),可是你要確保最外層的那個函數是在react function componnet裏面調用的。

上面的說法依據是什麼呢?那是由於hook是掛載在dispatcher身上的,而具體的dispatcher是在運行時注入的。dispatcher的注入時機是發生在renderWithHook()這個方法被調用的時候,這一點在上面【幾個事實】一節中有提到。從renderWithHook到hook的調用棧是這樣的:

renderWithHook() -> dispatcher注入 -> component() -> hook() -> resolveDispatcher()
複製代碼

那麼,咱們看一眼resolveDispatcher方法的實現源碼就能找到咱們要想找的依據:

function resolveDispatcher() {
    var dispatcher = ReactCurrentDispatcher.current;

    if (!(dispatcher !== null)) {
      {
        throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
      }
    }

    return dispatcher;
  }
複製代碼

也就是說,假如你不在react function component裏面調用hook的話,那麼renderWithHook()這個方法就不會被執行到,renderWithHook()沒有被執行到,也就是說跳過了dispatcher的注入。dispatcher沒有被注入,你就去調用hook,此時dispatcher爲null,所以就會報錯。

以上,就是第二條戒律背後的依據分析。至於第一條戒律,它不斷地在強調hook的調用順序要一致。要想搞懂這個緣由,首先咱們得搞懂什麼是【hook的調用順序】?

什麼是hook的調用順序?

答案是:「hook在【mount階段】的調用順序就是hook的調用順序」。也就是說,咱們判斷某一次component render的時候,hook的調用順序是否一致,參照的是hook在mount階段所肯定下來的調用順序。舉個例子:

// mount階段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

//第一次update階段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

//第二次update階段
const [age,setAge] = useState(28)
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
複製代碼

參照mount階段所肯定的順序:useState(0) -> useState('sam') -> useState(28),第一次update階段的hook調用順序是一致的,第二次update階段的hook調用順序就不一致了。

總而言之,hook的調用順序以mount階段所確立的順序爲準。

關於hook的調用順序的結論

首先,二話不說,咱們先下兩個有關於hook調用順序的結論:

  1. hook的【調用次數】要一致。多了,少了,都會報錯。
  2. 最好保持hook的【調用位置】是一致的。

其實,通過思考的同窗都會知道,調用順序一致能夠拆分了兩個方面:hook的數量一致,hook的調用位置要一致(也就是相同的那個hook要在相同的次序被調用)。

官方文檔所提出的的hook的調用順序要一致,這是沒問題的。它這麼作,既能保證咱們不出錯,又能保證咱們不去作那些無心義的事情(改變hook的調用位置意義不大)。

可是,從源碼的角度來看,hook的調用位置不一致並不必定會致使程序出錯。假如你知道怎麼承接調用hook所返回的引用的話,那麼你的程序還會照常運行。之因此探討這一點,是由於我要打惟官方文檔論的迷信思想,加深【源碼是檢驗對錯的惟一標準】的認知。

首先,咱們來看看,爲何能下第一條結論。咱們先看看hook的調用次數多的狀況。假如咱們有這樣的代碼:

// mount階段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)

//第一次update階段
const [count,setCount] = useState(0)
const [name,setName] = useState('sam')
const [age,setAge] = useState(28)
const [sex,setSex] = useState('男')
複製代碼

通過mount階段,咱們會獲得這樣的一條hook鏈:

=============           =============             =============
| count Hook |  ---->   | name Hook  |  ---->     | age Hook  | -----> null
=============           =============             =============
複製代碼

上面也提到了,update階段的主要任務之一就是遍歷舊的hook鏈去建立新的hook鏈。在updateWorkInProgressHook方法裏面,在拷貝hook對象以前的動做是要計算出當前要拷貝的hook對象,也就是變量nextCurrentHook的值:

let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
複製代碼

假設,如今咱們來到了update階段的第四次hook調用,那麼代碼會執行到nextCurrentHook = currentHook.next;。currentHook是上一次(第三次)被成功拷貝的對象,而且是存於舊鏈上。由於舊的hook鏈只有三個hook對象,那麼此時currentHook對象已是最後一個hook對象了,currentHook.next的值天然是爲null了。也就是說當前準備拷貝的hook對象(nextCurrentHook)是爲null的。咱們的斷言失敗,程序直接報錯:

// 假如咱們斷言失敗,則會拋出錯誤
  invariant(
    nextCurrentHook !== null,
    'Rendered more hooks than during the previous render.',
  );
複製代碼

以上就是hook調用次數多了會報錯的狀況。下面,咱們來看看hook調用次數少了的狀況。咱們直接關注renderWithHooks方法裏面的這幾行代碼:

export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any {
  // ....
  let children = Component(props, secondArg);
  // ....
  const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
  // .....
  invariant(
    !didRenderTooFewHooks,
    'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',
  );

  return children;
}
複製代碼

這裏的Component就是咱們hook調用所在的function component。解讀上面所給出的代碼,咱們能夠得知:若是全部的hook都調用完畢,你那個全局變量currentHook的next指針還指向別的東西(非null值)的話,那麼證實update階段,hook的調用次數少了,致使了next指針的移動次數少了。若是hook的調用次數是同樣的話,那麼此時currentHook是等於舊的hook鏈上的最後一個元素,咱們的斷言就不會失敗。

從上面的源碼分析,咱們能夠得知,hook的調用次數是不能多,也不能少了。由於多了,少了,react都會報錯,程序就中止運行。

最後,咱們來看看結論二。

爲了證實個人觀點是對的,咱們直接來運行下面的示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>react hook</title>
</head>
<body>
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script></script>
    <script> window.onload = function(){ let _count = 0; const { useState, useEffect, createElement } = React const root = document.getElementById('root') let count,setState let name,setName function Counter(){ if(_count === 0){ // mount階段 const arr1= useState(0) count = arr1[0] setState = arr1[1] const arr2 = useState('sam') name = arr2[0] setName = arr2[1] }else if(_count >= 1){ // update階段 const arr1 = useState('little sam') count = arr1[0] setState = arr1[1] const arr2 = useState(0) name = arr2[0] setName = arr2[1] } _count += 1 return createElement('button',{ onClick:()=> { setState(count=> count + 1) } },count) } ReactDOM.render(createElement(Counter),root) } </script>
</body>
</html>
複製代碼

直接運行上面的例子,程序是會正常運行的,界面的顯示效果也是正確的。從而佐證了個人結論是正確的。那爲何呢?那是由於在update階段,要想正確地承接住hook調用所返回的引用,hook的名字是不重要的,重要的是它的位置。上面update階段,雖然hook的調用的位置是調換了,可是咱們知道第一個位置的hook對象仍是指向mount階段的count hook對象,因此,我仍是能正確地用它所對應的變量來承接,因此,後面的render也就不會出錯。

以上示例僅僅是爲了佐證個人第二個結論。實際的開發中,咱們不會這麼幹。或者說,目前我沒有遇到必須這麼作的開發場景。

以上就是從源碼角度去探索react hook的調用順序爲何這麼重要的緣由。

其餘hook

上面只是拿useState和useReducer這兩個hook做爲本次講解react hook原理的樣例,還有不少hook沒有涉及到,好比說十分重要的useEffect就沒有講到。可是,若是你深刻到hook的源碼(react/packages/react-reconciler/src/ReactFiberHooks.new.js)中去看的話,幾乎全部的都有如下共性:

  • 都有mount階段和update階段
  • 在mount階段,都會調用mountWorkInProgressHook()來生成hook對象;在update階段,都會調用updateWorkInProgressHook()來拷貝生成新的hook對象。這就意味着,相同階段,無論你是什麼類型的hook,你們都是處在同一個hook鏈身上
  • 每一個hook都對應一個hook對象,不一樣類型的hook的不一樣之處主要體如今它們掛載在hook.memoizedState字段上的值是不一樣的。好比說,useState和useReducer掛載的是state(state的數據結構由咱們本身決定),useEffect掛載的是effect對象,useCallback掛載的是由callback和依賴數組組成的數組等等。

而在其餘的hook中,useEffect過重要了,又相對不同,無疑是最值得咱們深刻探索的hook。假若有時間,下一篇文章不妨探索探索它的運行原理。

總結

參考資料

相關文章
相關標籤/搜索