日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React超詳細分析useState與useReducer源碼_React

作者:goClient1992 ? 更新時間: 2022-12-06 編程語言

熱身準備

在正式講useState,我們先熱熱身,了解下必備知識。

為什么會有hooks

大家都知道hooks是在函數組件的產物。之前class組件為什么沒有出現hooks這種東西呢?

答案很簡單,不需要。

因為在class組件中,在運行時,只會生成一個實例,而在這個實例中會保存組件的state等信息。在后續的更新操作中,也只是調用其中的render方法,實例中的信息不會丟失。而在函數組件中,每次渲染,更新都會去執行這個函數組件,所以在函數組件中是沒辦法保存state等信息的。為了保存state等信息,于是有了hooks,用來記錄函數組件的狀態,執行副作用。

hooks執行時機

上面提到,在函數組件中,每次渲染,更新都會去執行這個函數組件。所以我們在函數組件內部聲明的hooks也會在每次執行函數組件時執行。

在這個時候,可能有的同學聽了我上面的說法(hooks用來記錄函數組件的狀態,執行副作用),又有疑惑了,既然每次函數組件執行都會執行hooks方法,那hooks是怎么記錄函數組件的狀態的呢?

答案是,記錄在函數組件對應的fiber節點中。

兩套hooks

在我們剛開始學習使用hooks時,可能會有疑惑, 為什么hooks要在函數組件的頂部聲明,而不能在條件語句或內部函數中聲明?

答案是,React維護了兩套hooks,一套用來在項目初始化mount時,初始化hooks。而在后續的更新操作中會基于初始化的hooks執行更新操作。如果我們在條件語句或函數中聲明hooks,有可能在項目初始化時不會聲明,這樣就會導致在后面的更新操作中出問題。

hooks存儲

提前講一下hooks存儲方式,避免看暈了~~~

每個初始化的hook都會創建一個hook結構,多個hook是通過聲明順序用鏈表的結構相關聯,最終這個鏈表會存放在fiber.memoizedState中:

var hook = {
    memoizedState: null,   // 存儲hook操作,不要和fiber.memoizedState搞混了
    baseState: null,
    baseQueue: null,
    queue: null,    // 存儲該hook本次更新階段的所有更新操作
    next: null      // 鏈接下一個hook
};

而在每個hook.queue中存放的么個update也是一個鏈表結構存儲的,千萬不要和hook的鏈表搞混了。

接下來,讓我們帶著下面幾個問題看文章:

  1. 為什么setState后不能馬上拿到最新的state的值?
  2. 多個setState是如何合并的?
  3. setState到底是同步還是異步的?
  4. 為什么setState的值相同時,函數組件不更新?

假如我們有下面這樣一段代碼:

function App(){
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count => count + 1)
  }
  return (
    <div>
        勇敢牛牛,        <span>不怕困難</span>
        <span onClick={handleClick}>{count}</span>
    </div>
  )
}

初始化 mount

useState

我們先來看下useState()函數:

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

上面的dispatcher就會涉及到開始提到的兩套hooks的變換使用,initialState是我們傳入useState的參數,可以是基礎數據類型,也可以是函數,我們主要看dispatcher.useState(initialState)方法,因為我們這里是初始化,它會調用mountState方法:相關參考視頻:傳送門

function mountState(initialState) {
  var hook = mountWorkInProgressHook();   // workInProgressHook
  if (typeof initialState === 'function') {
    // 在這里,如果我們傳入的參數是函數,會執行拿到return作為initialState
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  var queue = hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

上面的代碼還是比較簡單,主要就是根據useState()的入參生成一個queue并保存在hook中,然后將入參和綁定了兩個參數的dispatchAction作為返回值暴露到函數組件中去使用。

這兩個返回值,第一個hook.memoizedState比較好理解,就是初始值,第二個dispatch,也就是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)這是個什么東西呢?

我們知道使用useState()方法會返回兩個值state, setState,這個setState就對應上面的dispatchAction,這個函數是怎么做到幫我們設置state的值的呢?

我們先保留這個疑問,往下看,在后面會慢慢揭曉答案。

接下來我們主要看看mountWorkInProgressHook都做了些什么。

mountWorkInProgressHook

function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
  };
  // 這里的if/else主要用來區分是否是第一個hook
  if (workInProgressHook === null) {
    currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  } else {
  //  把hook加到hooks鏈表的最后一條, 并且指針指向這條hook
    workInProgressHook = workInProgressHook.next = hook;  
  }
  return workInProgressHook;
}

從上面的currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;這一行代碼,我們可以發現,hook是存放在對應fiber.memoizedState上的。

workInProgressHook = workInProgressHook.next = hook; ,從這一行代碼,我們能知道,如果是有多個hook,他們是以鏈表的形式進行的存放。

不僅僅是useState()這個hook會在初始化時走mountWorkInProgressHook方法,其他的hook,例如:useEffect, useRef, useCallback等在初始化時都是調用的這個方法。

到這里我們能搞明白兩件事:

  • hooks的狀態數據是存放在對應的函數組件的fiber.memoizedState
  • 一個函數組件上如果有多個hook,他們會通過聲明的順序以鏈表的結構存儲;

到這里,我們的useState()已經完成了它初始化時的所有工作了,簡單概括下,useState()在初始化時會將我們傳入的初始值以hook的結構存放到對應的fiber.memoizedState,以數組形式返回[state, dispatchAction]

更新update

當我們以某種形式觸發setState()時,React也會根據setState()的值來決定如何更新視圖。

在上面講到,useState在初始化時會返回[state, dispatchAction],那我們調用setState()方法,實際上就是調用dispatchAction,而且這個函數在初始化時還通過bind綁定了兩個參數, 一個是useState初始化時函數組件對應的fiber,另一個是hook結構的queue

來看下我精簡后的dispatchAction(去除了和setState無關的代碼)

function dispatchAction(fiber, queue, action) {
  // 創建一個update,用于后續的更新,這里的action就是我們setState的入參
  var update = {
    lane: lane,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  };
  // 這段閉環鏈表插入update的操作有沒有很熟悉?
  var pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  var alternate = fiber.alternate;
    // 判斷當前是否是渲染階段
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      var lastRenderedReducer = queue.lastRenderedReducer;
       // 這個if語句里的一大段就是用來判斷我們這次更新是否和上次一樣,如果一樣就不會在進行調度更新
      if (lastRenderedReducer !== null) {
        var prevDispatcher;
        {
          prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          var currentState = queue.lastRenderedState;
          var eagerState = lastRenderedReducer(currentState, action);
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (objectIs(eagerState, currentState)) {
            return;
          }
        } finally {
          {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
        }
      }
    }
    // 將攜帶有update的fiber進行調度更新
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

上面的代碼已經是我盡力精簡的結果了。。。代碼上有注釋,各位看官湊合看下。

不愿細看的我來總結下dispatchAction做的事情:

  • 創建一個update并加入到fiber.hook.queue鏈表中,并且鏈表指針指向這個update
  • 判斷當前是否是渲染階段決定要不要馬上調度更新;
  • 判斷這次的操作和上次的操作是否相同, 如果相同則不進行調度更新;
  • 滿足上述條件則將帶有updatefiber進行調度更新;

到這里我們又搞明白了一個問題:

為什么setState的值相同時,函數組件不更新?

updateState

我們這里不詳細講解調度更新的過程, 后面文章安排, 這里我們只需要知道,在接下來更新過程中,會再次執行我們的函數組件,這時又會調用useState方法了。前面講過,React維護了兩套hooks,一套用于初始化, 一套用于更新。 這個在調度更新時就已經完成了切換。所以我們這次調用useState方法會和之前初始化有所不同。

這次我們進入useState,會看到其實是調用的updateState方法

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

看到這幾行代碼,看官們應該就明白為什么網上有人說useStateuseReducer相似。原來在useState的更新中調用的就是updateReducer啊。

updateReducer

本來很長,想讓各位看官忍一忍。于心不忍,忍痛減了很多

function updateReducer(reducer, initialArg, init) {
  // 創建一個新的hook,帶有dispatchAction創建的update
  var hook = updateWorkInProgressHook();
  var queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  var current = currentHook;
  var baseQueue = current.baseQueue; 
  var pendingQueue = queue.pending;
  current.baseQueue = baseQueue = pendingQueue;
  if (baseQueue !== null) {
    // 從這里能看到之前講的創建閉環鏈表插入update的好處了吧?直接next就能找到第一個update
    var first = baseQueue.next;
    var newState = current.baseState;
    var update = first;
    // 開始遍歷update鏈表執行所有setState
    do {
      var updateLane = update.lane;
      // 假如我們這個update上有多個setState,在循環過程中,最終都會做合并操作
      var action = update.action;
      // 這里的reducer會判斷action類型,下面講
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== first);
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

上面的更新中,會循環遍歷update進行一個合并操作,只取最后一個setState的值,這時候可能有人會問那直接取最后一個setState的值不是更方便嗎?

這樣做是不行的,因為setState入參可以是基礎類型也可以是函數, 如果傳入的是函數,它會依賴上一個setState的值來完成更新操作,下面的代碼就是上面的循環中的reducer

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

到這里我們搞明白了一個問題,多個setState是如何合并的?

updateWorkInProgressHook

下面是偽代碼,我把很多的邏輯判斷給刪除了,免了太長又讓各位看官難受,原來的代碼里會判斷當前的hook是不是第一個調度更新的hook,我這里為了簡單就按第一個來解析

function updateWorkInProgressHook() {
  var nextCurrentHook;
  nextCurrentHook = current.memoizedState;
  var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null
      }
  currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
  return workInProgressHook;
}

從上面代碼能看出來,updateWorkInProgressHook拋去那些判斷, 其實做的事情也很簡單,就是基于fiber.memoizedState創建一個新的hook結構覆蓋之前的hook。前面dispatchAction講到會把update加入到hook.queue中,在這里的newHook.queue上就有這個update

總結

總結下useState初始化和setState更新:

  1. useState會在第一次執行函數組件時進行初始化,返回[state, dispatchAction]
  2. 當我們通過setState也就是dispatchAction進行調度更新時,會創建一個update加入到hook.queue中。
  3. 當更新過程中再次執行函數組件,也會調用useState方法,此時的useState內部會使用更新時的hooks
  4. 通過updateWorkInProgressHook獲取到dispatchAction創建的update
  5. updateReducer通過遍歷update鏈表完成setState合并。
  6. 返回update后的[newState, dispatchAction].

還有兩個問題

為什么setState后不能馬上拿到最新的state的值? React其實可以這么做,為什么沒有這么做,因為每個setState都會觸發更新,React出于性能考慮,會做一個合并操作。所以setState只是觸發了dispatchAction生成了一個update的動作,新的state會存儲在update中,等到下一次render, 觸發這個useState所在的函數組件執行,才會賦值新的state

setState到底是同步還是異步的?

同步的,假如我們有這樣一段代碼:

const handleClick = () => {
  setCount(2)
  setCount(count => count + 1)
  console.log('after setCount')
}

你會驚奇的發現頁面還沒有更新count,但是控制臺已經打印了after setCount

之所以表現上像是異步,是因為內部使用了try{...}finally{...}。當調用setState觸發調度更新時,更新操作會放在finally中,返回去繼續執行handlelick的邏輯。于是會出現上面的情況。

看完這篇文章, 我們可以弄明白下面這幾個問題:

  1. 為什么setState后不能馬上拿到最新的state的值?
  2. 多個setState是如何合并的?
  3. setState到底是同步還是異步的?
  4. 為什么setState的值相同時,函數組件不更新?
  5. setState是怎么完成更新的?
  6. useState是什么時候初始化又是什么時候開始更新的?

原文鏈接:https://blog.csdn.net/It_kc/article/details/127644784

欄目分類
最近更新