React Hooks原理探究,看完不懂,你打我

概覽

React 中一般使用 類定義 或者 函數定義 建立組件:html

在類定義中,咱們可使用到許多 React 特性,例如 state、 各類組件生命週期鉤子等,可是在函數定義中,咱們卻無能爲力,所以 React 16.8 版本推出了一個新功能 (React Hooks),經過它,能夠更好的在函數定義組件中使用 React 特性。node

好處:react

一、跨組件複用: 其實 render props / HOC 也是爲了複用,相比於它們,Hooks 做爲官方的底層 API,最爲輕量,並且改形成本小,不會影響原來的組件層次結構和傳說中的嵌套地獄;git

二、類定義更爲複雜: 不一樣的生命週期會使邏輯變得分散且混亂,不易維護和管理; 時刻須要關注this的指向問題; 代碼複用代價高,高階組件的使用常常會使整個組件樹變得臃腫;github

三、狀態與UI隔離: 正是因爲 Hooks 的特性,狀態邏輯會變成更小的粒度,而且極容易被抽象成一個自定義 Hooks,組件中的狀態和 UI 變得更爲清晰和隔離。算法

注意:編程

避免在 循環/條件判斷/嵌套函數 中調用 hooks,保證調用順序的穩定; 只有 函數定義組件 和 hooks 能夠調用 hooks,避免在 類組件 或者 普通函數 中調用; 不能在useEffect中使用useState,React 會報錯提示; 類組件不會被替換或廢棄,不須要強制改造類組件,兩種方式能並存;redux

重要鉤子:數組

  • useState: 用於定義組件的 State,對標到類組件中this.state的功能
  • useEffect:經過依賴觸發的鉤子函數,經常使用於模擬類組件中的componentDidMount,componentDidUpdate,componentWillUnmount方法
  • 其它內置鉤子:useContext: 獲取 context 對象
  • useReducer: 相似於 Redux 思想的實現,但其並不足以替代 Redux,能夠理解成一個組件內部的 redux,並非持久化存儲,會隨着組件被銷燬而銷燬;屬於組件內部,各個組件是相互隔離的,單純用它並沒有法共享數據;配合useContext的全局性,能夠完成一個輕量級的 Redux
  • useCallback: 緩存回調函數,避免傳入的回調每次都是新的函數實例而致使依賴組件從新渲染,具備性能優化的效果;
  • useMemo: 用於緩存傳入的 props,避免依賴的組件每次都從新渲染;
  • useRef: 獲取組件的真實節點;
  • useLayoutEffect:DOM更新同步鉤子。用法與useEffect相似,只是區別於執行時間點的不一樣。useEffect屬於異步執行,並不會等待 DOM 真正渲染後執行,而useLayoutEffect則會真正渲染後才觸發;能夠獲取更新後的 state;
  • 自定義鉤子(useXxxxx): 基於 Hooks 能夠引用其它 Hooks 這個特性,咱們能夠編寫自定義鉤子。

利用數組模擬useState的實現原理

咱們可使用Array模擬useState的原理,正如文章React hooks: not magic, just arrays所說,可是React底層真實的實現,是利用的鏈表,這裏咱們下面會說到瀏覽器

當調用 useState 的時候,會返回形如 (變量, 函數) 的一個元祖。而且 state 的初始值就是外部調用 useState 的時候,傳入的參數。

理清楚了傳參和返回值,再來看下 useState 還作了些什麼。正以下面代碼所示,當點擊按鈕的時候,執行setNum,狀態 num 被更新,而且 UI 視圖更新。顯然,useState 返回的用於更改狀態的函數,自動調用了render方法來觸發視圖更新。

function App() {
  const [num, setNum] = useState(0);

  return (
    <div> <div>num: {num}</div> <button onClick={() => setNum(num + 1)}>加 1</button> </div>
  );
}

複製代碼

初步模擬

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
}

let state;

function useState(initialState){
  state = state || initialState;

  function setState(newState) {
    state = newState;
    render();
  }

  return [state, setState];
}

render(); // 首次渲染
複製代碼

初步模擬讓咱們發現了Hooks的第一個核心原理:閉包,是的Hooks返回的statesetState方法,在hooks內部都是利用閉包實現的

可是真實的useXXX都是能夠屢次聲明使用的,因此咱們這裏的初步實現並不支持對多個變量聲明

爲何不能在循環、判斷內部使用Hook

首先,利用Array模擬React Hook原理

前面 useState 的簡單實現裏,初始的狀態是保存在一個全局變量中的。以此類推,多個狀態,應該是保存在一個專門的全局容器中。這個容器,就是一個樸實無華的 Array 對象。具體過程以下:

  • 第一次渲染時候,根據 useState 順序,逐個聲明 state 而且將其放入全局 Array 中。每次聲明 state,都要將 cursor 增長 1。
  • 更新 state,觸發再次渲染的時候。cursor 被重置爲 0。按照 useState 的聲明順序,依次拿出最新的 state 的值,視圖更新。

舉例:

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi");
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}
複製代碼

上面代碼的建立流程

1)初始化

建立兩個Array, setters and state,設置遊標cursor = 0;

hooks_create_1.png

2) 首次渲染

遍歷全部的useState,將setterspush進入數組,將statepush進入狀態數組

hooks_create_2.png

3)重渲染

後續的每次重渲染都會重置遊標cursor = 0,並依次從數組中取出以前的state

4)事件觸發

每一個事件都有對應遊標的state值,任何state事件觸發,都會修改state數組中對應的state值

hooks_create_3.png

完整模擬useState

import React from "react";
import ReactDOM from "react-dom";

const states = [];
let cursor = 0;

function useState(initialState) {
  const currenCursor = cursor;
  states[currenCursor] = states[currenCursor] || initialState; // 檢查是否渲染過

  function setState(newState) {
    states[currenCursor] = newState;
    render();
  }

  cursor+=1; // 更新遊標
  return [states[currenCursor], setState];
}

function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(1);

  return (
    <div> <div>count1: {count1}</div> <div> <button onClick={() => setCount1(count1 + 1)}>add1 1</button> <button onClick={() => setCount1(count1 - 1)}>delete1 1</button> </div> <hr /> <div>num2: {num2}</div> <div> <button onClick={() => setCount2(count2 + 1)}>add2 1</button> <button onClick={() => setCount2(count2 - 1)}>delete2 1</button> </div> </div>
  );
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  cursor = 0; // 重置cursor
}

render(); // 首次渲染
複製代碼

若是在循環,判斷中使用Hooks

let firstRender = true;

function RenderFunctionComponent() {
  let initName;
  
  if(firstRender){
    [initName] = useState("Rudi");
    firstRender = false;
  }
  const [firstName, setFirstName] = useState(initName);
  const [lastName, setLastName] = useState("Yardley");

  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}
複製代碼

建立流程示意圖

hooks_create_4.png

重渲染

hooks_create_5.png

能夠看到,由於firstRender的條件判斷,遊標爲0的state值會被異常設置成useState(initName),遊標爲1的state會被異常設置成useState("Yardley"),而其實Yardley是遊標爲2的state的值

也就是說初始化組件的時候,hooks會直接維護一套數組,對應相應的state和setState方法,若是在條件渲染中使用,會致使重渲染的時候,異常的遊標對應,異常的遊標對應也會致使調用的setState方法失效

模擬useEffect的實現原理

useEffect多是咱們在使用hooks的時候,使用頻率僅次於useState的的鉤子方法了,它的做用是反作用,說直白就是某些state或者props變化的時候,須要監聽並執行相應的操做,那麼咱們就須要使用useEffect了,對標就是Class組件中的componentDidMount,componentDidUpdate,componentWillUnmount方法的集合

模擬實現(依然是利用Array + Cursor的思路)

const allDeps = [];
let effectCursor = 0;

function useEffect(callback, deps = []) {
  if (!allDeps[effectCursor]) {
    // 初次渲染:賦值 + 調用回調函數
    allDeps[effectCursor] = deps;
    effectCursor+=1;
    callback();
    return;
  }

  const currenEffectCursor = effectCursor;
  const rawDeps = allDeps[currenEffectCursor];
  // 檢測依賴項是否發生變化,發生變化須要從新render
  const isChanged = rawDeps.some(
    (dep,index) => dep !== deps[index]
  );
  // 依賴變化
  if (isChanged) {
    // 執行回調
    callback();
    // 修改新的依賴
    allDeps[effectCursor] = deps;
  }
  // 遊標遞增
  effectCursor+=1;
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  effectCursor = 0; // 注意將 effectCursor 重置爲0
}
複製代碼

真實的React實現

咱們用數組模擬出了Hooks的實現原理,可是React的真實實現是用單鏈表的形式代替數組的,經過next串聯起全部的hook

首先讓咱們看一張圖

hooks_6.png

Dispatcher

dispatcher 是一個包含了 hooks 函數的共享對象。它將基於 ReactDOM 的渲染階段被動態地分配或清理,而且它將確保用戶沒法在React組件外訪問到Hooks,源碼參考

hooks在啓用時被一個叫作enableHooks 的標誌位變量啓用或禁用,在渲染根組件時,判斷該標誌位並簡單的切換到合適的 dispatcher 上,源碼參考

部分源碼

function renderRoot(root: FiberRoot, isYieldy: boolean): void {
  invariant(
    !isWorking,
    'renderRoot was called recursively. This error is likely caused ' +
      'by a bug in React. Please file an issue.',
  );

  flushPassiveEffects();

  isWorking = true;
  // 控制hooks的當前Dispatcher
  if (enableHooks) {
    ReactCurrentOwner.currentDispatcher = Dispatcher;
  } else {
    ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
  }
  
  ...
複製代碼

當完成渲染後,dispatcher將被置爲null,這是爲了防止在ReactDOM的渲染外被異常訪問,源碼參考

部分源碼

// We're done performing work. Time to clean up.
  isWorking = false;
  ReactCurrentOwner.currentDispatcher = null;
  resetContextDependences();
  resetHooks();
複製代碼

在Hooks內部,使用resolveDispatcher方法解析當前的dispatcher引用,若是當前的dispatcher異常,則會報錯

部分源碼

function resolveDispatcher() {
  const dispatcher = ReactCurrentOwner.currentDispatcher;
  invariant(
    dispatcher !== null,
    'Hooks can only be called inside the body of a function component.',
  );
  return dispatcher;
}
複製代碼

真正的Hooks

能夠說Dispatcher是Hooks機制下的對外統一暴露控制器,渲染過程當中,經過flag標誌控制當前的上下文dispatcher,核心意義就是嚴格控制hooks的調用渲染,防止hooks在異常的地方被調用了

hooks queue

hooks的表現是:按照調用順序被連接在一塊兒的節點(nodes)。總結一下hooks的一些屬性

  • 初次渲染建立初始狀態
  • 狀態值能夠被更新
  • React會在重渲染後,記住以前的狀態值
  • React會按照調用順序,取得和更新正確的狀態
  • React知道當前的hook是屬於哪一個fiber的

因此咱們看Hooks的時候,就不能單純的認爲每一個hook節點是一個對象,而是一個鏈表節點,而整個hooks模型,則是一個隊列

{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'baz',
      next: null
    }
  }
}
複製代碼

咱們能夠看到源碼對於一個Hook和Effect模型的定義,源碼

export type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any> | null,
  queue: UpdateQueue<any> | null,
  next: Hook | null,
};

type Effect = {
  tag: HookEffectTag,
  create: () => mixed,
  destroy: (() => mixed) | null,
  inputs: Array<mixed>,
  next: Effect,
};

...

export function useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
  return useReducer(
    basicStateReducer,
    // useReducer has a special case to support lazy useState initializers
    (initialState: any),
  );
}
複製代碼

首先,能夠看到useState的實現就是useReducer的某一種狀況的實現,因此在官方文檔上,也說了useReducer是useState的另一種實現方案,結合了Redux的思想,能夠避免過多的傳遞迴調函數,而能夠直接傳遞dispatch到深層次的組件中去 官網關於useReducer的說明

這裏我仍是貼上關於useReducer的用法案例,其實主要是能理解redux或者dva的原理和用法,就能夠對標useReducer的用法

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </>
  );
}
複製代碼

回到Hook的定義,咱們如今就能夠對每一個參數進行說明了

  • memoizedState:hook更新後的緩存state
  • baseState:初始化initialState
  • baseUpdate:最近一次調用更新state方法的action
  • queue:調度操做的隊列,等待進入reducer
  • next:link到下一個hook,經過next串聯每一個hook

結合fiber看hooks

對於fiber實現,這裏不作詳細解釋,咱們這裏只須要知道,React在V16中,對組件構建渲染的機制,從模式改成了fiber模式,變成了具備鏈表和指針的單鏈表樹遍歷算法。經過指針映射,每一個單元都記錄着遍歷當下的上一步和下一步,從而使遍歷變得能夠被暫停或者重啓 這裏的理解就是一種任務分割調度算法,將原先同步更新渲染的任務分割成一個個獨立的小任務單位,根據不一樣的優先級,將小任務分散到瀏覽器的空閒時間執行,充分利用主進程的時間循環機制

fiber簡單說概念,就是組件渲染的一個基礎任務切割單元,裏面包含了當前組件構建的最基礎的一個任務內容單元

其中,要提到一個很重要的概念memoizedState,這個字段是否是很眼熟,上面關於hook的定義裏面,也有這個字段,是的,fiber數據結構中,也有這個字段,在fiber中,memoizedState的意義就是指向屬於這個fiber的hooks隊列的首個hook,而hook中的memoizedState則指的是當前hook緩存的state值(這裏筆者在看一些博客的時候,發現有的博主把兩個數據結構中的同名字段搞混淆了)

咱們能夠看源碼

// There's no existing queue, so this is the initial render.
  if (reducer === basicStateReducer) {
    // Special case for `useState`.
    if (typeof initialState === 'function') {
      initialState = initialState();
    }
  } else if (initialAction !== undefined && initialAction !== null) {
    initialState = reducer(initialState, initialAction);
  }
  // 注意:重點
  workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
複製代碼

上面能夠看到,initialState做爲初始state值,被同時賦值給了baseStatememoizedState

再看三段段段源碼,源碼連接

// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let firstCurrentHook: Hook | null = null;
let currentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
複製代碼
export function prepareToUseHooks( current: Fiber | null, workInProgress: Fiber, nextRenderExpirationTime: ExpirationTime, ): void {
  if (!enableHooks) {
    return;
  }
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
  firstCurrentHook = current !== null ? current.memoizedState : null;

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

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

  // isReRender = false;
  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;
}
複製代碼
export function finishHooks( Component: any, props: any, children: any, refOrContext: any, ): any {
  if (!enableHooks) {
    return children;
  }

  // This must be called after every function component to prevent hooks from
  // being used in classes.

  while (didScheduleRenderPhaseUpdate) {
    // Updates were scheduled during the render phase. They are stored in
    // the `renderPhaseUpdates` map. Call the component again, reusing the
    // work-in-progress hooks and applying the additional updates on top. Keep
    // restarting until no more updates are scheduled.
    didScheduleRenderPhaseUpdate = false;
    numberOfReRenders += 1;

    // Start over from the beginning of the list
    currentHook = null;
    workInProgressHook = null;
    componentUpdateQueue = null;

    children = Component(props, refOrContext);
  }
  renderPhaseUpdates = null;
  numberOfReRenders = 0;

  const renderedWork: Fiber = (currentlyRenderingFiber: any);

  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  firstCurrentHook = null;
  currentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;
  
  ...
複製代碼

其中一段源碼有這麼一段註釋:Hooks are stored as a linked list on the fiber's memoizedState field.,大概意思是Hooks是以鏈表的形式儲存在fibermemoizedState字段中

第二段代碼是fiber中,hook執行前置函數

第三段代碼是fiber中,hook執行後置函數,方法中有這麼一句renderedWork.memoizedState = firstWorkInProgressHook;

因此咱們來總結一下

1)Hook數據結構中和fiber數據結構中都有memoizedState字段,可是表達的意義不一樣,Hook中是做爲緩存的state值,可是fiber中是指向的當前fiber下的hooks隊列的首個hook(hook是鏈表結構,指向首個,就意味着能夠訪問整個hooks隊列)

2)fiber中調用hook的時候,會先調用一個前置函數,其中 currentlyRenderingFiber = workInProgress;

firstCurrentHook = current !== null ? current.memoizedState : null;

這兩句代碼分別將當前渲染的fiber和當前執行的hooks隊列的首個hook賦值給了當前的全局變量currentlyRenderingFiberfirstCurrentHook

再看下關於currentlyRenderingFiber變量的源碼說明

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

currentlyRenderingFiber就是定義當前正在渲染中的fiber結構

3)fiber調用hooks結束的時候,會調用finishHooks方法,能夠看到,會將當前fiber的memoizedState字段存入firstWorkInProgressHook,也就是將hooks隊列的首個hook存入,而後將currentlyRenderingFiber字段置爲null

Class or Hooks

從當下的環境來看,Hooks已經逐漸成爲主流組件方式,好比Ant4.x的組件,已經全面推薦Hooks模式,Hooks的有點主要在於精簡的編碼模式,函數式編程思想,而Calss組件的主要優勢在於【完整】,【精準】的組件流程控制,包括可使用shouldComponentUpdate等生命週期對渲染作嚴格控制

Class組件

在業務開發時的思考模式是:【先作什麼,再作什麼】,this.setState第二個回調的參數,就是這種思想的絕對體現,而後配合【生命週期函數】完成一整個組件的功能,對於組件封裝和複用的角度,HOC模式也必須依賴Class實現

Hooks

對標Class組件,使用Hooks須要有一個編程思路上的轉變,Hooks的業務開發的思考模式是:【依賴】,【反作用】

全部的狀態維護好後,須要思考的就是圍繞這些狀態產生的【反作用】,個人什麼state或者props變了以後,對應的【反作用】須要幹什麼事,也是這種設計理念下,useEffect的功能能夠直接對標Class組件的componentDidMountcomponentDidUpdatecomponentWillUnmount方法的集合

可是Class在目前來講任有不可替代性,由於Class擁有完整的生命週期控制,好比shouldComponentUpdate等生命週期,而Hooks則沒法作到如此精細化的控制

經過 Hooks 咱們能夠對 state 邏輯進行良好的封裝,輕鬆作到隔離和複用,優勢主要體如今:

  • 複用代碼更容易:hooks 是普通的 JavaScript 函數,因此開發者能夠將內置的 hooks 組合處處理 state 邏輯的自定義 hooks中,這樣複雜的問題能夠轉化一個單一職責的函數,並能夠被整個應用或者 React 社區所使用;
  • 使用組合方式更優雅:不一樣於 render props 或高階組件等的模式,hooks 不會在組件樹中引入沒必要要的嵌套,也不會受到 mixins 的負面影響;
  • 更少的代碼量:一個 useEffect 執行單一職責,能夠幹掉生命週期函數中的重複代碼。避免將同一職責代碼分拆在幾個生命週期函數中,更好的複用能力能夠幫助優秀的開發者最大限度下降代碼量;
  • 代碼邏輯更清晰:hooks 幫助開發者將組件拆分爲功能獨立的函數單元,輕鬆作到「分離關注點」,代碼邏輯更加清晰易懂;

【Tips:function組件沒有實例,因此沒法經過ref控制,可是Hooks下可使用React.forwardRef來將ref傳遞給function組件,而後經過useImperativeHandle將子組件的實例操做,選擇性的暴露給父組件】

參考:

[譯] React Hooks 底層解析

React Hooks 原理

Under the hood of React’s hooks system

React-ReactFiberHooks源碼

相關文章
相關標籤/搜索