自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

燒腦預(yù)警,這波心智負擔(dān)有點重,深度探討 useState 的實現(xiàn)原理

開發(fā) 后端
UseState 調(diào)用分為兩個階段,一個是初始化階段,一個是更新階段。當我們在BeginWork 中調(diào)用 RenderWithHooks 時,通過判斷 Fiber.memozedState 是否有值來分辨當前執(zhí)行屬于初始階段還是更新階段。

在前面的一篇文章中,我們介紹了 Fiber 的詳細屬性所代表的含義。在函數(shù)式組件中,其中與 hook 相關(guān)的屬性為 memoizedState。

Fiber = {
  memoizedState: Hook
}

Fiber.memoizedState 是一個鏈表的起點,該鏈表的節(jié)點信息為。

export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
}

useState 調(diào)用分為兩個階段,一個是初始化階段,一個是更新階段。當我們在 beginWork 中調(diào)用 renderWithHooks 時,通過判斷 Fiber.memozedState 是否有值來分辨當前執(zhí)行屬于初始階段還是更新階段。

ReactCurrentDispatcher.current =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

在 react 模塊中,我們可以看到 useState 的源碼非常簡單。

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

這里的 dispatcher,其實就是我們在 react-reconciler 中判斷好的 ReactCurrentDispatcher.currenthook 的初始化方法掛載在 HooksDispatcherOnMount 上。

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,

  unstable_isNewReconciler: enableNewReconciler,
};

hook 的更新方法掛載在 HooksDispatcherOnUpdate 上。

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,

  unstable_isNewReconciler: enableNewReconciler,
};

因此,在初始化時,useState 調(diào)用的是 mountState,在更新時,useState 調(diào)用的是 updateState

一、mountState

mountState 的源碼如下:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

理解這個源碼的關(guān)鍵在第一行代碼。

const hook = mountWorkInProgressHook();

react 在 ReactFiberHooks.new.js 模塊全局中創(chuàng)建了如下三個變量。

let currentlyRenderingFiber: Fiber = (null: any);
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;

currentlyRenderingFiber 表示當前正在 render 中的 Fiber 節(jié)點。currentHook 表示當前 Fiber 的鏈表。

workInProgressHook 表示當前正在構(gòu)建中的新鏈表。

mountWorkInProgressHook 方法會創(chuàng)建當前這個 mountState 執(zhí)行所產(chǎn)生的 hook 鏈表節(jié)點。

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // 作為第一個節(jié)點
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 添加到鏈表的下一個節(jié)點
    workInProgressHook = workInProgressHook.next = hook;
  }
  // 返回當前節(jié)點
  return workInProgressHook;
}

hook 節(jié)點的 queue 表示一個新的鏈表結(jié)構(gòu),用于存儲針對同一個 state 的多次 update 操作。,.pending 指向下一個 update 鏈表節(jié)點。此時因為是初始化操作,因此值為 null,此時我們會先創(chuàng)建一個 queue。

const queue: UpdateQueue<S, BasicStateAction<S>> = {
  pending: null,
  lanes: NoLanes,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: (initialState: any),
};
hook.queue = queue;

此時,dispatch 還沒有賦值。在接下來我們調(diào)用了 dispatchSetState,我們待會兒來詳細介紹這個方法,他會幫助 queue.pending 完善鏈表結(jié)構(gòu)或者進入調(diào)度階段,并返回了當前 hook 需要的 dispatch 方法。

const dispatch: Dispatch<
  BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
  null,
  currentlyRenderingFiber,
  queue,
): any));

最后將初始化之后的緩存值和操作方法通過數(shù)組的方式返回。

return [hook.memoizedState, dispatch];

二、updateState

更新時,將會調(diào)用 updateState 方法,他的代碼非常簡單,就是直接調(diào)用了一下 updateReducer。

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

這里的需要注意的是有一個模塊中的全局方法 basicStateReducer,該方法執(zhí)行會結(jié)合傳入的 action 返回最新的 state 值。

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}

代碼中區(qū)分的情況是 useState 與 useReducer 的不同。useState 傳入的是值,而 useReducer 傳入的是函數(shù)

三、updateReducer

updateReducer 的代碼量稍微多了一些,不過他的主要邏輯是計算出最新的 state 值。

當我們使用 setState 多次調(diào)用 dispatch 之后, 在 Hook 節(jié)點的 hook.queue 上會保存一個循環(huán)鏈表用于存儲上一次的每次調(diào)用傳入的 state 值,updateReducer 的主要邏輯就是遍歷該循環(huán)鏈表,并計算出最新值。

此時首先會將 queue.pending 的鏈表賦值給 hook.baseQueue,然后置空 queue.pending。

const pendingQueue = queue.pending;
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;

然后通過 while 循環(huán)遍歷 hook.baseQueue 通過 reducer 計算出最新的 state 值。

// 簡化版代碼
const first = baseQueue.next;

if (first !== null) {
  let newState = current.baseState;
  let update = first;
  do {
    // 執(zhí)行每一次更新,去更新狀態(tài)
    const action = update.action;
    newState = reducer(newState, action);
    update = update.next;
  } while (update !== null && update !== first);

  hook.memoizedState = newState;
}

最后再返回。

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

四、dispatchSetState

當我們調(diào)用 setState 時,最終調(diào)用的是 dispatchSetState 方法。

setLoading -> dispatch -> dispatchSetState

該方法有兩個邏輯,一個是同步調(diào)用,一個是并發(fā)模式下的異步調(diào)用。

同步調(diào)用時,主要的目的在于創(chuàng)建 hook.queue.pending 指向的環(huán)形鏈表。

首先我們要創(chuàng)建一個鏈表節(jié)點,該節(jié)點我們稱之為 update。

const lane = requestUpdateLane(fiber);

const update: Update<S, A> = {
  lane,
  action,
  hasEagerState: false,
  eagerState: null,
  next: (null: any),
};

然后會判斷是否在 render 的時候調(diào)用了該方法。

if (isRenderPhaseUpdate(fiber)) {
  enqueueRenderPhaseUpdate(queue, update);
} else {

isRenderPhaseUpdate 用于判斷當前是否是在 render 時調(diào)用,他的邏輯也非常簡單。

function isRenderPhaseUpdate(fiber: Fiber) {
  const alternate = fiber.alternate;
  return (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  );
}

這里需要重點關(guān)注是 enqueueRenderPhaseUpdate 是如何創(chuàng)建環(huán)形鏈表的。他的代碼如下:

function enqueueRenderPhaseUpdate<S, A>(
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
) {
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
}

我們用圖示來表達一下這個邏輯,光看代碼可能理解起來比較困難。

當只有一個 update 節(jié)點時。

新增一個:

再新增一個:

在后續(xù)的邏輯中,會面臨的一種情況是當渲染正在發(fā)生時,收到了來自并發(fā)事件的更新,我們需要等待直到當前渲染結(jié)束或中斷再將其加入到 Fiber/Hook 隊列。因此React 需要一個數(shù)組來存儲這些更新,代碼邏輯如下:

const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;
function enqueueUpdate(
  fiber: Fiber,
  queue: ConcurrentQueue | null,
  update: ConcurrentUpdate | null,
  lane: Lane,
) {
  concurrentQueues[concurrentQueuesIndex++] = fiber;
  concurrentQueues[concurrentQueuesIndex++] = queue;
  concurrentQueues[concurrentQueuesIndex++] = update;
  concurrentQueues[concurrentQueuesIndex++] = lane;

  concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);

  fiber.lanes = mergeLanes(fiber.lanes, lane);
  const alternate = fiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
}

在這個基礎(chǔ)之上,React 就有機會處理那些不會立即導(dǎo)致重新渲染的更新進入隊列。如果后續(xù)有更高優(yōu)先級的更新出現(xiàn),將會重新對其進行排序。

export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
): void {
  // This function is used to queue an update that doesn't need a rerender. The
  // only reason we queue it is in case there's a subsequent higher priority
  // update that causes it to be rebased.
  const lane = NoLane;
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
}

dispatchSetState 的邏輯中,符合條件就會執(zhí)行該函數(shù)。

if (is(eagerState, currentState)) {
  // Fast path. We can bail out without scheduling React to re-render.
  // It's still possible that we'll need to rebase this update later,
  // if the component re-renders for a different reason and by that
  // time the reducer has changed.
  // TODO: Do we still need to entangle transitions in this case?
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return;
}

很顯然,這就是并發(fā)更新的邏輯,代碼會最終調(diào)用 scheduleUpdateOnFiber,該方法是由 react-reconciler 提供,他后續(xù)會將任務(wù)帶入到 scheduler 中調(diào)度。

// 與 enqueueConcurrentHookUpdateAndEagerlyBailout 方法邏輯
// 但會返回 root 節(jié)點
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);

五、總結(jié)與思考

這就是 useState 的實現(xiàn)原理。其中包含了大量的邏輯操作,可能跟我們在使用時所想的那樣有點不太一樣。這里大量借助了閉包和鏈表結(jié)構(gòu)來完成整個構(gòu)想。

這個邏輯里面也會有大量的探討存在于大廠面試的過程中。例如

  • 為什么不能把 hook 的寫法放到 if 判斷中去。
  • setState 的合并操作是如何做到的。
  • hook 鏈表和 queue.pending 的環(huán)狀鏈表都應(yīng)該如何理解?
  • setState 之后,為什么無法直接拿到最新值,徹底消化了之后這些問題都能很好的得到解答。
責(zé)任編輯:姜華 來源: 這波能反殺
相關(guān)推薦

2020-03-04 14:10:14

戴爾

2022-04-25 21:50:09

前端JS面試題

2024-03-07 12:40:28

Python*args開發(fā)

2012-06-06 15:38:44

2021-10-15 13:47:19

覆蓋率檢測 istanbul 總代碼的比例

2024-11-21 12:09:26

2015-11-19 10:14:12

電影燒腦電影推薦

2009-03-23 09:24:00

HSDPACDMA

2023-12-22 08:46:15

useEffectVueMobx

2012-08-08 10:04:41

IBM但W

2022-03-31 09:50:45

JS面試題

2021-10-19 07:41:45

React組件前端

2024-01-29 08:00:00

架構(gòu)微服務(wù)開發(fā)

2022-02-11 14:01:22

底層String字符串

2024-01-09 07:26:16

ReactVue前端

2010-05-24 17:13:34

Linux SNMP

2010-12-22 11:19:09

Java字節(jié)代碼

2020-07-22 14:30:50

程序員財富螞蟻金服

2010-09-08 09:49:28

藍牙協(xié)議棧
點贊
收藏

51CTO技術(shù)棧公眾號