欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React超詳細分析useState與useReducer源碼

 更新時間:2022年11月04日 11:22:41   作者:goClient1992  
我正在處理的組件是表單的時間輸入。表單相對復雜,并且是動態(tài)生成的,根據(jù)嵌套在其他數(shù)據(jù)中的數(shù)據(jù)顯示不同的字段。我正在用useReducer管理表單的狀態(tài),到目前為止效果很好

熱身準備

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

為什么會有hooks

大家都知道hooks是在函數(shù)組件的產(chǎn)物。之前class組件為什么沒有出現(xiàn)hooks這種東西呢?

答案很簡單,不需要。

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

hooks執(zhí)行時機

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

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

答案是,記錄在函數(shù)組件對應的fiber節(jié)點中。

兩套hooks

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

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

hooks存儲

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

每個初始化的hook都會創(chuàng)建一個hook結(jié)構(gòu),多個hook是通過聲明順序用鏈表的結(jié)構(gòu)相關(guān)聯(lián),最終這個鏈表會存放在fiber.memoizedState中:

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

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

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

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

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

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()函數(shù):

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

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

function mountState(initialState) {
  var hook = mountWorkInProgressHook();   // workInProgressHook
  if (typeof initialState === 'function') {
    // 在這里,如果我們傳入的參數(shù)是函數(shù),會執(zhí)行拿到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];
}

上面的代碼還是比較簡單,主要就是根據(jù)useState()的入?yún)⑸梢粋€queue并保存在hook中,然后將入?yún)⒑徒壎藘蓚€參數(shù)的dispatchAction作為返回值暴露到函數(shù)組件中去使用。

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

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

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

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

mountWorkInProgressHook

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

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

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

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

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

  • hooks的狀態(tài)數(shù)據(jù)是存放在對應的函數(shù)組件的fiber.memoizedState;
  • 一個函數(shù)組件上如果有多個hook,他們會通過聲明的順序以鏈表的結(jié)構(gòu)存儲;

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

更新update

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

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

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

function dispatchAction(fiber, queue, action) {
  // 創(chuàng)建一個update,用于后續(xù)的更新,這里的action就是我們setState的入?yún)?
  var update = {
    lane: lane,
    action: action,
    eagerReducer: null,
    eagerState: null,
    next: null
  };
  // 這段閉環(huán)鏈表插入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語句里的一大段就是用來判斷我們這次更新是否和上次一樣,如果一樣就不會在進行調(diào)度更新
      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進行調(diào)度更新
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

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

不愿細看的我來總結(jié)下dispatchAction做的事情:

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

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

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

updateState

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

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

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

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

updateReducer

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

function updateReducer(reducer, initialArg, init) {
  // 創(chuàng)建一個新的hook,帶有dispatchAction創(chuàng)建的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) {
    // 從這里能看到之前講的創(chuàng)建閉環(huán)鏈表插入update的好處了吧?直接next就能找到第一個update
    var first = baseQueue.next;
    var newState = current.baseState;
    var update = first;
    // 開始遍歷update鏈表執(zhí)行所有setState
    do {
      var updateLane = update.lane;
      // 假如我們這個update上有多個setState,在循環(huán)過程中,最終都會做合并操作
      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];
}

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

這樣做是不行的,因為setState入?yún)⒖梢允腔A類型也可以是函數(shù), 如果傳入的是函數(shù),它會依賴上一個setState的值來完成更新操作,下面的代碼就是上面的循環(huán)中的reducer

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

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

updateWorkInProgressHook

下面是偽代碼,我把很多的邏輯判斷給刪除了,免了太長又讓各位看官難受,原來的代碼里會判斷當前的hook是不是第一個調(diào)度更新的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創(chuàng)建一個新的hook結(jié)構(gòu)覆蓋之前的hook。前面dispatchAction講到會把update加入到hook.queue中,在這里的newHook.queue上就有這個update

總結(jié)

總結(jié)下useState初始化和setState更新:

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

還有兩個問題

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

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

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

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

你會驚奇的發(fā)現(xiàn)頁面還沒有更新count,但是控制臺已經(jīng)打印了after setCount。

之所以表現(xiàn)上像是異步,是因為內(nèi)部使用了try{...}finally{...}。當調(diào)用setState觸發(fā)調(diào)度更新時,更新操作會放在finally中,返回去繼續(xù)執(zhí)行handlelick的邏輯。于是會出現(xiàn)上面的情況。

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

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

到此這篇關(guān)于React超詳細分析useState與useReducer源碼的文章就介紹到這了,更多相關(guān)React useState與useReducer內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • React懶加載實現(xiàn)原理深入分析

    React懶加載實現(xiàn)原理深入分析

    懶加載意思是不會預加載,而是需要使用某段代碼,某個組件或者某張圖片時,才加載他們(延遲加載),這篇文章主要介紹了React懶加載實現(xiàn)原理
    2022-11-11
  • 無廢話快速上手React路由開發(fā)

    無廢話快速上手React路由開發(fā)

    本文以簡潔為目標,幫助快速上手react-router-dom默認你接觸過路由相關(guān)的開發(fā),通過實例代碼講解的很詳細,對React路由相關(guān)知識感興趣的朋友一起看看吧
    2021-05-05
  • React Native懸浮按鈕組件的示例代碼

    React Native懸浮按鈕組件的示例代碼

    本篇文章主要介紹了React Native懸浮按鈕組件的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起看看吧。
    2018-04-04
  • React 高階組件入門介紹

    React 高階組件入門介紹

    本篇文章主要介紹了React高階組件入門介紹,這篇文章中我們詳細的介紹了什么是高階組件,如何使用高階組件,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01
  • React中的CSS局部引入過程

    React中的CSS局部引入過程

    這篇文章主要介紹了React中的CSS局部引入過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • 40行代碼把Vue3的響應式集成進React做狀態(tài)管理

    40行代碼把Vue3的響應式集成進React做狀態(tài)管理

    這篇文章主要介紹了40行代碼把Vue3的響應式集成進React做狀態(tài)管理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-05-05
  • React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案

    React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案

    這篇文章主要為大家介紹了React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02
  • ChatGLM?集成LangChain工具詳解

    ChatGLM?集成LangChain工具詳解

    這篇文章主要為大家介紹了Svelte和React框架使用比較,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-04-04
  • React如何以Hook的方式使用Echarts

    React如何以Hook的方式使用Echarts

    這篇文章主要介紹了React如何以Hook的方式使用Echarts問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • React項目中decorators裝飾器報錯問題解決方案

    React項目中decorators裝飾器報錯問題解決方案

    這篇文章主要介紹了React項目中decorators裝飾器報錯,本文給大家分享問題所在原因及解決方案,通過圖文實例相結(jié)合給大家介紹的非常詳細,需要的朋友可以參考下
    2023-01-01

最新評論