react hooks 源碼分析 --- useState

1. react hooks簡介

react hooks 是react 16.8.0 的新增特性吝岭,它可以讓你在不編寫class的情況下使用state以及其他的一些react特性。

??在過去的react版本中牙勘,如果我們想要使用狀態(tài)管理或者想要在render之后去做一些事情论泛,我們必須使用class組件才能辦到揩尸。但是現(xiàn)在hooks的出現(xiàn),使得函數(shù)組件也同樣可以做到屁奏。
??hooks實際上是一些以use開頭來明名的函數(shù)岩榆,它就像鉤子一樣,把函數(shù)組件不具備的特性鉤進來坟瓢,使得函數(shù)組件也同樣可以使用這些特性勇边。
??話不多說,下面我們就開始看一下第一個hook的api折联。

2.useState 使用規(guī)則

function User(props) {
  let [count, setCount] = useState(0); // 這里的count粒褒,setCount類似于class組件里的state,setState诚镰,我們要改變count這個狀態(tài)的值奕坟,只需要調(diào)用setCount這個函數(shù)就可以了祥款,它接受一個參數(shù),就是你要更改的值月杉。
  let [name, setName] = useState('Mary'); // 你可以在組件內(nèi)部多次調(diào)用useState來創(chuàng)建多個狀態(tài)變量

  return <div>
    <div>當(dāng)前計數(shù): count</div>
    <button onClick={() => { setCount(count+1); }}>count+1</button>
  </div>
}

??useState使得我們可以在函數(shù)組件里使用狀態(tài)刃跛,它接受一個參數(shù),就是當(dāng)前狀態(tài)的初始值苛萎。返回兩個變量奠伪,第一個變量就是我們的狀態(tài)變量,第二個就是改變這個狀態(tài)的函數(shù)首懈,類似于class組件里的state和setState绊率。
注意,這里useState返回的是一個數(shù)組究履,所以變量的名字是我們自己任意取的滤否。

useState class state
可以在組件內(nèi)部多次使用,創(chuàng)建多個狀態(tài)變量 只有一個state對象
set函數(shù)最仑,傳進來的參數(shù)完全覆蓋該狀態(tài)值 合并state

3.源碼分析

??當(dāng)前react版本為16.9.0藐俺。打開源碼字管,我們首先從react.js文件入手嗽测,找到useState的源碼家卖。

import {
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState, // 在這里
  useResponder,
} from './ReactHooks'; // 所以我們要找的源碼在這個文件里面

??我們在進到ReactHooks.js文件里看一下

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

??從上述代碼可以看出默垄,我們的useState函數(shù)是掛到dispatcher對象上面的,那dispatcher對象到底是什么呢星虹,我們再進到resolveDispatcher函數(shù)里看一下挣饥。
??dispatcher對象被賦值為ReactCurrentDispatcher.current自晰,我們在進一步看一下ReactCurrentDispatcher是什么剑逃。

import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';

const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher), // current是Dispatcher類型的
};

export default ReactCurrentDispatcher;

??dispatcher我們看到ReactCurrentDispatcher.current被初始化為null浙宜,似乎到這里我們什么也沒找到。
??但我們找到了一條線索蛹磺,那就是useState其實是掛載ReactCurrentDispatcher.current對象上面的粟瞬,所以我們只要找到它被賦值的地方就可以了。
??但這部分的內(nèi)容萤捆,實際上屬于fiber調(diào)度的范疇裙品,所以我們就簡單提一下,不做過多闡述俗或,實際上真正賦值的地方是在render階段.

reactFiberHooks.js的renderWithHooks函數(shù)中市怎。
文件路徑
ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount  // 組件掛載階段
        : HooksDispatcherOnUpdate; // 組件更新階段

??上面代碼,當(dāng)nextCurrentHook為空的時候蕴侣,被賦值為HooksDispatcherOnMount焰轻,不為空的時候被賦值為HooksDispatcherOnUpdate,意思就是說昆雀,當(dāng)組件第一次render辱志,也就是掛載的時候,我們的hook api是在HooksDispatcherOnMount這個對象上的狞膘,非首次渲染是在HooksDispatcherOnUpdate對象上的揩懒。

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  ...
  useState: mountState,
  ...
};
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  ...
  useState: updateState,
  ...
};

??所以我們需要分兩個分支來看源碼。

3.1 mountState

??首先我們需要知道挽封,在組件里已球,多次調(diào)用useState,或者其他hook辅愿,那react怎么知道我們當(dāng)前是哪一個hook呢智亮。其實在react內(nèi)部,所有的hook api第一次被調(diào)用的時候都會先創(chuàng)建一個hook對象点待,來保存相應(yīng)的hook信息阔蛉。然后,這個hook對象癞埠,會被加到一個鏈表上状原,這樣我們每次渲染的時候,只要從這個鏈表上面依次的去取hook對象苗踪,就知道了當(dāng)前是哪一個hook了颠区。
下面我們就看一下這個hook對象的具體格式。

const hook: Hook = {
    memoizedState: null, // 緩存當(dāng)前state的值
    baseState: null, // 初始化initState通铲,以及每次dispatch之后的newState
    queue: null, // update quene
    baseUpdate: null, //基于哪一個hook進行更新毕莱,循環(huán)update quene的起點
    next: null, // 指向下一個hook
};

對于useState來說,memoizedState屬性上保存的就是當(dāng)前hook對應(yīng)狀態(tài)變量當(dāng)前的值颅夺,也就是我們獲取到的狀態(tài)變量的值央串。那這個quene上面保存的是什么呢,我們稍后在解釋碗啄。
??言歸正傳质和,我們開始將mountState函數(shù)。組件首次渲染的源碼稚字,就是mountState這個函數(shù)饲宿。也就是說首次渲染時useState的源碼就是mountState。
那么我們來看看它的實現(xiàn)胆描。

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook(); // 第一步:創(chuàng)建新的hook對象并加到鏈上瘫想,返回workInProgressHook
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState; // 第二步:獲取初始值并初始化hook對象
  const queue = (hook.queue = { // 第三步:創(chuàng)建更新隊列(update quene),并初始化
    last: null, // 最后一次的update對象
    dispatch: null, // 更新函數(shù)
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any), // 前面最后一次更新的state值昌讲,更新的值有可能是函數(shù)国夜,函數(shù)計算需要用到前一個state的值
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind( // 第四步
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber), // 綁定當(dāng)前fiber和quene
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

第一步,創(chuàng)建hook對象短绸,并將該hook對象加到hook鏈的末尾车吹。

我們來看一下代碼筹裕。

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {  // 創(chuàng)建hook對象
    memoizedState: null,
 
    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  if (workInProgressHook === null) { // 如果是組件內(nèi)部的第一個hook
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else { // 不是第一個hook對象,就直接把新創(chuàng)建的hook對象加到hook鏈表的末尾
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

第二步:初始化hook對象的狀態(tài)值窄驹,也就是我們傳進來的initState的值朝卒。
第三步:創(chuàng)建更新隊列,這個隊列是更新狀態(tài)值的時候用的乐埠。
第四步:綁定dispatchAction函數(shù)抗斤。我們可以看到最后一行返回的就是這個函數(shù)。也就是說這個函數(shù)丈咐,其實就是我們改變狀態(tài)用的函數(shù)瑞眼,就相當(dāng)于是setState函數(shù)。這里它先做了一個綁定當(dāng)前quene和fiber對象的動作棵逊,就是為了在調(diào)用setState的時候伤疙,知道該更改的是那一個狀態(tài)的值。
??至此歹河,我們就看完了mountState函數(shù)掩浙。
下面這張圖,是我自己畫的簡易版useState源碼的流程圖秸歧。


useState源碼流程圖

??那么到這里厨姚,我們已經(jīng)走完了組件首次渲染調(diào)用useState時的邏輯。現(xiàn)在键菱,我們已經(jīng)拿到了我們的狀態(tài)變量state谬墙,那么我們就可以改變這個狀態(tài)了,也就是調(diào)用set函數(shù)经备,這里為了說明方便拭抬,我們就直接說setState函數(shù)了(實際上你可以隨意取名字)。

3.2 dispatchAction

前面已經(jīng)說過dispatchAction就是我們更改狀態(tài)值時調(diào)用的函數(shù)侵蒙。

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A, // 2
) {
...
if(){
  rerender邏輯
}else{
  const update: Update<S, A> = { // 第一步
      expirationTime,
      suspenseConfig,
      action, // 2
      eagerReducer: null,
      eagerState: null,
      next: null,
    };

    // 第二步:將update加到quene上,更新quene的last為當(dāng)前update,注意quene是一個環(huán)形鏈表
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update; // 環(huán)形鏈
    } else {
      const first = last.next; // 這個last.next是指向第一個update造虎,因為quene是一個環(huán)形鏈表
      if (first !== null) {
        // Still circular.
        update.next = first; // 使quene變成環(huán)形鏈表
      }
      last.next = update; // 將update加到quene上。
    }
    queue.last = update; // 更新quene的last為當(dāng)前update
}
...

省略無關(guān)代碼纷闺,我們可以看到實際上算凿,dispatchAction這個函數(shù)主要做了兩件事情。
第一件就是創(chuàng)建了一個update對象犁功,這個對象上面保存了本次更新的相關(guān)信息氓轰,包括新的狀態(tài)值action。
第二件浸卦,就是將所有的update對象串成了一個環(huán)形鏈表署鸡,保存在我們hook對象的queue屬性上面。所以我們就知道了queue這個屬性的意義,它是保存所有更新行為的地方靴庆。
在這里我們可以看到时捌,我們要更改的狀態(tài)值并沒有真的改變,只是被緩存起來了撒穷。那么真正改變狀態(tài)值的地方在哪呢匣椰?答案就是在下一次render時裆熙,函數(shù)組件里的useState又一次被調(diào)用了端礼,這個時候才是真的更新state的時機。

3.3 updateState

這里就是我們組件更新時入录,調(diào)用useState時真正走的邏輯了蛤奥。

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
  reducer: (S, A) => S, // 對于useState來說就是basicStateReducer
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook(); // 獲取當(dāng)前正在工作的hook,Q1
  const queue = hook.queue; // 更新隊列
 // The last update in the entire queue
  const last = queue.last; // 最后一次的update對象
  // The last update that is part of the base state.
  const baseUpdate = hook.baseUpdate; // 上一輪更新的最后一次更新對象
  const baseState = hook.baseState; // 上一次的action僚稿,現(xiàn)在是初始值

  // Find the first unprocessed update.
  let first;
  if (baseUpdate !== null) {
    if (last !== null) {
      // For the first update, the queue is a circular linked list where
      // `queue.last.next = queue.first`. Once the first update commits, and
      // the `baseUpdate` is no longer empty, we can unravel the list.
      last.next = null; // 因為quene是一個環(huán)形鏈表凡桥,所以這里要置空
    } 
    first = baseUpdate.next; // 第一次是用的last.next作為第一個需要更新的update,第二次之后就是基于上一次的baseUpdate來開始了(baseUpdate就是上一次的最后一個更新)
  } else {
    first = last !== null ? last.next : null; // last.next是第一個update
  }
  if (first !== null) { // 沒有更新,則不需要執(zhí)行蚀同,直接返回
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    do { // 循環(huán)鏈表缅刽,執(zhí)行每一次更新
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        ...
      } else { // 正常邏輯
        // This update does have sufficient priority.
        // Process this update.
        if (update.eagerReducer === reducer) { // 如果是useState,他的reducer就是basicStateReducer
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          newState = ((update.eagerState: any): S);
        } else {
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
    } while (update !== null && update !== first);

    if (!didSkip) { // 不跳過蠢络,就更新baseUpdate和baseState
      newBaseUpdate = prevUpdate;
      newBaseState = newState;
    }
    ...
    hook.memoizedState = newState; // 更新hook對象
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;

    queue.lastRenderedState = newState;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

updateState做的事情衰猛,實際上就是拿到更新隊列,循環(huán)隊列刹孔,并根據(jù)每一個update對象對當(dāng)前hook進行狀態(tài)更新啡省。最后返回最終的結(jié)果。

這是我在學(xué)習(xí)useState源碼時的自問自答

1髓霞、怎么循環(huán)hook對象的卦睹,在哪里操作的
    (1)從當(dāng)前fiber對象的memoizedState屬性保存著當(dāng)前組件的第一個hook對象
    (2)在每次執(zhí)行updateState的時候,首先需要獲取當(dāng)前工作中的hook方库,就是在這里循環(huán)的hook
    (3)hook鏈?zhǔn)且粋€環(huán)形鏈嗎结序?不是,是單向鏈表
        在mount階段纵潦,workInProgressHook.next = null,update階段最后一個hook的next依然是null
        是不是說當(dāng)前fiber對象的memoizedState一直都是第一個hook (462行)
2.Q:更新函數(shù)綁定當(dāng)前hook的地方在哪
  A:在dispatchAcion.bind的地方徐鹤,綁定了fiber和quene
3.Q:更新state時,怎么定位到第一個需要執(zhí)行的update的
  A:基于baseUpdate來開始更新
4.Q:renderWithHooks為什么第一次沒有執(zhí)行 FunctionComponent這個分支酪穿?
  A:renderWithHooks是在組件更新階段執(zhí)行的FunctionComponent
5.Q:useState可以放對象嗎凳干?
  A:可以,但是如果setState里的對象還是同一個就不會觸發(fā)重新渲染

第一次正式的寫技術(shù)文章被济,作文水平有限救赐,希望可以幫到大家。

參考
[掘金]? useState源碼解析

Youmeng博客 ? 閱讀源碼后,來講講React Hooks是怎么實現(xiàn)的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末经磅,一起剝皮案震驚了整個濱河市泌绣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌预厌,老刑警劉巖阿迈,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異轧叽,居然都是意外死亡苗沧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門炭晒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來待逞,“玉大人,你說我怎么就攤上這事网严∈队#” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵震束,是天一觀的道長怜庸。 經(jīng)常有香客問我,道長垢村,這世上最難降的妖魔是什么割疾? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮肝断,結(jié)果婚禮上杈曲,老公的妹妹穿的比我還像新娘。我一直安慰自己胸懈,他們只是感情好担扑,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趣钱,像睡著了一般涌献。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上首有,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天燕垃,我揣著相機與錄音,去河邊找鬼井联。 笑死卜壕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的烙常。 我是一名探鬼主播轴捎,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侦副?” 一聲冷哼從身側(cè)響起侦锯,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秦驯,沒想到半個月后尺碰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡译隘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年亲桥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片细燎。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡两曼,死狀恐怖皂甘,靈堂內(nèi)的尸體忽然破棺而出玻驻,到底是詐尸還是另有隱情,我是刑警寧澤偿枕,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布璧瞬,位于F島的核電站,受9級特大地震影響渐夸,放射性物質(zhì)發(fā)生泄漏嗤锉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一墓塌、第九天 我趴在偏房一處隱蔽的房頂上張望瘟忱。 院中可真熱鬧,春花似錦苫幢、人聲如沸访诱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽触菜。三九已至,卻和暖如春哀峻,著一層夾襖步出監(jiān)牢的瞬間涡相,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工剩蟀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留催蝗,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓育特,卻偏偏與公主長得像丙号,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344