來來來,手摸手寫一個hook

來來來,手摸手寫一個hook

hello,這里是瀟晨,今天就帶著大家一起來手寫一個迷你版的hooks,方便大家理解hook在源碼中的運行機制,配有圖解,保姆級的教程,只求同學(xué)一個小小的??,??。

第一步:引入React和ReactDOM

因為我們要將jsx轉(zhuǎn)變?yōu)?code>virtual-dom,這一步分工作就交給babel吧,而jsxbabel進行詞法解析之后會形成React.createElement()的調(diào)用,而React.createElement()執(zhí)行之后的返回結(jié)果就是jsx對象或者叫virtual-dom。

又因為我們要將我們的demo渲染到dom上,所以我們引入ReactDOM

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

第二步:我們來寫一個小demo

我們定義兩個狀態(tài)countage,在點擊的時候觸發(fā)更新,讓它們的值加1。

在源碼中useState是保存在一個Dispatcher對象上面的,并且在mountupdate的時候取到的是不同的hooks,所以我們先暫時從Dispatcher上拿到useState,等下在來定義Dispatcher。

接下來定義一個schedule函數(shù),每次調(diào)用的時候會重新渲染組件。

function App() {
  let [count, setCount] = Dispatcher.useState(1);
  let [age, setAge] = Dispatcher.useState(10);
  return (
    <>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(() => count + 1)}> Add count</button>
      <p>Age is {age}</p>
      <button onClick={() => setAge(() => age + 1)}> Add age</button>
    </>
  );
}

function schedule() {   //每次調(diào)用會重新渲染組件
  ReactDOM.render(<App />, document.querySelector("#root"));
}

schedule();

第三步:定義Dispatcher

在看這部分前,先來捋清楚fiber、hookupdate的關(guān)系,看圖:

image-20211129105128673

Dispatcher是什么:Dispatcher在源碼中就是一個對象,上面存放著各種各樣的hooks,在mountupdate的時候會使用過不同的Dispatcher,來看看在源碼中Dispatcher是什么樣子:

在調(diào)用useState之后,會調(diào)用一個resolveDispatcher的函數(shù),這個函數(shù)調(diào)用之后會返回一個dispatcher對象,這個對象上就有useState等鉤子。

image-20211126164214374

那我們來看看這個函數(shù)做了啥事情,這個函數(shù)比較簡單,直接從ReactCurrentDispatcher對象上拿到current,然后返回出來的這個current就是dispatcher,那這個ReactCurrentDispatcher又是個啥?別急,繼續(xù)在源碼中來找一下。

image-20211126164903336

在源碼中有這樣一段代碼,如果是在正式環(huán)境中,分為兩種情況

  1. 如果滿足 current === null || current.memoizedState === null,說明我們處于首次渲染的時候,也就是mount的時候,其中current就是我們fiber節(jié)點,memoizedState保存了fiberhook,也就是說在應(yīng)用首次渲染的時候,current fiber是不存在的,我們還沒有創(chuàng)造出任何fiber節(jié)點,或者存在某些fiber,但是上面沒有構(gòu)建相應(yīng)的hook,這個時候就可以認為是處于首次渲染的時候,我們?nèi)〉降氖?code>HooksDispatcherOnMount
  2. 如果不滿足 current === null || current.memoizedState === null,就說明我們處于更新階段,也就是update的時候,我們?nèi)〉降氖?code>HooksDispatcherOnUpdate
if (__DEV__) {
    if (current !== null && current.memoizedState !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }

那我們就來看一下這個HooksDispatcherOnMountHooksDispatcherOnUpdate是個什么,好家伙,原來你包含了所有的hooks啊。

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,
};

所以dispatcher就是個對象,里面包含了所有的hooks,在首次渲染和更新的時候拿到的是不同的dispatcher,在調(diào)用hooks的時候就會調(diào)用到不同的函數(shù),比如如果使用了useState,在mount的時候調(diào)用到的就是mountState,在update的時候調(diào)用到的就是updateState。

image-20211126170906166

現(xiàn)在我們來手寫一下dispatcherdispatcher是個對象,對象上存在useState,我們用一個自執(zhí)行函數(shù)來表示,此外還需要用到兩個變量和一個常量fiber

  • workInProgressHook表示遍歷到的hook(因為hook會保存在鏈表上,需要遍歷鏈表計算hook上保存的狀態(tài))
  • 為了簡單起見,定義一個isMount=true表示mount的時候,在update的時候?qū)⑺O(shè)置成false
  • 為簡單起見,fiber就定義成一個對象,memoizedState表示這個fiber節(jié)點上存放的hook鏈表,stateNode就是第二步的demo。
let workInProgressHook;//當前工作中的hook
let isMount = true;//是否時mount時

const fiber = {//fiber節(jié)點
  memoizedState: null,//hook鏈表
  stateNode: App
};

const Dispatcher = (() => {//Dispatcher對象
  function useState(){
    //。。。
  }

  return {
    useState
  };
})();

在定義useState之前,首先來看看hookupdate的數(shù)據(jù)結(jié)構(gòu)

hook:
  • queue:上面有pending屬性,pending也是一條環(huán)狀鏈表,上面存放了未被更新的update,也就是說這些update會以next指針連接成環(huán)狀鏈表。
  • memoizedState表示當前的狀態(tài)
  • next:指向下一個hook,形成一條鏈表
 const hook = {//構(gòu)建hook
   queue: {
     pending: null//未執(zhí)行的update鏈表
   },
   memoizedState: null,//當前state
   next: null//下一個hook
 };
update:
  • action:是出發(fā)更新的函數(shù)
  • next:連接下一個update,形成一條環(huán)狀鏈表
 const update = {//構(gòu)建update
    action,
    next: null
  };

那接下來定義useState吧,分三個部分:

  • 創(chuàng)建hook或取到hook
    1. mount的時候:調(diào)用mountWorkInProgressHook創(chuàng)建一個初始的hook,賦值useState傳進來的初始值initialState
    2. update的時候:調(diào)用updateWorkInProgressHook,拿到當前正在工作的hook
  • 計算hook上未更新的狀態(tài):遍歷hook上的pending鏈表,調(diào)用鏈表節(jié)點上的action函數(shù),生成一個新的狀態(tài),然后更新hook上的狀態(tài)。
  • 返回新的狀態(tài)和dispatchAction傳入queue參數(shù)
function useState(initialState) {
    //第1步:創(chuàng)建hook或取到hook
    let hook;
    if (isMount) {
      hook = mountWorkInProgressHook();
      hook.memoizedState = initialState;//初始狀態(tài)
    } else {
      hook = updateWorkInProgressHook();
    }
        //第2步:計算hook上未更新的狀態(tài)
    let baseState = hook.memoizedState;//初始狀態(tài)
    if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next;//第一個update

      do {
        const action = firstUpdate.action;
        baseState = action(baseState);//調(diào)用action計算新的狀態(tài)
        firstUpdate = firstUpdate.next;//通過update的action計算state
      } while (firstUpdate !== hook.queue.pending);//當鏈表還沒遍歷完時 進行循環(huán)

      hook.queue.pending = null;//重置update鏈表
    }
    hook.memoizedState = baseState;//賦值新的state
  
        //第3步:返回新的狀態(tài)和dispatchAction傳入queue參數(shù)
    return [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回
  }

接下來定義mountWorkInProgressHookupdateWorkInProgressHook這兩個函數(shù)

  • mountWorkInProgressHook:在mount的時候調(diào)用,新創(chuàng)建一個hook對象,
    1. 如果當前fiber不存在memoizedState,那當前hook就是這個fiber上的第一個hook,將hook賦值給fiber.memoizedState
    2. 如果當前fiber存在memoizedState,那將當前hook接在workInProgressHook.next后面。
    3. 將當前hook賦值給workInProgressHook
  • updateWorkInProgressHook:在update的時候調(diào)用,返回當前的hook,也就是workInProgressHook,并且將workInProgressHook指向hook鏈表的下一個。
function mountWorkInProgressHook() {//mount時調(diào)用
    const hook = {//構(gòu)建hook
      queue: {
        pending: null//未執(zhí)行的update鏈表
      },
      memoizedState: null,//當前state
      next: null//下一個hook
    };
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;//第一個hook的話直接賦值給fiber.memoizedState
    } else {
      workInProgressHook.next = hook;//不是第一個的話就加在上一個hook的后面,形成鏈表
    }
    workInProgressHook = hook;//記錄當前工作的hook
    return workInProgressHook;
  }

function updateWorkInProgressHook() {//update時調(diào)用
  let curHook = workInProgressHook;
  workInProgressHook = workInProgressHook.next;//下一個hook
  return curHook;
}

第四步:定義dispatchAction

  • 創(chuàng)建update,掛載載queue.pending

    1. 如果之前queue.pending不存在,那創(chuàng)建的這個update就是第一個,則update.next = update
    2. 如果之前queue.pending存在,則將創(chuàng)建的這個update加入queue.pending的環(huán)狀鏈表中
    1
  • isMount=false,并且賦值workInProgressHook,調(diào)用schedule進行更新渲染

function dispatchAction(queue, action) {//觸發(fā)更新
  const update = {//構(gòu)建update
    action,
    next: null
  };
  if (queue.pending === null) {
    update.next = update;//update的環(huán)狀鏈表
  } else {
    update.next = queue.pending.next;//新的update的next指向前一個update
    queue.pending.next = update;//前一個update的next指向新的update
  }
  queue.pending = update;//更新queue.pending

  isMount = false;//標志mount結(jié)束
  workInProgressHook = fiber.memoizedState;//更新workInProgressHook
  schedule();//調(diào)度更新
}

最終代碼

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

let workInProgressHook;//當前工作中的hook
let isMount = true;//是否時mount時

const fiber = {//fiber節(jié)點
  memoizedState: null,//hook鏈表
  stateNode: App//dom
};

const Dispatcher = (() => {//Dispatcher對象
  function mountWorkInProgressHook() {//mount時調(diào)用
    const hook = {//構(gòu)建hook
      queue: {
        pending: null//未執(zhí)行的update鏈表
      },
      memoizedState: null,//當前state
      next: null//下一個hook
    };
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;//第一個hook的話直接賦值給fiber.memoizedState
    } else {
      workInProgressHook.next = hook;//不是第一個的話就加在上一個hook的后面,形成鏈表
    }
    workInProgressHook = hook;//記錄當前工作的hook
    return workInProgressHook;
  }
  function updateWorkInProgressHook() {//update時調(diào)用
    let curHook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;//下一個hook
    return curHook;
  }
  function useState(initialState) {
    let hook;
    if (isMount) {
      hook = mountWorkInProgressHook();
      hook.memoizedState = initialState;//初始狀態(tài)
    } else {
      hook = updateWorkInProgressHook();
    }

    let baseState = hook.memoizedState;//初始狀態(tài)
    if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next;//第一個update

      do {
        const action = firstUpdate.action;
        baseState = action(baseState);
        firstUpdate = firstUpdate.next;//循環(huán)update鏈表
      } while (firstUpdate !== hook.queue.pending);//通過update的action計算state

      hook.queue.pending = null;//重置update鏈表
    }
    hook.memoizedState = baseState;//賦值新的state

    return [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回
  }

  return {
    useState
  };
})();

function dispatchAction(queue, action) {//觸發(fā)更新
  const update = {//構(gòu)建update
    action,
    next: null
  };
  if (queue.pending === null) {
    update.next = update;//update的環(huán)狀鏈表
  } else {
    update.next = queue.pending.next;//新的update的next指向前一個update
    queue.pending.next = update;//前一個update的next指向新的update
  }
  queue.pending = update;//更新queue.pending

  isMount = false;//標志mount結(jié)束
  workInProgressHook = fiber.memoizedState;//更新workInProgressHook
  schedule();//調(diào)度更新
}

function App() {
  let [count, setCount] = Dispatcher.useState(1);
  let [age, setAge] = Dispatcher.useState(10);
  return (
    <>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(() => count + 1)}> Add count</button>
      <p>Age is {age}</p>
      <button onClick={() => setAge(() => age + 1)}> Add age</button>
    </>
  );
}

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

schedule();

預(yù)覽效果https://codesandbox.io/s/custom-hook-tyf19?file=/src/index.js

視頻講解(高效學(xué)習(xí)):點擊學(xué)習(xí)

往期react源碼解析文章:

1.開篇介紹和面試題

2.react的設(shè)計理念

3.react源碼架構(gòu)

4.源碼目錄結(jié)構(gòu)和調(diào)試

5.jsx&核心api

6.legacy和concurrent模式入口函數(shù)

7.Fiber架構(gòu)

8.render階段

9.diff算法

10.commit階段

11.生命周期

12.狀態(tài)更新流程

13.hooks源碼

14.手寫hooks

15.scheduler&Lane

16.concurrent模式

17.context

18事件系統(tǒng)

19.手寫迷你版react

20.總結(jié)&第一章的面試題解答

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容