usestate中的回调函数_React Hooks 源码解析(3):useState
- React 源碼版本: v16.11.0
- 源碼注釋筆記:
在寫本文之前,事先閱讀了網上了一些文章,關于 Hooks 的源碼解析要么過于淺顯、要么就不細致,所以本文著重講解源碼,由淺入深,爭取一行代碼也不放過。那本系列講解第一個 Hooks 便是 useState,我們將從 useState 的用法開始,再闡述規(guī)則、講解原理,再簡單實現(xiàn),最后源碼解析。另外,在本篇開頭,再補充一個 Hooks 的概述,前兩篇限于篇幅問題一直沒有寫一塊。
注:距離上篇文章已經過去了兩個月,這兩個月業(yè)務繁忙所以沒有什么時間更新該系列的文章,但 react 這兩個月卻從 16.9 更新到了 16.11,review 了一下這幾次的更新都未涉及到 hooks,所以我也直接把源碼筆記這塊更新到了 16.11。
1. React Hooks 概述
Hook 是 React 16.8 的新增特性,它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。其本質上就是一類特殊的函數(shù),它們約定以 use 開頭,可以為 Function Component 注入一些功能,賦予 Function Component 一些 Class Component 所具備的能力。
例如,原本我們說 Function Component 無法保存狀態(tài),所以我們經常說 Stateless Function Component,但是現(xiàn)在我們借助 useState 這個 hook 就可以讓 Function Component 像 Class Component 一樣具有狀態(tài)。前段時間 @types/react 也將 SFC 改成了 FC。
1.1 動機
在 React 官網的 Hook 簡介中列舉了推出 Hook 的原因:
一,組件之間復用狀態(tài)邏輯很難。是我們系列第二篇中一直討論的問題,此處不再贅述。
二,復雜組件變得難以理解,即組件邏輯復雜。主要是針對 Class Component 來說,我們經常要在組件的各種生命周期中編寫代碼,如在 componentDidMount 和 componentDidUpdate 中獲取數(shù)據,但是在 componentDidMount 中可能也包括很多其他的邏輯,使得組件越開發(fā)越臃腫,且邏輯明顯扎堆在各種生命周期函數(shù)中,使得 React 開發(fā)成為了“面向生命周期編程”。而 Hooks 的出現(xiàn),將這種這種“面向生命周期編程”變成了“面向業(yè)務邏輯編程”,使得開發(fā)者不用再去關心本不該關心的生命周期。
三,難以理解的 class,表現(xiàn)為函數(shù)式編程比 OOP 更加簡單。那么再深入一些去考慮性能,Hook 會因為在渲染時創(chuàng)建函數(shù)而變慢嗎?答案是不會,在現(xiàn)在瀏覽器中閉包和類的原始性能只有在極端場景下又有有明顯的區(qū)別。反而,我們可以認為 Hook 的設計在某些方面會更加高效:
其實,React Hooks 帶來的好處不僅是更函數(shù)式、更新粒度更細、代碼更清晰,還有以下三個優(yōu)點:
1.2 Hooks API
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
- useResponder
以上 Hooks API 都會在未來一一講解,此處不再贅述。本文先講解 useState。
1.3 自定義 Hooks
通過自定義 Hook,可以將組件邏輯提取到可重用的函數(shù)中。這里安利一個網站:https://usehooks.com/,里面收集了實用的自定義 Hooks,可以無縫接入項目中使用,充分體現(xiàn)了 Hooks 的可復用性之強、使用之簡單。
2. useState 的用法與規(guī)則
import React, { useState } from 'react'const App: React.FC = () => {const [count, setCount] = useState<number>(0)const [name, setName] = useState<string>('airing')const [age, setAge] = useState<number>(18)return (<><p>You clicked {count} times</p><button onClick={() => {setCount(count + 1)setAge(age + 1)}}>Click me</button></>) }export default App如果用過 redux 的話,這一幕一定非常眼熟。給定一個初始 state,然后通過 dispatch 一個 action,再經由 reducer 改變 state,再返回新的 state,觸發(fā)組件重新渲染。
它等價于下面這個 Class Component:
import React from 'react'class Example extends React.Component {constructor(props) {super(props);this.state = {count: 0,age: 18,name: 'airing'};}render() {return (<><p>You clicked {this.state.count} times</p><button onClick={() => this.setState({ count: this.state.count + 1,age: this.state.age + 1})}>Click me</button></>);} }可以看到 Function Component 比 Class Component 簡潔需要,useState 的使用也非常簡單。但需要注意的是,Hooks 的使用必須要符合這條規(guī)則:確保 Hook 在每一次渲染中都按照同樣的順序被調用。因此最好每次只在最頂層使用 Hook,不要在循環(huán)、條件、嵌套函數(shù)中調用 Hooks,否則容易出錯。
那么,為什么我們必須要滿足這條規(guī)則?接下來,我們看一下 useState 的實現(xiàn)原理并自己親自動手實現(xiàn)一個 useState 便可一目了然。
3. useState 的原理與簡單實現(xiàn)
3.1 Demo 1: dispatch
第二節(jié)中我們發(fā)現(xiàn) useState 的用法蠻像 Redux 的,那我們基于 Redux 的思想,自己動手實現(xiàn)一個 useState:
function useState(initialValue) {let state = initialValuefunction dispatch(newState) {state = newStaterender(<App />, document.getElementById('root'))}return [state, dispatch] }我們將從 React 中引入的 useState 替換成自己實現(xiàn)的:
import React from 'react' import { render } from 'react-dom'function useState(initialValue: any) {let state = initialValuefunction dispatch(newState: any) {state = newStaterender(<App />, document.getElementById('root'))}return [state, dispatch] }const App: React.FC = () => {const [count, setCount] = useState(0)const [name, setName] = useState('airing')const [age, setAge] = useState(18)return (<><p>You clicked {count} times</p><p>Your age is {age}</p><p>Your name is {name}</p><button onClick={() => {setCount(count + 1)setAge(age + 1)}}>Click me</button></>) }export default App這個時候我們發(fā)現(xiàn)點擊按鈕不會有任何響應,count 和 age 都沒有變化。因為我們實現(xiàn)的 useState 并不具備存儲功能,每次重新渲染上一次的 state 就重置了。這里想到可以在外部用個變量來存儲。
3.2 Demo 2: 記憶 state
基于此,我們優(yōu)化一下剛才實現(xiàn)的 useState:
let _state: any function useState(initialValue: any) {_state = _state | initialValuefunction setState(newState: any) {_state = newStaterender(<App />, document.getElementById('root'))}return [_state, setState] }雖然按鈕點擊有變化了,但是效果不太對。如果我們刪掉 age 和 name 這兩個 useState 會發(fā)現(xiàn)效果是正常的。這是因為我們只用了單個變量去儲存,那自然只能存儲一個 useState 的值。那我們想到可以用備忘錄,即一個數(shù)組,去儲存所有的 state,但同時我們需要維護好數(shù)組的索引。
3.3 Demo 3: 備忘錄
基于此,我們再次優(yōu)化一下剛才實現(xiàn)的 useState:
let memoizedState: any[] = [] // hooks 的值存放在這個數(shù)組里 let cursor = 0 // 當前 memoizedState 的索引function useState(initialValue: any) {memoizedState[cursor] = memoizedState[cursor] || initialValueconst currentCursor = cursorfunction setState(newState: any) {memoizedState[currentCursor] = newStatecursor = 0render(<App />, document.getElementById('root'))}return [memoizedState[cursor++], setState] // 返回當前 state,并把 cursor 加 1 }我們點擊三次按鈕之后,打印出 memoizedState 的數(shù)據如下:
打開頁面初次渲染,每次 useState 執(zhí)行時都會將對應的 setState 綁定到對應索引的位置,然后將初始 state 存入 memoizedState 中。
在點擊按鈕的時候,會觸發(fā) setCount 和 setAge,每個 setState 都有其對應索引的引用,因此觸發(fā)對應的 setState 會改變對應位置的 state 的值。
這里是模擬實現(xiàn) useState,所以每次調用 setState 都有一次重新渲染的過程。
重新渲染依舊是依次執(zhí)行 useState,但是 memoizedState 中已經有了上一次是 state 值,因此初始化的值并不是傳入的初始值而是上一次的值。
因此剛才在第二節(jié)中遺留問題的答案就很明顯了,為什么 Hooks 需要確保 Hook 在每一次渲染中都按照同樣的順序被調用?因為 memoizedState 是按 Hooks 定義的順序來放置數(shù)據的,如果 Hooks 的順序變化,memoizedState 并不會感知到。因此最好每次只在最頂層使用 Hook,不要在循環(huán)、條件、嵌套函數(shù)中調用 Hooks。
最后,我們來看看 React 中是怎樣實現(xiàn) useState 的。
4. useState 源碼解析
4.1 入口
首先在入口文件 packages/react/src/React.js 中我們找到 useState,其源自 packages/react/src/ReactHooks.js。
export function useState<S>(initialState: (() => S) | S) {const dispatcher = resolveDispatcher();return dispatcher.useState(initialState); }resolveDispatcher() 返回的是 ReactCurrentDispatcher.current,所以 useState 其實就是 ReactCurrentDispatcher.current.useState。
那么,ReactCurrentDispatcher 是什么?
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';const ReactCurrentDispatcher = {current: (null: null | Dispatcher), }我們最終找到了 packages/react-reconciler/src/ReactFiberHooks.js,在這里有 useState 具體實現(xiàn)。該文件也包含了所有 React Hooks 的核心處理邏輯。
4.2 類型定義
4.2.1 Hook
在開始之前,我們先看看 ReactFiberHooks.js 中幾個類型的定義。首先是 Hooks:
export type Hook = {memoizedState: any, // 指向當前渲染節(jié)點 Fiber, 上一次完整更新之后的最終狀態(tài)值baseState: any, // 初始化 initialState, 已經每次 dispatch 之后 newStatebaseUpdate: Update<any, any> | null, // 當前需要更新的 Update ,每次更新完之后,會賦值上一個 update,方便 react 在渲染錯誤的邊緣,數(shù)據回溯queue: UpdateQueue<any, any> | null, // 緩存的更新隊列,存儲多次更新行為next: Hook | null, // link 到下一個 hooks,通過 next 串聯(lián)每一 hooks };可以看到,Hooks 的數(shù)據結構和我們之前自己實現(xiàn)的基本一致,memoizedState 也是一個數(shù)組,準確來說 React 的 Hooks 是一個單向鏈表,Hook.next 指向下一個 Hook。
4.2.2 Update & UpdateQueue
那么 baseUpdate 和 queue 又是什么呢?先看一下 Update 和 UpdateQueue 的類型定義:
type Update<S, A> = {expirationTime: ExpirationTime, // 當前更新的過期時間suspenseConfig: null | SuspenseConfig,action: A,eagerReducer: ((S, A) => S) | null,eagerState: S | null,next: Update<S, A> | null, // link 下一個 Updatepriority?: ReactPriorityLevel, // 優(yōu)先級 };type UpdateQueue<S, A> = {last: Update<S, A> | null,dispatch: (A => mixed) | null,lastRenderedReducer: ((S, A) => S) | null,lastRenderedState: S | null, };Update 稱作一個更新,在調度一次 React 更新時會用到。UpdateQueue 是 Update 的隊列,同時還帶有更新時的 dispatch。具體的 React Fiber 和 React 更新調度的流程本篇不會涉及,后續(xù)會有單獨的文章補充講解。
4.2.3 HooksDispatcherOnMount & HooksDispatcherOnUpdate
還有兩個 Dispatch 的類型定義需要關注一下,一個是首次加載時的 HooksDispatcherOnMount,另一個是更新時的 HooksDispatcherOnUpdate。
const HooksDispatcherOnMount: Dispatcher = {readContext,useCallback: mountCallback,useContext: readContext,useEffect: mountEffect,useImperativeHandle: mountImperativeHandle,useLayoutEffect: mountLayoutEffect,useMemo: mountMemo,useReducer: mountReducer,useRef: mountRef,useState: mountState,useDebugValue: mountDebugValue,useResponder: createResponderListener, };const HooksDispatcherOnUpdate: Dispatcher = {readContext,useCallback: updateCallback,useContext: readContext,useEffect: updateEffect,useImperativeHandle: updateImperativeHandle,useLayoutEffect: updateLayoutEffect,useMemo: updateMemo,useReducer: updateReducer,useRef: updateRef,useState: updateState,useDebugValue: updateDebugValue,useResponder: createResponderListener, };4.3 首次渲染
4.3.1 renderWithHooks
React Fiber 會從 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork() 開始執(zhí)行(React Fiber 的具體流程后續(xù)單獨成文補充講解),對于 Function Component,其走以下邏輯加載或更新組件:
case FunctionComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateFunctionComponent(current,workInProgress,Component,resolvedProps,renderExpirationTime,);}在 updateFunctionComponent 中,對于 Hooks 的處理是:
nextChildren = renderWithHooks(current,workInProgress,Component,nextProps,context,renderExpirationTime, );因此,我們發(fā)現(xiàn) React Hooks 的渲染核心入口是 renderWithHooks。其他的渲染流程我們并不關心,本文我們著重來看看 renderWithHooks 及其之后的邏輯。
我們回到 ReactFiberHooks.js 來看看 renderWithHooks 具體做了什么,去除容錯代碼和 __DEV__ 的部分,renderWithHooks 代碼如下:
export function renderWithHooks(current: Fiber | null,workInProgress: Fiber,Component: any,props: any,refOrContext: any,nextRenderExpirationTime: ExpirationTime, ): any {renderExpirationTime = nextRenderExpirationTime;currentlyRenderingFiber = workInProgress;nextCurrentHook = current !== null ? current.memoizedState : null;// The following should have already been reset// currentHook = null;// workInProgressHook = null;// remainingExpirationTime = NoWork;// componentUpdateQueue = null;// didScheduleRenderPhaseUpdate = false;// renderPhaseUpdates = null;// numberOfReRenders = 0;// sideEffectTag = 0;// TODO Warn if no hooks are used at all during mount, then some are used during update.// Currently we will identify the update render as a mount because nextCurrentHook === null.// This is tricky because it's valid for certain types of components (e.g. React.lazy)// Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.// Non-stateful hooks (e.g. context) don't get added to memoizedState,// so nextCurrentHook would be null during updates and mounts.ReactCurrentDispatcher.current =nextCurrentHook === null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;let children = Component(props, refOrContext);if (didScheduleRenderPhaseUpdate) {do {didScheduleRenderPhaseUpdate = false;numberOfReRenders += 1;// Start over from the beginning of the listnextCurrentHook = current !== null ? current.memoizedState : null;nextWorkInProgressHook = firstWorkInProgressHook;currentHook = null;workInProgressHook = null;componentUpdateQueue = null;ReactCurrentDispatcher.current = __DEV__? HooksDispatcherOnUpdateInDEV: HooksDispatcherOnUpdate;children = Component(props, refOrContext);} while (didScheduleRenderPhaseUpdate);renderPhaseUpdates = null;numberOfReRenders = 0;}// We can assume the previous dispatcher is always this one, since we set it// at the beginning of the render phase and there's no re-entrancy.ReactCurrentDispatcher.current = ContextOnlyDispatcher;const renderedWork: Fiber = (currentlyRenderingFiber: any);renderedWork.memoizedState = firstWorkInProgressHook;renderedWork.expirationTime = remainingExpirationTime;renderedWork.updateQueue = (componentUpdateQueue: any);renderedWork.effectTag |= sideEffectTag;// This check uses currentHook so that it works the same in DEV and prod bundles.// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.const didRenderTooFewHooks =currentHook !== null && currentHook.next !== null;renderExpirationTime = NoWork;currentlyRenderingFiber = null;currentHook = null;nextCurrentHook = null;firstWorkInProgressHook = null;workInProgressHook = null;nextWorkInProgressHook = null;remainingExpirationTime = NoWork;componentUpdateQueue = null;sideEffectTag = 0;// These were reset above// didScheduleRenderPhaseUpdate = false;// renderPhaseUpdates = null;// numberOfReRenders = 0;return children; }renderWithHooks 包括三個部分,首先是賦值 4.1 中提到的 ReactCurrentDispatcher.current,后續(xù)是做 didScheduleRenderPhaseUpdate 以及一些初始化的工作。核心是第一部分,我們來看看:
nextCurrentHook = current !== null ? current.memoizedState : null;ReactCurrentDispatcher.current =nextCurrentHook === null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;如果當前 Fiber 為空,就認為是首次加載,ReactCurrentDispatcher.current.useState 將賦值成 HooksDispatcherOnMount.useState,否則賦值 HooksDispatcherOnUpdate.useState。根據 4.2 中的類型定義,即首次加載時,useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnMount.useState = mountState;更新時 useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnUpdate.useState = updateState。
4.3.2 mountState
首先看看 mountState 的實現(xiàn):
// 第一次調用組件的 useState 時實際調用的方法 function mountState<S>(initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {// 創(chuàng)建一個新的 Hook,并返回當前 workInProgressHookconst hook = mountWorkInProgressHook();if (typeof initialState === 'function') {initialState = initialState();}hook.memoizedState = hook.baseState = initialState;// 新建一個隊列const queue = (hook.queue = {last: null, // 最后一次更新邏輯, 包括 {action,next} 即狀態(tài)值和下一次 Updatedispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: (initialState: any), // 最后一次渲染組件時的狀態(tài)});const dispatch: Dispatch<BasicStateAction<S>,> = (queue.dispatch = (dispatchAction.bind(null,// 綁定當前 fiber 和 queue.((currentlyRenderingFiber: any): Fiber),queue,): any));return [hook.memoizedState, dispatch]; }4.3.3 mountWorkInProgressHook
mountWorkInProgressHook 是創(chuàng)建一個新的 Hook 并返回當前 workInProgressHook,實現(xiàn)如下:
// 創(chuàng)建一個新的 hook,并返回當前 workInProgressHook function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null,baseState: null,queue: null,baseUpdate: null,next: null,};// 只有在第一次打開頁面的時候,workInProgressHook 為空if (workInProgressHook === null) {firstWorkInProgressHook = workInProgressHook = hook;} else {// 已經存在 workInProgressHook 就將新創(chuàng)建的這個 Hook 接在 workInProgressHook 的尾部。workInProgressHook = workInProgressHook.next = hook;}return workInProgressHook; }4.3.4 dispatchAction
我們注意到 mountState 還做了一件很關鍵的事情,綁定當前 fiber 和 queue 到 dispatchAction 上:
const dispatch: Dispatch<BasicStateAction<S>,> = (queue.dispatch = (dispatchAction.bind(null,// 綁定當前 fiber 和 queue((currentlyRenderingFiber: any): Fiber),queue,): any));那我們看一下 dispatchAction 是如何實現(xiàn)的:
function dispatchAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A, ) {const alternate = fiber.alternate;if (fiber === currentlyRenderingFiber ||(alternate !== null && alternate === currentlyRenderingFiber)) {// 此分支為 re-render 時的 Fiber 調度處理didScheduleRenderPhaseUpdate = true;const update: Update<S, A> = {expirationTime: renderExpirationTime,suspenseConfig: null,action,eagerReducer: null,eagerState: null,next: null,};// 將本次更新周期里的更新記錄緩存進 renderPhaseUpdates 中if (renderPhaseUpdates === null) {renderPhaseUpdates = new Map();}const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);if (firstRenderPhaseUpdate === undefined) {renderPhaseUpdates.set(queue, update);} else {let lastRenderPhaseUpdate = firstRenderPhaseUpdate;while (lastRenderPhaseUpdate.next !== null) {lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;}lastRenderPhaseUpdate.next = update;}} else {const currentTime = requestCurrentTime();const suspenseConfig = requestCurrentSuspenseConfig();const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);// 存儲所有的更新行為,以便在 re-render 流程中計算最新的狀態(tài)值const update: Update<S, A> = {expirationTime,suspenseConfig,action,eagerReducer: null,eagerState: null,next: null,};// Append the update to the end of the list.const last = queue.last;if (last === null) {// This is the first update. Create a circular list.update.next = update;} else {// ... 更新循環(huán)鏈表const first = last.next;if (first !== null) {// Still circular.update.next = first;}last.next = update;}queue.last = update;// 省略特殊情況 Fiber NoWork 時的代碼// 創(chuàng)建一個更新任務,執(zhí)行 fiber 的渲染scheduleWork(fiber, expirationTime);} }if 的第一個分支涉及 Fiber 的調度,我們此處僅是提及,本文不詳細講解 Fiber,只要知道 fiber === currentlyRenderingFiber 時是 re-render,即當前更新周期中又產生了新的周期即可。如果是 re-render,didScheduleRenderPhaseUpdate 置為 true,而在 renderWithHooks 中 如果 didScheduleRenderPhaseUpdate 為 true,就會循環(huán)計數(shù) numberOfReRenders 來記錄 re-render 的次數(shù);另外 nextWorkInProgressHook 也會有值。所以后續(xù)的代碼中,有用 numberOfReRenders > 0 來判斷是否是 re-render 的,也有用 nextWorkInProgressHook 是否為空來判斷是否是 re-render 的。
同時,如果是 re-render,會把所有更新過程中產生的更新記錄在 renderPhaseUpdates 這個 Map 上,以每個 Hook 的 queue 為 key。
至于最后 scheduleWork 的具體工作,我們后續(xù)單獨成文來分析。
4.4 更新
4.4.1 updateState
我們看看更新過程中的 useState 時實際調用的方法 updateState:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action; }// 第一次之后每一次執(zhí)行 useState 時實際調用的方法 function updateState<S>(initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {return updateReducer(basicStateReducer, (initialState: any)); }可以發(fā)現(xiàn),其實 updateState 最終調用的其實是 updateReducer。對于 useState 觸發(fā)的 update action 來說,basicStateReducer 就是直接返回 action 的值(如果 action 是函數(shù)還會幫忙調用一下)。因此,useState 只是 useReduer 的一個特殊情況而已,其傳入的 reducer 為
basicStateReducer,負責改變 state,而非 useReducer 那樣可以傳入自定義的 reducer。
4.4.2 updateReducer
那我們來看看 updateReducer 做了些什么:
function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: I => S, ): [S, Dispatch<A>] {// 獲取當前正在工作中的 hookconst hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;if (numberOfReRenders > 0) {// re-render:當前更新周期中產生了新的更新const dispatch: Dispatch<A> = (queue.dispatch: any);if (renderPhaseUpdates !== null) {// 所有更新過程中產生的更新記錄在 renderPhaseUpdates 這個 Map上,以每個 Hook 的 queue 為 key。const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);if (firstRenderPhaseUpdate !== undefined) {renderPhaseUpdates.delete(queue);let newState = hook.memoizedState;let update = firstRenderPhaseUpdate;do {// 如果是 re-render,繼續(xù)執(zhí)行這些更新直到當前渲染周期中沒有更新為止const action = update.action;newState = reducer(newState, action);update = update.next;} while (update !== null);if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();}hook.memoizedState = newState;if (hook.baseUpdate === queue.last) {hook.baseState = newState;}queue.lastRenderedState = newState;return [newState, dispatch];}}return [hook.memoizedState, dispatch];}const last = queue.last;const baseUpdate = hook.baseUpdate;const baseState = hook.baseState;let first;if (baseUpdate !== null) {if (last !== null) {last.next = null;}first = baseUpdate.next;} else {first = last !== null ? last.next : null;}if (first !== null) {let newState = baseState;let newBaseState = null;let newBaseUpdate = null;let prevUpdate = baseUpdate;let update = first;let didSkip = false;do {const updateExpirationTime = update.expirationTime;if (updateExpirationTime < renderExpirationTime) {if (!didSkip) {didSkip = true;newBaseUpdate = prevUpdate;newBaseState = newState;}if (updateExpirationTime > remainingExpirationTime) {remainingExpirationTime = updateExpirationTime;}} else {markRenderEventTimeAndConfig(updateExpirationTime,update.suspenseConfig,);// 循環(huán)鏈表,執(zhí)行每一次更新if (update.eagerReducer === reducer) {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) {newBaseUpdate = prevUpdate;newBaseState = newState;}if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();}hook.memoizedState = newState;hook.baseUpdate = newBaseUpdate;hook.baseState = newBaseState;queue.lastRenderedState = newState;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch]; }updateReducer 分為兩種情況:
在 4.3.4 中我們提到 numberOfReRenders 記錄了 re-render 的次數(shù),如果大于 0 說明當前更新周期中又產生了新的更新,那么就繼續(xù)執(zhí)行這些更新,根據 reducer 和 update.action 來創(chuàng)建新的 state,直到當前渲染周期中沒有更新為止,最后賦值給 Hook.memoizedState 以及 Hook.baseState。
注:其實單獨使用 useState 的話幾乎不會遇到 re-render 的場景,除非直接把 setState 寫在函數(shù)的頂部,但是這樣會導致無限 re-render,numberOfReRenders 會突破限制,在 4.3.4 dispatchAction 中讓程序報錯(4.3.4 隱去了 __DEV__ 與這部分容錯代碼):invariant(numberOfReRenders < RE_RENDER_LIMIT,'Too many re-renders. React limits the number of renders to prevent ' +'an infinite loop.',);那么再來看一下非 re-render 的情況,除去 Fiber 相關的代碼和特殊邏輯,重點在于 do-while 循環(huán),這段代碼負責循環(huán)鏈表,執(zhí)行每一次更新:
do {// 循環(huán)鏈表,執(zhí)行每一次更新if (update.eagerReducer === reducer) {newState = ((update.eagerState: any): S);} else {const action = update.action;newState = reducer(newState, action);}prevUpdate = update;update = update.next; } while (update !== null && update !== first);還有一點需要注意,在這種情況下需要對每一個 update 判斷優(yōu)先級,如果不是當前整體更新優(yōu)先級內的更新會被跳過,第一個跳過的 update 會變成新的 Hook.baseUpdate。需要保證后續(xù)的更新要在 baseUpdate 更新之后的基礎上再次執(zhí)行,因此結果可能會不一樣。這里的具體邏輯后續(xù)會成文單獨解析。最后同樣需要賦值給 Hook.memoizedState 以及 Hook.baseState。
4.4.3 updateWorkInProgressHook
這里補充一下,注意到第一行代碼獲取 Hook 的方式就與 mountState 不同,updateWorkInProgressHook 是獲取當前正在工作中的 Hook。實現(xiàn)如下:
// 獲取當前正在工作中的 Hook,即 workInProgressHook function updateWorkInProgressHook(): Hook {if (nextWorkInProgressHook !== null) {// There's already a work-in-progress. Reuse it.workInProgressHook = nextWorkInProgressHook;nextWorkInProgressHook = workInProgressHook.next;currentHook = nextCurrentHook;nextCurrentHook = currentHook !== null ? currentHook.next : null;} else {// Clone from the current hook.currentHook = nextCurrentHook;const newHook: Hook = {memoizedState: currentHook.memoizedState,baseState: currentHook.baseState,queue: currentHook.queue,baseUpdate: currentHook.baseUpdate,next: null,};if (workInProgressHook === null) {workInProgressHook = firstWorkInProgressHook = newHook;} else {workInProgressHook = workInProgressHook.next = newHook;}nextCurrentHook = currentHook.next;}return workInProgressHook; }這里分為兩種情況,在 4.3.4 中我們提到如果 nextWorkInProgressHook 存在那么就是 re-render,如果是 re-render 說明當前更新周期中還要繼續(xù)處理 workInProgressHook。
如果不是 re-render,就取下一個 Hook 為當前的 Hook,同時像 4.3.3 mountWorkInProgressHook 一樣,新建一個 Hook 并返回 workInProgressHook。
總之,updateWorkInProgressHook 獲取到了當前工作中的 workInProgressHook。
5 結語
直觀一點,我截了一個 Hook 在運行中的數(shù)據結構,如下圖所示:
總結一下上文中解析的流程,如下圖所示:
如果對于 useState 的源碼仍有所疑惑,可以自己寫個小 Demo 在關鍵函數(shù)打斷點調試一下。
總結
以上是生活随笔為你收集整理的usestate中的回调函数_React Hooks 源码解析(3):useState的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python打出由边框包围的_pytho
- 下一篇: python编写木马攻击_用Python