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

從源碼理解 React Hook 是如何工作的

開發(fā) 前端
本文只講了狀態(tài) Hook 代表 UseState,和 副作用 Hook 代表 UseEffect,其他 Hook 其實也差不多。

大家好,我是前端西瓜哥。

今天我們從源碼來理解 React Hook 是如何工作的。

React Hook 是 React 16.8 后新加入的黑魔法,讓我們可以 在函數(shù)組件內(nèi)保存內(nèi)部狀態(tài)。

Hook 的優(yōu)勢:

  1. 比組件更小粒度的復用,之前復用需要用 Mixin 或 高階組件(HOC,一個能夠返回組件的組件)進行封裝,前者依賴關(guān)系隱式導致難以維護,后者粒度過大、嵌套過深。
  2. 將處理同一個邏輯的業(yè)務(wù)代碼放在一起,讓代碼可以更好維護。如果是類組件,得放各個生命周期函數(shù)中,邏輯會很分散。
  3. 類組件的 class 寫法容易寫錯,一不小心 this 就指向錯誤,沒錯就是說事件響應(yīng)函數(shù)你。另外讀取值也麻煩,要寫很長的this.state.count。
  4. 擁抱函數(shù)式編程,這是 React 團隊所提倡的編程寫法。

一些全局變量

在講解源碼之前,先認識一些 重要的全局變量:

currentlyRenderingFiber:正在處理的函數(shù)組件對應(yīng) fiber。在執(zhí)行 useState 等 hook 時,需要通過它知道當前 hook 對應(yīng)哪個 fiber。

workInProgressHook:掛載時正在處理的 hook 對象。我們會沿著 workInProcess.memoizedState 鏈表一個個往下走,這個 workInProgressHook 就是該鏈表的指針。

currentHook:舊的 fiber 的 hooks 鏈表(current.memorizedState)指針。

ReactCurrentDispatcher:全局對象,是一個 hook 調(diào)度器對象,其下有 useState、useEffect 等方法,是我們業(yè)務(wù)代碼中 hook 底層調(diào)用的方法。ReactCurrentDispatcher 有三種:

  1. ContextOnlyDispatcher:所有方法都會拋出錯誤,用于防止開發(fā)者在調(diào)用函數(shù)組件的其他時機調(diào)用 React Hook;
  2. HooksDispatcherOnMount:掛載階段用。比如它的 useState 要將初始值保存起來;
  3. HooksDispatcherOnUpdate:更新階段用。比如它的 useState 會無視傳入的初始值,而是從鏈表中取出值。

renderWithHooks

構(gòu)建函數(shù)實例是在 renderWithHooks 方法中進行的。

主要邏輯為:

  1. workInProgress 賦值給全局變量 currentlyRenderingFiber,之后執(zhí)行 hook 就能知道是給哪個組件更新狀態(tài)了。
  2. 選擇 hook 調(diào)度器:根據(jù)是掛載還是更新階段,ReactCurrentDispatcher 設(shè)置為對應(yīng) hook 調(diào)度器。
  3. 調(diào)用函數(shù)組件,進行 render。函數(shù)組件內(nèi)部會調(diào)用 Hook,并返回 ReactElement。
  4. 重置全局變量,比如 currentlyRenderingFiber 設(shè)置回 null;ReactCurrentDispatcher 還原為 ContextOnlyDispatcher,防止在錯誤時機使用 Hook。
function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderLanes
) {
renderLanes = nextRenderLanes;

// 1. 將 workInProgress 賦值給全局變量 currentlyRenderingFiber
// 這樣我們在調(diào)用 Hook 時就能知道對應(yīng)的 fiber 是誰
currentlyRenderingFiber = workInProgress;

workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

// 2. 根據(jù)是掛載還是更新階段,選擇對應(yīng) hook 調(diào)度器
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;

// 3. 調(diào)用函數(shù)組件,里面執(zhí)行各種 React Hook,并返回 ReactElement
let children = Component(props, secondArg);

// 4. hook 調(diào)度器還原為 ContextOnlyDispatcher
ReactCurrentDispatcher.current = ContextOnlyDispatcher;

const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;

// 將一些全局變量進行重置
renderLanes = NoLanes;
currentlyRenderingFiber = null;
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;

// Hook 數(shù)量比上次少,對不上,報錯
if (didRenderTooFewHooks) {
throw new Error(
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
}

return children;
}

下面看看在函數(shù)組件一些常見 Hook 是如何工作的。

useState

首先討論 狀態(tài) Hook 中最常見的一種:useState。

掛載階段(狀態(tài)初始化)

useState 在掛載階段,調(diào)用的是 HooksDispatcherOnMount.useState,也就是 mountState。

  1. 創(chuàng)建新的 hook 空對象,掛到 workInProcess.memorizedState 隊列上(mountWorkInProgressHook 方法)。
  2. dispatchSetState 綁定對應(yīng) fiber 和 queue,方便以后 setState 快速找到相關(guān)對象,最后返回狀態(tài)值和更新狀態(tài)方法。
function mountState(initialState) {
// 1. 創(chuàng)建一個 hook 對象,并添加到 workInProcess.memoizedState 鏈表上
const hook = mountWorkInProgressHook();

// useState 傳入的可能是個函數(shù),要調(diào)用一下拿到初始值
if (typeof initialState === 'function') {
initialState = initialState();
}

hook.memoizedState = hook.baseState = initialState;

const queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;

// 更新 state 的方法
const dispatch = queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
);

// 返回我們經(jīng)常用的 [state, setState]
return [hook.memoizedState, dispatch];
}

mountWorkInProgressHook 實現(xiàn):

function mountWorkInProgressHook() {
// 新的 hook 空對象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};

// 給 memoizedState 鏈表加節(jié)點的邏輯
// 寫過單鏈表的會比較理解,頭節(jié)點要特殊處理
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

更新狀態(tài)操作(setState)

之前 mountState 時,我們返回了一個綁定了 fiber、queue 參數(shù)的 dispatchSetState。setState 更新操作調(diào)用的正是這個 dispatchSetState。

第一個 setState 在被調(diào)用時會立即計算新狀態(tài),這是為了 做新舊 state 對比,決定是否更新組件。如果對比發(fā)現(xiàn)狀態(tài)沒變,繼續(xù)計算下一個 setState 的新狀態(tài),直到找到為止。如果沒找到,就不進行更新。

其后的 setState 則不會計算,等到組件重新 render 再計算。

為對比新舊狀態(tài)計算出來的狀態(tài)值,會保存到 update.eagerState,并將 update.hasEagerState 設(shè)置為 true,之后更新時通過它來直接拿到計算后的最新值。

dispatchSetState 會拿到對應(yīng)的 fiber、queue(對應(yīng) hook 的 queue)、action(新的狀態(tài))。

  1. 創(chuàng)建一個 update 空對象;
  2. 計算出最新狀態(tài),放入到 update.egerState。
  3. 對比新舊狀態(tài)是否相同(使用 Object.is 對比)。相同就不更新了,結(jié)束。不相同,進行后續(xù)的操作。
  4. 將 update 放到 queue.interleaved 或 concurrentQueues 鏈表上(.new 和 .old 文件的邏輯差得有點多),之后更新階段會搬到 queue.pending。
  5. 將當前 fiber 的 lanes 設(shè)置為 SyncLane,這樣后面的 setState 就不會立刻計算最新狀態(tài)了,而是在更新階段才計算。
  6. 接著是調(diào)度更新(scheduleUpdateOnFiber),讓調(diào)度器進行調(diào)度,執(zhí)行更新操作。
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);

// 創(chuàng)建一個 update 更新對象
const update = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};

if (isRenderPhaseUpdate(fiber)) {
// 渲染階段更新,先不討論這種特殊情況
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
// 第二次 setState 時,fiber.lanes 為 SyncLane
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState;
// 計算新狀態(tài)
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 對比新舊狀態(tài)是否不同
if (is(eagerState, currentState)) {
// 狀態(tài)沒改變,當前 setState 無效,return 結(jié)束,無事發(fā)生
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
}

// 將 update 加到 queue 鏈表末尾
// 將 fiber 標記為 SyncLane
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 調(diào)度 fiber 更新
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}

更新階段(獲取最新狀態(tài))

我們先了解一個前置知識:useState 是特殊的 useReducer。

useState 本質(zhì)上在使用 useReducer,在 React 源碼層提供了特殊的名為 basicStateReducer 的 reducer,后面源碼解析中會看到它。

const _useState = (initalVal) => {
return React.useReducer(
function (preState, action) {
// action 對應(yīng) setState 傳入的最新狀態(tài)
// 如果不是函數(shù),直接更新為最新狀態(tài)
// 如果是函數(shù),傳入 preState 并調(diào)用函數(shù),并將返回值作為最新狀態(tài)
return typeof action === 'function' ? action(preState) : action;
},
initalVal
)
}

回到正題。

useState 在更新階段會拿到上一次的狀態(tài)值,此階段調(diào)用的是 HooksDispatcherOnUpdate.useState,也就是 updateState。

updateState 會調(diào)用 updateReducer(useReducer 更新階段也用這個),這也是為什么我說 setState 是特殊 useReducer 的原因。

updateReducer 主要工作有兩個:

  1. 從 current.memorizedState 拷貝 hook 到 workInProcess 下(updateWorkInProgressHook 方法)。
  2. 將 hook.queue.pending 隊列合并到 currentHook.baseQueue 下,然后遍歷隊列中的 update 對象,使用 action 和 reducer 計算出最新的狀態(tài),更新到 hook 上,最后返回新狀態(tài)和新 setState。
function updateState(initialState) {
// 實際用的是 updateReducer
return updateReducer(basicStateReducer);
}
// reducer 函數(shù)
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}

// setReducer 更新階段對應(yīng)的 updateReducer
function updateReducer(reducer, initialArg, init) {
// ----- 【1】 拷貝 hook(current -> workInProcess),并返回這個 hook -----
const hook = updateWorkInProgressHook();

// ----- 【2】 讀取隊列,計算出最新狀態(tài),更新 hook 的狀態(tài) -----
// ...
}

先看看 updateWorkInProgressHook 方法。

該方法中,currentHook 設(shè)置為 current.memoizedState 鏈表的下一個 hook,拷貝它到 currentlyRenderingFiber.memoizedState 鏈表上,返回這個 hook。

function updateWorkInProgressHook() {
// 1. 移動 currentHook 指針
//(來自 current.memoizedState 鏈表)
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}

// 2. 移動 workInProgressHook 指針
//(來自 currentlyRenderingFiber.memoizedState 鏈表)
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}

if (nextWorkInProgressHook !== null) {
// 這種情況為 “渲染時更新邏輯”(在 render 時調(diào)用了 setState)
// 為了更聚焦普通情況,這里不討論
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 3. 渲染時不更新,nextWorkInProgressHook 就一定是 null
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}

currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null // next 就不拷貝了
};

// 4. 經(jīng)典單鏈表末尾加節(jié)點寫法
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}

// 5. 返回拷貝 hook 對象
return workInProgressHook;
}

拿到拷貝后的 hook,就可以計算新狀態(tài)值了。

首先將 hook.queue.pending 隊列合并到 currentHook.baseQueue 下。該隊列包含了一系列 update 對象(因為可能調(diào)用了多次 setState),里面保存有 setState 傳入的最新狀態(tài)值(函數(shù)或其他值)。

然后遍歷 update 計算出最新狀態(tài),保存回 hook,并返回最新狀態(tài)值和 setState 方法。

function updateReducer(reducer, initialArg, init) {
// ----- 【1】 拷貝 hook(current -> workInProcess),并返回這個 hook ----
const hook = updateWorkInProgressHook();

// ----- 【2】 讀取隊列,計算出最新狀態(tài),更新 hook 的狀態(tài) -----
// 取出 hook.queue 鏈表,添加到 current.baseQueue 末尾
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}

// 處理更新隊列
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;

let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;

// 循環(huán),根據(jù) baseQueue 鏈表下的 update 對象計算新狀態(tài)
do {
// 刪掉了一些跳過更新的邏輯

if (update.hasEagerState) {
// 為了對比新舊狀態(tài)來決定是否更新,所計算的新狀態(tài)。
// 如果不同,給 update.hasEagerState 設(shè)置為 true
// 新狀態(tài)賦值給 update.eagerState
newState = update.eagerState;
} else {
// 計算新狀態(tài)
const action = update.action;
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 更新 hook 狀態(tài)
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}

useEffect

有些邏輯類似 useState,比如創(chuàng)建 hook 的 mountWorkInProgressHook 方法實現(xiàn),所以一些重復邏輯就不說了,直奔核心。

掛載階段

核心函數(shù)是 mountEffectImpl。

  1. 【mountWorkInProgressHook】創(chuàng)建一個 hook 空對象,放到 workInProcess.memorizedState 下。
  2. 【pushEffect】創(chuàng)建 effect,添加到 當前 fiber 的 updateQueue 的鏈表上,并將該 effect 賦值給 hook.memoizedState。
mountEffectImpl(fiberFlags, hookFlags, create, deps) {
// create 和 deps 是 useEffect 接受的兩個參數(shù)

// 1. 新建 hook 對象
const hook = mountWorkInProgressHook();

const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;

// 2. 新建 effect 對象,放到 hook.memoizedState 下。
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}

pushEffect 實現(xiàn):

function pushEffect(tag, create, destroy, deps) {
// 創(chuàng)建 effect
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
next: null
};
var componentUpdateQueue = currentlyRenderingFiber.updateQueue;

// 添加到當前 fiber.updateQueue 下。
// updateQueue.laseEffect 保存鏈表的最后一個 effect
// 且使用的是環(huán)形鏈表,通過 updateQueue.laseEffect.next 得到鏈表頭節(jié)點

// 如果 updateQueue 為 null,初始化一個空的 updateQueue 對象
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 往 updateQueue.lastEffect 鏈表上添加 effect 對象。
var lastEffect = componentUpdateQueue.lastEffect;

if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}

return effect;
}

更新階段

核心實現(xiàn)在 updateEffectImpl。

  1. 從 current 拷貝 hook 到 workInProcess。
  2. 對比新舊依賴項 deps,如果沒改變,也創(chuàng)建 effect 加隊列上(但最終不會執(zhí)行),結(jié)束;否則繼續(xù)。
  3. 給當前 fiber 打上 PassiveEffect,表示有 useEffect 的回調(diào)要執(zhí)行。
  4. 創(chuàng)建 effect ,tag 補上加 HookHasEffect,然后加隊列上,后面會執(zhí)行。
function updateEffect(create, dep) {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// hookFlags 此時為 PassiveEffect(代表)

// 1. 從 current 拷貝 hook 到 workInProcess
const hook = updateWorkInProgressHook();

const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;

if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
// 存在依賴項
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 依賴項沒有改變,結(jié)束
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 還是會新建 effect,更新 updateQueue 和 memorizedState
// 但 tag 只是 PassiveEffect,后面遍歷時不會執(zhí)行
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}

// 當前 fiber 打上 PassiveEffect 標記
// 該標記表示存在需要執(zhí)行的 useEffect
currentlyRenderingFiber.flags |= fiberFlags;

hook.memoizedState = pushEffect(
// 相比上面依賴項不變的情況,這里加了 HookHasEffect 標簽
// 之后根據(jù) fiber.updateQueue 會執(zhí)行
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}

我們看下依賴項對比算法 areHookInputsEqual 的細節(jié),它同時遍歷到新舊依賴項最長的尾部,進行 Object.is 對比。在空數(shù)組情況下,這個比較一定返回 true,所以能模擬 componentDidMount / Unmount 的效果。

function areHookInputsEqual(nextDeps, prevDeps) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}

useEffect 的 create 和 destroy 的執(zhí)行時機

當 commit 階段結(jié)束后,useEffect 的 create 和 destroy 會被 Schedule 調(diào)度器異步調(diào)度執(zhí)行。

fiber.updateQueue 下的 effect 會按順序取出,然后一個個執(zhí)行。

function commitPassiveUnmountOnFiber(finishedWork) {
// 執(zhí)行所有 tag 為 HookPassive | HookHasEffect 的 effect 的 destroy
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
// 執(zhí)行所有 tag 為 HookPassive | HookHasEffect 的 effect 的 create
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
}

之前依賴項相同的話,雖然也創(chuàng)建 effect,但它的 tag 對不上,是不會執(zhí)行的。

一些面試題的簡單回答

1、React Hooks 為什么不能寫在條件語句中?

我們要保證 React Hooks 的順序一致。

函數(shù)組件的狀態(tài)是保存在 fiber.memorizedState 中的。它是一個鏈表,保存調(diào)用 Hook 生成的 hook 對象,這些對象保存著狀態(tài)值。當更新時,我們每調(diào)用一個 Hook,其實就是從 fiber.memorizedState 鏈表中讀取下一個 hook,取出它的狀態(tài)。

如果順序不一致了或者數(shù)量不一致了,就會導致錯誤,取出了一個其他 Hook 對應(yīng)的狀態(tài)值。

2、React Hooks 為什么必須在函數(shù)組件內(nèi)部執(zhí)行?React 如何能夠監(jiān)聽 React Hooks 在外部執(zhí)行并拋出異常??

Hooks 底層調(diào)用的是一個全局變量 ReactCurrentDispatcher 的一系列方法。

這個全局變量會在不同階段設(shè)置為不同的對象。render 過程中,掛載階段設(shè)置為 HooksDispatcherOnMount,更新階段設(shè)置為 HooksDispatcherOnUpdate。它們會讀取 currentlyRenderingFiber 全局變量,這個全局變量代表正在處理的 fiber,讀取它進行一些設(shè)置狀態(tài)和讀取狀態(tài)等操作。

在 render 階段外,會設(shè)置為 ContextOnlyDispatcher,這個對象下所有方法都會拋出錯誤,因為此時不存在正常處理的 fiber,使用時機是并不對。

結(jié)尾

本文只講了狀態(tài) Hook 代表 useState,和 副作用 Hook 代表 useEffect,其他 Hook 其實也差不多。

責任編輯:姜華 來源: 前端西瓜哥
相關(guān)推薦

2022-12-07 11:21:30

Reactdiff

2022-04-14 09:01:39

React源碼Flow

2016-10-26 20:49:24

ReactJavascript前端

2019-04-11 15:45:08

ReactMixin前端

2022-04-14 11:50:39

函數(shù)組件hook

2024-09-05 08:04:16

ReactuseState()工具

2022-05-04 10:38:58

React閉包組件

2019-02-18 14:42:18

React.jsUI前端

2020-12-20 10:02:17

ContextReactrender

2021-05-10 17:20:55

AIOps開發(fā)人員人工智能

2024-07-16 09:51:39

HTMLHookReact

2020-05-28 13:33:30

React Hook前端開發(fā)

2022-05-05 08:31:48

useRefuseEffecthook

2011-08-08 13:45:58

jQuery

2024-01-16 08:43:51

React底層機制Hook

2022-03-27 09:06:04

React類型定義前端

2021-05-14 09:49:47

React HookReact應(yīng)用

2022-08-01 07:56:23

React Hook開發(fā)組件

2020-10-21 11:34:49

React Hook庫

2024-09-06 17:55:27

Springboot開發(fā)
點贊
收藏

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