網(wǎng)站首頁 編程語言 正文
前言
在深究 React 的 setState 原理的時候,我們先要考慮一個問題:setState 是異步的嗎?
首先以 class component 為例,請看下述代碼(demo-0)
class App extends React.Component { state = { count: 0 } handleCountClick = () => { this.setState({ count: this.state.count + 1 }); console.log(this.state.count); } render() { return ( <div className='app-box'> <div onClick={this.handleCountClick}>the count is {this.state.count}</div> </div> ) } } ReactDOM.render( <App />, document.getElementById('container') );
count
初始值為 0,當(dāng)我們觸發(fā)handleCountClick
事件的時候,執(zhí)行了count + 1
操作,并打印了count
,此時打印出的count
是多少呢?答案不是 1 而是 0
類似的 function component 與 class component 原理一致。現(xiàn)在我們以 function component 為例,請看下述代碼 (demo-1)
const App = function () { const [count, setCount] = React.useState(0); const handleCountClick = () => { setCount((count) => { return count + 1; }); console.log(count); } return <div className='app-box'> <div onClick={handleCountClick}>the count is {count}</div> </div> } ReactDOM.render( <App />, document.getElementById('container') );
同樣的,這里打印出的 count
也為 0
相信大家都知道這個看起來是異步的現(xiàn)象,但他真的是異步的嗎?
為什么setState看起來是異步的
首先得思考一個問題:如何判斷這個函數(shù)是否為異步?
最直接的,我們寫一個 setTimeout
,打個 debugger 試試看
我們都知道 setTimeout
里的回調(diào)函數(shù)是異步的,也正如上圖所示,chrome 會給 setTimeout
打上一個 async
的標(biāo)簽。
接下來我們 debugger setState
看看
React.useState
返回的第二個參數(shù)實際就是這個 dispatchSetState
函數(shù)(下文細(xì)說)。但正如上圖所示,這個函數(shù)并沒有 async
標(biāo)簽,所以 setState
并不是異步的。
那么拋開這些概念來看,上文中 demo-1 的類似異步的現(xiàn)象是怎么發(fā)生的呢?
簡單的來說,其步驟如下所示。基于此,我們接下來更深入的看看 React 在這個過程中做了什么
從first paint開始
first paint 就是『首次渲染』,為突出顯示,就用英文代替。
這里先簡單看一下App往下的 fiber tree 結(jié)構(gòu)。每個 fiber node 還有一個return
指向其 parent fiber node,這里就不細(xì)說了
我們都知道 React 渲染的時候,得遍歷一遍 fiber tree,當(dāng)走到 App 這個 fiber node 的時候發(fā)生了什么呢?
接下來我們看看詳細(xì)的代碼(這里的 workInProgress 就是整在處理的 fiber node,不關(guān)心的代碼已刪除)
首先要注意的是,雖然 App 是一個 FunctionComponent
,但是在 first paint 的時候,React 判斷其為 IndeterminateComponent
。
switch (workInProgress.tag) { // workInProgress.tag === 2 case IndeterminateComponent: { return mountIndeterminateComponent( current, workInProgress, workInProgress.type, renderLanes ); } // ... case FunctionComponent: { /** ... */} }
接下來走進這個 mountIndeterminateComponent
,里頭有個關(guān)鍵的函數(shù) renderWithHooks
;而在 renderWithHooks
中,我們會根據(jù)組件處于不同的狀態(tài),給 ReactCurrentDispatcher.current
掛載不同的 dispatcher
。而在first paint 時,掛載的是HooksDispatcherOnMountInDEV
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) { value = renderWithHooks( null, workInProgress, Component, props, context, renderLanes ); } function renderWithHooks() { // ... if (current !== null && current.memoizedState !== null) { // 此時 React 認(rèn)為組件在更新 ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV; } else if (hookTypesDev !== null) { // handle edge case,這里我們不關(guān)心 } else { // 此時 React 認(rèn)為組件為 first paint 階段 ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; } // ... var children = Component(props, secondArg); // 調(diào)用我們的 Component }
這個 HooksDispatcherOnMountInDEV
里就是組件 first paint 的時候所用到的各種 hooks,相關(guān)參考視頻講解:進入學(xué)習(xí)
HooksDispatcherOnMountInDEV = { // ... useState: function (initialState) { currentHookNameInDev = 'useState'; mountHookTypesDev(); var prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV; try { return mountState(initialState); } finally { ReactCurrentDispatcher.current = prevDispatcher; } }, // ... }
接下里走進我們的 App()
,我們會調(diào)用 React.useState
,點進去看看,代碼如下。這里的 dispatcher
就是上文掛載到 ReactCurrentDispatcher.current
的 HooksDispatcherOnMountInDEV
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } // ... HooksDispatcherOnMountInDEV = { // ... useState: function (initialState) { currentHookNameInDev = 'useState'; mountHookTypesDev(); var prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV; try { return mountState(initialState); } finally { ReactCurrentDispatcher$1.current = prevDispatcher; } }, // ... }
這里會調(diào)用 mountState
函數(shù)
function mountState(initialState) { var hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { // $FlowFixMe: Flow doesn't like mixed types initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; var queue = { pending: null, interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; hook.queue = queue; var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }
這個函數(shù)做了這么幾件事情:
執(zhí)行 mountWorkInProgressHook
函數(shù):
function mountWorkInProgressHook() { var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; if (workInProgressHook === null) { // This is the first hook in the list currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
- 創(chuàng)建一個
hook
- 若無
hook
鏈,則創(chuàng)建一個hook
鏈;若有,則將新建的hook
加至末尾 - 將新建的這個
hook
掛載到workInProgressHook
以及當(dāng)前 fiber node 的memoizedState
上 - 返回
workInProgressHook
,也就是這個新建的hook
判斷傳入的 initialState
是否為一個函數(shù),若是,則調(diào)用它并重新賦值給 initialState
(在我們的demo-1里是『0』)
將 initialState
掛到 hook.memoizedState
以及 hook.baseState
給 hook
上添加一個 queue
。這個 queue
有多個屬性,其中queue.dispatch
掛載的是一個 dispatchSetState
。這里要注意一下這一行代碼
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
Function.prototype.bind
的第一個參數(shù)都知道是綁 this 的,后面兩個就是綁定了 dispatchSetState
所需要的第一個參數(shù)(當(dāng)前fiber)和第二個參數(shù)(當(dāng)前queue)。
這也是為什么雖然 dispatchSetState
本身需要三個參數(shù),但我們使用的時候都是 setState(params)
,只用傳一個參數(shù)的原因。
返回一個數(shù)組,也就是我們常見的 React.useState
返回的形式。此時這個 state
是 0 至此為止,React.useState
在 first paint 里做的事兒就完成了,接下來就是正常渲染,展示頁面
觸發(fā)組件更新
要觸發(fā)組件更新,自然就是點擊這個綁定了事件監(jiān)聽的 div
,觸發(fā) setCount
。回憶一下,這個 setCount
就是上文講述的,暴露出來的 dispatchSetState
。并且正如上文所述,我們傳進去的參數(shù)實際上是 dispatchSetState
的第三個參數(shù) action
。(這個函數(shù)自然也涉及一些 React 執(zhí)行優(yōu)先級的判斷,不在本文的討論范圍內(nèi)就省略了)
function dispatchSetState(fiber, queue, action) { var update = { lane: lane, action: action, hasEagerState: false, eagerState: null, next: null }; enqueueUpdate(fiber, queue, update); }
dispatchSetState
做了這么幾件事
創(chuàng)建一個 update
,把我們傳入的 action
放進去
進入 enqueueUpdate
函數(shù):
- 若
queue
上無update
鏈,則在queue
上以 剛創(chuàng)建的update
為頭節(jié)點構(gòu)建update
鏈 - 若
queue
上有update
鏈,則在該鏈的末尾添加這個 剛創(chuàng)建的update
function enqueueUpdate(fiber, queue, update, lane) { var pending = queue.pending; if (pending === null) { // This is the first update. Create a circular list. update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; var lastRenderedReducer = queue.lastRenderedReducer; var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); update.hasEagerState = true; update.eagerState = eagerState; }
- 根據(jù)
queue
上的各個參數(shù)(reducer、上次計算出的 state)計算出eagerState
,并掛載到當(dāng)前update
上
到此,我們實際上更新完 state
了,這個新的 state
掛載到哪兒了呢?在 fiber.memoizedState.queue.pending
上。注意:
-
fiber
即為當(dāng)前的遍歷到的 fiber node; -
pending
是一個環(huán)狀鏈表
此時我們打印進行打印,但這里打印的還是 first paint 里返回出來的 state
,也就是 0
更新渲染fiber tree
現(xiàn)在我們更新完 state,要開始跟新 fiber tree 了,進行最后的渲染。邏輯在 performSyncWorkOnRoot
函數(shù)里,同樣的,不關(guān)心的邏輯我們省略
function performSyncWorkOnRoot(root) { var exitStatus = renderRootSync(root, lanes); }
同樣的我們先看一眼 fiber tree 更新過程中 與 useState 相關(guān)的整個流程圖
首先我們走進 renderRootSync
,這個函數(shù)作用是遍歷一遍 fiber tree,當(dāng)遍歷的 App
時,此時的類型為 FunctionComponent
。還是我們前文所說的熟悉的步驟,走進 renderWithHooks
。注意此時 React 認(rèn)為該組件在更新了,所以給 dispatcher
掛載的就是 HooksDispatcherOnUpdateInDEV
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) { var children = Component(props, secondArg); }
我們再次走進 App
,這里又要再次調(diào)用 React.useState
了
const App = function () { const [count, setCount] = React.useState(0); const handleCountClick = () => { setCount(count + 1); } return <div className='app-box'> <div onClick={handleCountClick}>the count is {count}</div> </div> }
與之前不同的是,這次所使用的 dispatch
為 HooksDispatcherOnUpdateInDEV
。那么這個 dispatch
下的 useState
具體做了什么呢?
useState: function (initialState) { currentHookNameInDev = 'useState'; updateHookTypesDev(); var prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { return updateState(initialState); } finally { ReactCurrentDispatcher$1.current = prevDispatcher; } }
可以看到大致都差不多,唯一不同的是,這里調(diào)用的是 updateState
,而之前是 mountState
。
function updateState(initialState) { return updateReducer(basicStateReducer); }
function updateReducer(reducer, initialArg, init) { var first = baseQueue.next; var newState = current.baseState; do { // 遍歷更新 newState update = update.next; } while (update !== null && update !== first); hook.memoizedState = newState; queue.lastRenderedState = newState; return [hook.memoizedState, dispatch]; }
這里又調(diào)用了 updateReducer
,其中代碼很多不一一展示,關(guān)鍵步驟就是:
- 遍歷我們之前掛載到
fiber.memoizedState.queue.pending
上的環(huán)狀鏈表,并得到最后的newState
- 更新
hook
、queue
上的相關(guān)屬性,也就是將最新的這個state
記錄下來,這樣下次更新的時候可以這次為基礎(chǔ)再去更新 - 返回一個數(shù)組,形式為
[state, setState]
,此時這個state
即為計算后的newState
,其值為 1
接下來就走進 commitRootImpl
進行最后的渲染了,這不是本文的重點就不展開了,里頭涉及 useEffect
等鉤子函數(shù)的調(diào)用邏輯。
最后看一眼整個詳細(xì)的流程圖
寫在最后
上文只是描述了一個最簡單的 React.useState
使用場景,各位可以根據(jù)本文配合源碼,進行以下兩個嘗試:
Q1. 多個 state
的時候有什么變化?例如以下場景時:
const App = () => { const [count, setCount] = React.useState(0); const [str, setStr] = React.useState(''); // ... }
A1. 將會構(gòu)建一個上文所提到的 hook
鏈
Q2. 對同個 state
多次調(diào)用 setState
時有什么變化?例如以下場景:
const App = () => { const [count, setCount] = React.useState(0); const handleCountClick = () => { setCount(count + 1); setCount(count + 2); } return <div className='app-box'> <div onClick={handleCountClick}>the count is {count}</div> </div> }
A2. 將會構(gòu)建一個上文所提到的 update
鏈
原文鏈接:https://blog.csdn.net/weixin_59558923/article/details/127382555
相關(guān)推薦
- 2023-10-15 理解C/C++中的鏈接
- 2022-10-17 Android數(shù)據(jù)存儲方式操作模式解析_Android
- 2022-12-24 Python中通過@classmethod?實現(xiàn)多態(tài)的示例_python
- 2022-05-20 python繪制餅圖的方法詳解_python
- 2022-06-27 精簡高效的C#網(wǎng)站優(yōu)化經(jīng)驗技巧總結(jié)_C#教程
- 2022-09-24 ASP.NET?MVC實現(xiàn)文件下載_實用技巧
- 2022-06-09 python?Tkinter模塊使用方法詳解_python
- 2022-07-15 初識python的numpy模塊_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支