揭開react hook神祕面紗

前言

一開始react團隊對外宣佈hook 的時候,一眼看上去,以爲確定proxy或者getter實現的,而後在函數組件外面包一層class extend React.Component。讀setState鉤子的第一個return結果就返回state,第二個結果就是封裝了setState。後來發佈了,看了一下代碼,原來是維護一個隊列(能夠說很像數組,也能夠說維護一個鏈表)。javascript

renderWithHooks的整個過程

在源碼裏面,renderWithHooks函數是渲染一個組件會調用的,跟hook相關的操做都在這裏以後。 好比這段代碼:java

export default () => {
    const [n, setn] = useState(1);
    const [age, setAge] = useState(10);
    const [man, setSex] = useState(true);
    return (
        <div> 這是n:{n}<button onClick={() => setn(n + 1)}>n++</button> 年齡:{age}<button onClick={() => setAge(age + 2)}>age+2</button> 變性:{man ? 'man' : 'gay'}<button onClick={() => setSex(!man)}>change</button> </div>
    )
}
複製代碼

第一次掛載組件的時候,會給對應的state留下一個dispatch接口,這個接口是操做對應的state的,也就是setn、setAge、setSex,以return [initState, dispatch]這種形式返回。後面的更新,每次點擊都會讓整個組件函數從新執行,3次useState,源碼內部的實現是維護一個隊列,setter和對應的state是一一對應的:react

編號 state dispatch函數
1 _n setn_function
2 _age setAge_function
3 _sex setSex_function

下劃線開頭表示react hook內部維持的狀態, _function表示react hook內部暴露出來的改變該狀態的函數,這兩個只要第一次mount以後就會固定。之後每次更新,也是根據hook從頭至尾執行,並根據第幾個hook來拿到表裏面的第幾個state和它的dispatch函數數組

爲何要順序調用hook

官方有句話,必須順序調用hook。衍生的其餘規則:不要在if條件判斷中使用hook、必須在函數組件內使用hook、不要在循環中使用hook(其實只要保證循環每次都徹底同樣仍是能夠的)閉包

若是咱們就是這樣不按照套路使用的話,好比代碼裏面因爲某種條件判斷,使得咱們第二次調用組件函數的時候usestate的順序不同,僞代碼:dom

// 第一次
    const [n, setn] = useState(1);
    const [age, setAge] = useState(10);
    const [man, setSex] = useState(true);

// 第二次
    const [age, setAge] = useState(10);
    const [n, setn] = useState(1);
    const [man, setSex] = useState(true);
複製代碼

第一次:異步

編號 state dispatch函數 hook調用
1 _n setn_function const [n, setn] = useState(1);
2 _age setAge_function const [age, setAge] = useState(10);
3 _sex setSex_function const [man, setSex] = useState(true);

此時每個hook的第二個參數都對應本身的set__function。函數

第二次:ui

編號 state dispatch函數 hook調用
1 _n setn_function const [age, setAge] = useState(10);
2 _age setAge_function const [n, setn] = useState(1);
3 _sex setSex_function const [man, setSex] = useState(true);

這下問題來了:spa

return (
        <div> {/* setn指的是setAge_function,點了會改變_age。這裏的n展現的是_age */} 這是n:{n}<button onClick={() => setn(n + 1)}>n++</button> {/* setAge指的是setn_function,點了會改變_n。這裏的age展現的是_n */} 年齡:{age}<button onClick={() => setAge(age + 2)}>age+2</button> 變性:{man ? 'man' : 'gay'}<button onClick={() => setSex(!man)}>change</button> </div>
    )
複製代碼

點了一次後,n展現了age,age展現了n,可是他們邏輯就正常,該+1就+1,該+2就+2。其實,能夠經過代碼讓這種狀況不出現bug,只是,爲了讓一個不合法操做正常,加上hack代碼,同事兩行淚啊。

再來一個反例,若是第二次調用組件函數的時候,前面少調用一個hook。第一次仍是同樣,第二次:

編號 state dispatch函數 hook調用
1 _n setn_function const [age, setAge] = useState(10);
2 _age setAge_function const [man, setSex] = useState(true);

這時候,一眼看上去,setAge實際上就是改變n,setSex改變的是age。可是事實上,後面若是少了hook會報錯

從renderWithHooks開始

來到react-dom源碼裏面,crtl+f找到renderWithHooks:

function renderWithHooks(current, workInProgress, Component, props, refOrContext) {
  currentlyRenderingFiber$1 = workInProgress;
  // 第一次的狀態
  firstCurrentHook = nextCurrentHook = current !== null ? current.memoizedState : null;

  // 一些變量初始值:
  // currentHook = null;
  // workInProgressHook = null;

  // didScheduleRenderPhaseUpdate = false; 是否正在update中
  // numberOfReRenders = 0; 從新渲染的時候fiber節點數

  {
  // 第一次HooksDispatcherOnMountInDEV,第二次之後都是HooksDispatcherOnUpdateInDEV
  // firstCurrentHook就在前面被賦值一次,而nextCurrentHook會被賦值爲當前存放hook的對象裏面
    ReactCurrentDispatcher$1.current = nextCurrentHook === null ? HooksDispatcherOnMountInDEV : HooksDispatcherOnUpdateInDEV;
  }

  var children = Component(props, refOrContext);

  // 從新渲染的時候
  if (didScheduleRenderPhaseUpdate) {
    do {
      didScheduleRenderPhaseUpdate = false;
      numberOfReRenders += 1;

      // 從新渲染就是HooksDispatcherOnUpdateInDEV了
      ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;

      children = Component(props, refOrContext);
    } while (didScheduleRenderPhaseUpdate);
    numberOfReRenders = 0;
  }
  return children;
}
複製代碼

能夠看見,renderWithHooks對於首次掛載組件走的是HooksDispatcherOnMountInDEV相關的邏輯,之後的更新從新渲染走的是HooksDispatcherOnUpdateInDEV裏面的邏輯

current

ReactCurrentDispatcher$1.current會是HooksDispatcherOnMountInDEV和HooksDispatcherOnUpdateInDEV其中一個,它們都是一個存放全部的hook函數的對象。首先,咱們看一下第一次掛載的時候使用的HooksDispatcherOnMountInDEV裏面的useState:

HooksDispatcherOnMountInDEV = {
    // ....
    useState: function (initialState) {
      // ...已省略了容錯代碼
      currentHookNameInDev = 'useState';
      return mountState(initialState);
    },
}

// mountState函數
function mountState(initialState) {
  // 獲取當前的hook對象
  var hook = mountWorkInProgressHook();
  // 初始化的state以及狀態記憶
  hook.memoizedState = hook.baseState = initialState;
  // 之後用的dispatch函數就在hook.queue裏面
  var queue = hook.queue = {
    last: null,
    dispatch: null,
    eagerReducer: basicStateReducer,
    eagerState: initialState
  };
  // 暴露出來修改state的dispatch,也是更新狀態的核心,具體原理這裏不探究
  var dispatch = queue.dispatch = dispatchAction.bind(null,
  currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}
複製代碼

這就是第一步了,把初始狀態和dispatch存進去。後面的更新,當前的dispatcher用的是HooksDispatcherOnUpdateInDEV裏面的hook:

HooksDispatcherOnMountInDEV = {
    // ....
    useState: function (initialState) {
      // ...
      currentHookNameInDev = 'useState';
      return updateState(initialState);
      },
}

function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

function updateReducer(reducer, initialArg, init) {
  // 獲取當前hook
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;

  // 若是是從新渲染的過程,返回被dispatchAction函數修改過的新state和dispatch函數
  if (numberOfReRenders > 0) {
    var _dispatch = queue.dispatch;
    return [hook.memoizedState, _dispatch];
  }
複製代碼

updateWorkInProgressHook如何進行

一個hook對象是這樣的:

每一次hook都是用updateWorkInProgressHook獲取的。也是這個函數的實現,讓咱們看起來react內部是用一個數組維護了hook

function updateWorkInProgressHook() {
    // nextCurrentHook是前面的renderWithHooks賦值的
    currentHook = nextCurrentHook;

    var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      queue: currentHook.queue,
      baseUpdate: currentHook.baseUpdate,
      next: null
    };

   // 取下一個,就像遍歷同樣
    if (workInProgressHook === null) {
      // 第一次執行組件函數,最開始沒有in progress的hook
      workInProgressHook = firstWorkInProgressHook = newHook;
    } else {
      // 更新的時候
      workInProgressHook = workInProgressHook.next = newHook;
    }
    nextCurrentHook = currentHook.next;
    return workInProgressHook;
}
複製代碼

手動模擬更新還原過程

咱們仍是繼續在咱們的例子上面改。首先,先用最簡單的方法實現一個low一點的hook:

let state = []; // 存放useState的第一個返回值,狀態
let dispatchers = [];  // 存放useState的第二個返回值,dispatch函數
let cursor = 0;  // 遍歷用的遊標,替代next方便理解

function _useState(initState) {
// 第一次來就是賦值,後面來就是靠遊標讀取
  const dispatcher = dispatchers[cursor] ||
    (
      dispatchers[cursor] = (_cursor => newVal => { // 大家熟悉的閉包
        state[_cursor] = newVal;
        console.log('修改的新state>>', state);
      })(cursor),
      dispatchers[cursor]
    );

// 第一次來仍是賦值,後面來就是靠遊標直接讀取
  const value = (state[cursor]) || (state[cursor] = initState, state[cursor]);

  cursor++;
  return [value, dispatcher];
}
複製代碼

就這麼簡單,極其簡單版本hook。可是咱們要模擬react裏面的從新渲染更新,須要動一點手腳:

根組件就是HookIsHere組件
export default () => {
  return <HookIsHere />; } 複製代碼

脫離了react環境的簡易hook,若是用在HookIsHere組件中,須要手動模擬更新過程:

function HookIsHere() {
  updateHooks(); // react每次更新,都會跑徹底部hook,咱們用這個函數模擬
  return (
    <div> 修改n:<button onClick={() => { re_render(); // 表示從新渲染 const [n, setn] = updateHooks()[0]; setn(n + 1); }}>n++</button> 修改年齡:<button onClick={() => { re_render(); const [age, setAge] = updateHooks()[1]; setAge(age + 2); }}>age+2</button> 變性:<button onClick={() => { re_render(); const [man, setSex] = updateHooks()[2]; setSex(!man); }}>change</button> </div>
  );
}

// 首次掛載組件
function mountComponent() {
  console.log('模擬首次mount')
  return <HookIsHere />; // 會被render返回的,這裏只是模擬並無什麼卵用 } // 封裝一下,能讓咱們每次更新ui能夠從新把函數組件全部的useState運行一次 // 脫離react自身環境實現的簡易版本,只能經過這種方法模擬更新 function updateHooks() { return [ _useState(1), _useState(10), _useState(true) ] } // 每次從新渲染,遊標固然是清零的 function re_render() { cursor = 0; } console.log(state); // mount前 mountComponent(); // mount了 setTimeout(() => { console.log(state); // react有異步渲染的,如今能夠看見初始狀態 }); 複製代碼

打開控制檯,能夠看見咱們的本身造的hook跑起來了的console

所有代碼:

import React from 'react';

let state = [];
let dispatchers = [];
let cursor = 0;

function _useState(initState) {
  const dispatcher = dispatchers[cursor] ||
    (
      dispatchers[cursor] = (_cursor => newVal => {
        state[_cursor] = newVal;
        console.log('修改的新state>>', state, dispatchers);
      })(cursor),
      dispatchers[cursor]
    );
  const value = (state[cursor]) || (state[cursor] = initState, state[cursor]);

  cursor++;
  return [value, dispatcher];
}

function updateHooks() {
  return [
    _useState(1),
    _useState(10),
    _useState(true)
  ]
}

function HookIsHere() {
  updateHooks();
  return (
    <div> 修改n:<button onClick={() => { re_render(); const [n, setn] = updateHooks()[0]; setn(n + 1); }}>n++</button> 修改年齡:<button onClick={() => { re_render(); const [age, setAge] = updateHooks()[1]; setAge(age + 2); }}>age+2</button> 變性:<button onClick={() => { re_render(); const [man, setSex] = updateHooks()[2]; setSex(!man); }}>change</button> </div>
  );
}

function re_render() {
  cursor = 0;
}

function mountComponent() {
  console.log('模擬首次mount')
  cursor = 0;
  return <HookIsHere />; } console.log(state); mountComponent(); setTimeout(() => { console.log(state); }); export default () => { return <HookIsHere />; } 複製代碼
相關文章
相關標籤/搜索