React 的hooks 是在fiber 之後出現的特性,所以很多人誤以為hooks 是必須依賴fiber 才能實現的,其實並不是,它們倆沒啥必然聯繫。
現在,不只react 中實現了hooks,在preact、react ssr、midway 等框架中也實現了這個特性,它們的實作就是不依賴fiber 的。
我們分別來看看這些不同框架中的hooks 都是怎麼實現的:
react 是透過jsx 描述介面的,它會被babel 或tsc 等編譯工具編譯成render function,然後執行產生vdom:
這裡的render function 在React17 之前是React.createElement:
在React 17 之後換成了jsx:
這個jsx-runtime 會自動引入,不用像之前那樣每個元件都要保留一個React 的import 才行。
render function 執行產生vdom:
vdom 的結構是這樣的:
在React16 之前,會遞歸渲染這個vdom,並增刪改真實dom。
而在React16 引進了fiber 架構之後就多了一步:先把vdom 轉成fiber,之後再渲染fiber。
vdom 轉fiber 的過程叫做reconcile,最後增刪改真實dom 的過程叫做commit。
為什麼要做這樣的轉換呢?
因為vdom 只有子節點children 的引用,沒有父節點parent 和其他兄弟節點sibling 的引用,這導致了要一次性遞歸把所有vdom 節點渲染到dom 才行,不可打斷。
萬一打斷了會怎麼樣呢?因為沒有記錄父節點和兄弟節點,那隻能繼續處理子節點,卻不能處理vdom 的其他部分了。
所以React 才引入了這種fiber 的結構,也就是有父節點return、子節點child、兄弟節點sibling 等引用,可以打斷,因為斷了再恢復也能找到後面所有沒處理過的節點。
fiber 節點的結構是這樣的:
這個過程可以打斷,自然也就可以調度,也就是schdule 的過程。
所以fiber 架構就分為了schdule、reconcile(vdom 轉fiber)、commit(更新到dom)三個階段。
函數元件內可以用hooks 來存取一些值,這些值就是存在fiber 節點上的。
例如這個函數元件內用到了6 個hook:
那麼對應的fiber 節點上就有個6 個元素的memorizedState 鍊錶:
透過next 串聯起來:
不同的hook 在memorizedState 鍊錶不同的元素上存取值,這就是react hooks 的原理。
這個鍊錶有創建階段和更新階段,所以你會發現useXxx 的最終實作都分為了mountXxx 和updateXxx:
這裡的mount 階段就是創建hook 節點並組裝成鍊錶的:
會把創造好的hook 鍊錶掛到fiber 節點的memorizedState 屬性上。
那更新的時候自然也就能從fiber 節點上取出這個hook 鍊錶:
這樣在多次渲染中,useXxx 的api 都能在fiber 節點上找到對應的memorizedState。
這就是react hooks 的原理,可以看到它是把hook 存在fiber 節點上的。
那preact 有什麼不同呢?
preact 是相容react 程式碼的更輕量級的框架,它支援class 元件和function 元件,也支援了hooks 等react 特性。不過它沒有實作fiber 架構。
因為它主要考慮的是體積的極致(只有3kb),而不是效能的極致。
剛才我們了解了react 是把hook 鍊錶存放在fiber 節點上的,那preact 沒有fiber 節點,會把hook 鍊錶存在哪呢?
其實也很容易想到,fiber 只是對vdom 做了下改造用於提升性能的,和vdom 沒啥本質的區別,那就把hook 存在vdom 上不就行了?
確實,preact 就是把hook 鍊錶放在了vdom 上。
例如這個有4 個hooks 的函數元件:
它的實作就是在vdom 上存取對應的hook:
它沒有像react 那樣把hook 分成mount 和update 兩個階段,而是合併到一起處理了。
如圖,它把hooks 存在了component.__hooks 的陣列上,透過下標存取。
這個component 就是vdom 上的一個屬性:
也就是把hooks 的值存在了vnode._component._hooks 的陣列上。
比較下react 與preact 實作hooks 的差異:
react 中是把hook 鍊錶存放在fiberNode.memorizedState 屬性上,preact 中是把hook 鍊錶存放在vnode._component._hooks 屬性上
react 中的hook 鍊錶通過next 串聯,preact 中的hook 鍊錶就是一個數組,透過下標存取
react 把hook 鍊錶的創建和更新分離開,也就是useXxx 會分為mountXxx 和updateXxx 來實現,而preact 中合併在一起處理的
所以說, hooks 的實現並不依賴fiber,它只不過是找個地方存放元件對應的hook 的數據,渲染時能取到就行,存放在哪裡是無所謂的。
因為vdom、fiber 和元件渲染強相關,所以存放在了這些結構上。
像是react ssr 實作hooks,就既沒有存在fiber 上,也沒有存在vdom 上:
其實react-dom 套件除了可以做csr 外,也可以做ssr:
csr 時使用react-dom 的render 方法:
ssr 的時候使用react-dom/server 的renderToString 方法或renderToStream 方法:
大家覺得ssr 的時候會做vdom 到fiber 的轉換麼?
肯定不會呀,fiber 是為了提高在瀏覽器中運行時的渲染性能,把計算變成可打斷的,在空閒時做計算,才引入的一種結構。
服務端渲染自然就不需要fiber。
不需要fiber 的話,它把hook 鍊錶存放在哪裡呢? vdom 麼?
確實可以放在vdom,但其實並沒有。
如useRef 這個hooks:
它是從firstWorkInProgressHook 開始的用next 串聯的一個鍊錶。
而firstWorkInProgressHook 最開始用createHook 創建的第一個hook 節點:
並沒有掛載到vdom 上。
為什麼呢?
因為ssr 只需要渲染一次呀,又不需要更新,自然沒必要掛到vdom 上。
只要每次處理完每個元件的hooks 就清除一下這個hook 鍊錶就行:
所以,react ssr 時,hooks 是存在全域變數上的。
對比下react csr 和ssr 時的hooks 實現原理的區別:
csr 時會從vdom 創建fiber,用於把渲染變成可打斷的,通過空閒調度來提高性能,而ssr 時不會,是vdom 直接渲染的
csr 時把hooks 保存到了fiber 節點上,ssr 時是直接放在了全域變數上,每個元件處理完就清空。因為不會用第二次了
csr 時會把hook 的建立和更新分成mount 和update 兩個階段,而ssr 因為只會處理一次,只有創建階段
hooks 的實作原理其實不複雜,就是在某個上下文中存放一個鍊錶,然後hooks api 從鍊錶不同的元素上存取對應的資料來完成各自的邏輯。這個上下文可以是vdom、fiber 甚至是全域變數。
不過hooks 這個想法還挺火的,淘寶出的服務端框架midway 就在引進了hooks 的想法:
midway 是一個Node.js 框架:
服務端框架自然就沒有vdom、fiber 這種結構,不過hooks 的想法並不依賴這些,實作hooks 的api 只需要在某個上下文放一個鍊錶就行。
midway 就實作了類似react hooks 的api:
具體它這個hook 鍊錶存在哪我還沒看,不過我們已經掌握hooks 的實作原理了,只要有個上下文存放hook 鍊錶就行,在哪都可以。
react hooks 是在react fiber 架構之後出現的特性,很多人誤以為hooks 必須配合fiber 才能實現,我們分別看了react、preact、react ssr、midway 中的hooks 的實現,發現並不是這樣的:
所以,react hooks 必須依賴fiber 才能實現麼?
明顯不是,搭配fiber、搭配vdom、搭配全域變量,甚至任何一個上下文都可以。