react的工作流程
fiber是react的基本工作單元,所有的操作都要基於它實現。其實fiber就類似一個個element元素,react的工作流程其實就是遍歷fiber tree。
performUnitOfWork函數會執行當前的fiber節點,然後把這個fiber的子節點賦值給workInProgress,當子節點不存在時,就把兄弟節點賦值給workInProgress。
上層的workLoopSync函數的 while循環會根據下個workInProgress去遍歷。這樣就能實現一個深度優先遍歷,從而把所有的fiber執行完畢。
在performUnitOfWork函數中分為兩個階段:
1.beginWork
- 執行render函數以及hook,然後返回jsx
- 對返回的jsx執行diff,如果有新的fiber節點生成則賦值給workInProgress繼續迭代
2.completeWork回溯fiber tree
- 生成dom節點,組成一個虛擬dom樹
- 處理props
- 把所有含有副作用的fiber節點用firstEffect和lastEffect鏈接起來,組成一個鏈表,然後在commit階段遍歷執行
在completeWork執行到根節點時,證明所有的工作已經完成,就會執行commitRoot,它又分為三個階段:
1.before mutation(執行dom操作前)
調用掛載前的生命週期鈎子,比如getSnapshotBeforeUpdate,調度useEffect
2.mutation(執行dom操作)
執行dom操作,如果有組件被刪除,那麼還會調用componentWilUnmount或useLayoutEffect的銷燬函數
3.layout(執行dom操作後)
- 切換fiber tree
- 調用componentDidUpdate、componentDidMount或者useLayoutEffect的回調函數。
- layout結束後,執行之前調度的useEffect的創建和銷燬函數。
接下來我們重點看下hook的實現。
useState不同階段調用的方法不同
因而useState在mount時實際上調用的是mountState方法,update時調用的是updateState方法(updateState是updateReducer的語法糖寫法)。
當mount階段依次調用hook時,第一個生成的hook是掛在當前組件節點(reactFiber節點)的memoizedState屬性上,之後生成的hook則依次掛在上一個hook的next屬性上。
所以當我們將hook置於循環、條件語句、嵌套函數中時,那麼hook鏈表就會錯亂,會導致hook調用順序不可預測,那就沒法保證組件內部狀態一致性。當我們setState時會返回一個初始state和用於更新state的函數。
我們知道mount階段useState調用的是mountState,查看源碼後知道返回的其實是[hook.memoizedState, dispatch]。
dispatch其實就是dispatchSetState通過bind到當前組件節點、更新隊列後的函數。
假設執行了3次setOrder,分別是 setOrder('1')、setOrder('2')、setOrder('3')。
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;
setOrder('1')時
setOrder('2') 時
setOrder('3')時
上面説過updte階段實際調用的是updateReducer方法,這個方法中主要做了這幾件事
- 如果有新的更新還未處理,則加入當前更新鏈表中
- 清空待更新鏈表(queue.pending = null)
- 從待更新鏈表的第一個循環迭代更新,直到最後一個
- 更新當前hook狀態值並return出去
通過setOrder(1)、setOrder(2)、setOrder(3)的圖例我們知曉 queue.pending.next 即更新鏈表的總是指向第一個update,而queue.pending總是指向最後一個。
一開始將update(1)賦值給update,然後獲取newState也就是1,接下來update=update.next,此時update成了update(2),依次遍歷,終止條件為update === null || update === first,也就是當update = update(3)時滿足了終止條件,此時newState = 3,取到了最新值。這樣可以保證整個update鏈表都循環了一遍同時取到的是鏈表中的最後一個節點。所以無論setState多少次,拿到的總是最新的值(問題2)。
useEffect不同階段調用的方法不同
mount階段,useEffect調用的是mountEffect,update階段,useEffect調用的是updateEffect函數。
無論useEffect的依賴項是否相同都會調用pushEffect函數,唯一區別的是pushEffect函數的第一個參數是不同的,如果依賴項沒有變化則第一個參數是hookFlags,反之則是HookHasEffect|hookFlags(標識存在副作用更新鈎子)。
pushEffect主要做兩件事:
- 創建 effect 對象並返回
- 把這個 effect 鏈接到 currentlyRenderingFiber的updateQueue屬性上
結論:useEffect會生成一個effect對象,保存在hook節點的memoizedState中,同時也更新到currentlyRenderingFiber的updateQueue中,組成循環鏈表。每次render時,都會對比一下新舊hook裏保存的effect的deps有沒有改變,如果改變了,那就更新memoizedState為最新的effect,並且把effect的tag標識為存在副作用,然後currentlyRenderingFiber的updateQueue屬性裏。在commit階段,beforeMutation中,對有副作用的fiber,發起一個異步調度。等到layout結束後,這個異步調度的回調開始執行,處理effect的創建和銷燬回調。它會先調用effect的destroy,再調用create。
(本文作者:尚軍平)