博客 / 詳情

返回

React hooks原理淺談

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。

(本文作者:尚軍平)

圖片

user avatar xiangjiaochihuanggua 頭像 tigerandflower 頭像 columsys 頭像 buxia97 頭像 musicfe 頭像 mmmy_a 頭像 caideheirenyagao 頭像 liujunqi 頭像 u_12219 頭像 moziyu 頭像 shellingfordly 頭像 y_lucky 頭像
31 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.