React Hooks 的實(shí)現(xiàn)必須依賴 Fiber 么?
React 的 hooks 是在 fiber 之后出現(xiàn)的特性,所以很多人誤以為 hooks 是必須依賴 fiber 才能實(shí)現(xiàn)的,其實(shí)并不是,它們倆沒(méi)啥必然聯(lián)系。
現(xiàn)在,不止 react 中實(shí)現(xiàn)了 hooks,在 preact、react ssr、midway 等框架中也實(shí)現(xiàn)了這個(gè)特性,它們的實(shí)現(xiàn)就是不依賴 fiber 的。
我們分別來(lái)看一下這些不同框架中的 hooks 都是怎么實(shí)現(xiàn)的:
react 如何實(shí)現(xiàn) hooks
react 是通過(guò) jsx 描述界面的,它會(huì)被 babel 或 tsc 等編譯工具編譯成 render function,然后執(zhí)行產(chǎn)生 vdom:
這里的 render function 在 React17 之前是 React.createElement:
在 React 17 之后換成了 jsx:
這個(gè) jsx-runtime 會(huì)自動(dòng)引入,不用像之前那樣每個(gè)組件都要保留一個(gè) React 的 import 才行。
render function 執(zhí)行產(chǎn)生 vdom:
vdom 的結(jié)構(gòu)是這樣的:
在 React16 之前,會(huì)遞歸渲染這個(gè) vdom,增刪改真實(shí) dom。
而在 React16 引入了 fiber 架構(gòu)之后就多了一步:首先把 vdom 轉(zhuǎn)成 fiber,之后再渲染 fiber。
vdom 轉(zhuǎn) fiber 的過(guò)程叫做 reconcile,最后增刪改真實(shí) dom 的過(guò)程叫做 commit。
為什么要做這樣的轉(zhuǎn)換呢?
因?yàn)?vdom 只有子節(jié)點(diǎn) children 的引用,沒(méi)有父節(jié)點(diǎn) parent 和其他兄弟節(jié)點(diǎn) sibling 的引用,這導(dǎo)致了要一次性遞歸把所有 vdom 節(jié)點(diǎn)渲染到 dom 才行,不可打斷。
萬(wàn)一打斷了會(huì)怎么樣呢?因?yàn)闆](méi)有記錄父節(jié)點(diǎn)和兄弟節(jié)點(diǎn),那只能繼續(xù)處理子節(jié)點(diǎn),卻不能處理 vdom 的其他部分了。
所以 React 才引入了這種 fiber 的結(jié)構(gòu),也就是有父節(jié)點(diǎn) return、子節(jié)點(diǎn) child、兄弟節(jié)點(diǎn) sibling 等引用,可以打斷,因?yàn)閿嗔嗽倩謴?fù)也能找到后面所有沒(méi)處理過(guò)的節(jié)點(diǎn)。
fiber 節(jié)點(diǎn)的結(jié)構(gòu)是這樣的:
這個(gè)過(guò)程可以打斷,自然也就可以調(diào)度,也就是 schdule 的過(guò)程。
所以 fiber 架構(gòu)就分為了 schdule、reconcile(vdom 轉(zhuǎn) fiber)、commit(更新到 dom)三個(gè)階段。
函數(shù)組件內(nèi)可以用 hooks 來(lái)存取一些值,這些值就是存在 fiber 節(jié)點(diǎn)上的。
比如這個(gè)函數(shù)組件內(nèi)用到了 6 個(gè) hook:
那么對(duì)應(yīng)的 fiber 節(jié)點(diǎn)上就有個(gè) 6 個(gè)元素的 memorizedState 鏈表:
通過(guò) next 串聯(lián)起來(lái):
不同的 hook 在 memorizedState 鏈表不同的元素上存取值,這就是 react hooks 的原理。
這個(gè)鏈表有創(chuàng)建階段和更新階段,所以你會(huì)發(fā)現(xiàn) useXxx 的最終實(shí)現(xiàn)都分為了 mountXxx 和 updateXxx:
這里的 mount 階段就是創(chuàng)建 hook 節(jié)點(diǎn)并組裝成鏈表的:
會(huì)把創(chuàng)建好的 hook 鏈表掛到 fiber 節(jié)點(diǎn)的 memorizedState 屬性上。
那更新的時(shí)候自然也就能從 fiber 節(jié)點(diǎn)上取出這個(gè) hook 鏈表:
這樣在多次渲染中,useXxx 的 api 都能在 fiber 節(jié)點(diǎn)上找到對(duì)應(yīng)的 memorizedState。
這就是 react hooks 的原理,可以看到它是把 hook 存在 fiber 節(jié)點(diǎn)上的。
那 preact 有什么不同呢?
preact 如何實(shí)現(xiàn) hooks
preact 是兼容 react 代碼的更輕量級(jí)的框架,它支持 class 組件和 function 組件,也支持了 hooks 等 react 特性。不過(guò)它沒(méi)有實(shí)現(xiàn) fiber 架構(gòu)。
因?yàn)樗饕紤]的是體積的極致(只有 3kb),而不是性能的極致。
剛才我們了解了 react 是把 hook 鏈表存放在 fiber 節(jié)點(diǎn)上的,那 preact 沒(méi)有 fiber 節(jié)點(diǎn),會(huì)把 hook 鏈表存在哪呢?
其實(shí)也很容易想到,fiber 只是對(duì) vdom 做了下改造用于提升性能的,和 vdom 沒(méi)啥本質(zhì)的區(qū)別,那就把 hook 存在 vdom 上不就行了?
確實(shí),preact 就是把 hook 鏈表放在了 vdom 上。
比如這個(gè)有 4 個(gè) hooks 的函數(shù)組件:
它的實(shí)現(xiàn)就是在 vdom 上存取對(duì)應(yīng)的 hook:
它沒(méi)有像 react 那樣把 hook 分為 mount 和 update 兩個(gè)階段,而是合并到一起處理了。
如果,它把 hooks 存在了 component.__hooks 的數(shù)組上,通過(guò)下標(biāo)訪問(wèn)。
這個(gè) component 就是 vdom 上的一個(gè)屬性:
也就是把 hooks 的值存在了 vnode._component._hooks 的數(shù)組上。
對(duì)比下 react 和 preact 實(shí)現(xiàn) hooks 的差異:
- react 中是把 hook 鏈表存放在 fiberNode.memorizedState 屬性上,preact 中是把 hook 鏈表存放在 vnode._component._hooks 屬性上。
- react 中的 hook 鏈表通過(guò) next 串聯(lián),preact 中的 hook 鏈表就是個(gè)數(shù)組,通過(guò)下標(biāo)訪問(wèn)。
- react 把 hook 鏈表的創(chuàng)建和更新分離開(kāi),也就是 useXxx 會(huì)分為 mountXxx 和 updateXxx 來(lái)實(shí)現(xiàn),而 preact 中合并在一起處理的。
所以說(shuō),hooks 的實(shí)現(xiàn)并不依賴 fiber,它只不過(guò)是找個(gè)地方存放組件對(duì)應(yīng)的 hook 的數(shù)據(jù),渲染時(shí)能取到就行,存放在哪里是無(wú)所謂的。
因?yàn)?vdom、fiber 和組件渲染強(qiáng)相關(guān),所以存放在了這些結(jié)構(gòu)上。
像 react ssr 實(shí)現(xiàn) hooks,就既沒(méi)有存在 fiber 上,也沒(méi)有存在 vdom 上:
react ssr 如何實(shí)現(xiàn) hooks
其實(shí) react-dom 包除了可以做 csr 外,也可以做 ssr:
csr 時(shí)使用 react-dom 的 render 方法:
ssr 的時(shí)候使用 react-dom/server 的 renderToString 方法或 renderToStream 方法:
大家覺(jué)得 ssr 的時(shí)候會(huì)做 vdom 到 fiber 的轉(zhuǎn)換么?
肯定不會(huì)呀,fiber 是為了提高在瀏覽器中運(yùn)行時(shí)的渲染性能,把計(jì)算變成可打斷的,在空閑時(shí)做計(jì)算,才引入的一種結(jié)構(gòu)。
服務(wù)端渲染自然就不需要 fiber。
不需要 fiber 的話,它把 hook 鏈表存放在哪里呢?vdom 么?
確實(shí)可以放在 vdom,但是其實(shí)并沒(méi)有。
比如 useRef 這個(gè) hooks:
它是從 firstWorkInProgressHook 開(kāi)始的用 next 串聯(lián)的一個(gè)鏈表。
而 firstWorkInProgressHook 最開(kāi)始用 createHook 創(chuàng)建的第一個(gè) hook 節(jié)點(diǎn):
并沒(méi)有掛載到 vdom 上。
為什么呢?
因?yàn)?ssr 只需要渲染一次呀,又不需要更新,自然沒(méi)必要掛到 vdom 上。
只要每次處理完每個(gè)組件的 hooks 就清空一下這個(gè) hook 鏈表就行:
所以,react ssr 時(shí),hooks 是存在全局變量上的。
對(duì)比下 react csr 和 ssr 時(shí)的 hooks 實(shí)現(xiàn)原理的區(qū)別:
- csr 時(shí)會(huì)從 vdom 創(chuàng)建 fiber,用于把渲染變成可打斷的,通過(guò)空閑調(diào)度來(lái)提高性能,而 ssr 時(shí)不會(huì),是 vdom 直接渲染的。
- csr 時(shí)把 hooks 保存到了 fiber 節(jié)點(diǎn)上,ssr 時(shí)是直接放在了全局變量上,每個(gè)組件處理完就晴空。因?yàn)椴粫?huì)用第二次了。
- csr 時(shí)會(huì)把 hook 的創(chuàng)建和更新分為 mount 和 update 兩個(gè)階段,而 ssr 因?yàn)橹粫?huì)處理一次,只有創(chuàng)建階段。
hooks 的實(shí)現(xiàn)原理其實(shí)不復(fù)雜,就是在某個(gè)上下文中存放一個(gè)鏈表,然后 hooks api 從鏈表不同的元素上訪問(wèn)對(duì)應(yīng)的數(shù)據(jù)來(lái)完成各自的邏輯。這個(gè)上下文可以是 vdom、fiber 甚至是全局變量。
不過(guò) hooks 這個(gè)思想還是挺火的,淘寶出的服務(wù)端框架 midway 就在引入了 hooks 的思想:
midway 如何實(shí)現(xiàn) hooks
midway 是一個(gè) Node.js 框架:
服務(wù)端框架自然就沒(méi)有 vdom、fiber 這種結(jié)構(gòu),不過(guò) hooks 的思想并不依賴這些,實(shí)現(xiàn) hooks 的 api 只需要在某個(gè)上下文放一個(gè)鏈表就行。
midway 就實(shí)現(xiàn)了類(lèi)似 react hooks 的 api:
具體它這個(gè) hook 鏈表存在哪我還沒(méi)看,不過(guò)我們已經(jīng)掌握 hooks 的實(shí)現(xiàn)原理了,只要有個(gè)上下文存放 hook 鏈表就行,在哪都可以。
總結(jié)
react hooks 是在 react fiber 架構(gòu)之后出現(xiàn)的特性,很多人誤以為 hooks 必須配合 fiber 才能實(shí)現(xiàn),我們分別看了 react、preact、react ssr、midway 中的 hooks 的實(shí)現(xiàn),發(fā)現(xiàn)并不是這樣的:
- react 是把 vdom 轉(zhuǎn)成 fiber,然后把 hook 鏈表存放到了 fiber.memorizedState 屬性上,通過(guò) next 串聯(lián)
- preact 沒(méi)有實(shí)現(xiàn) fiber,它是把 hook 鏈表放到了 vnode._component._hooks 屬性上,數(shù)組實(shí)現(xiàn)的,通過(guò)下標(biāo)訪問(wèn)
- react ssr 時(shí)不需要 fiber,但是也沒(méi)有把 hook 鏈表掛到 vdom 上,而是直接放在了一個(gè)全局變量上,因?yàn)橹恍枰秩疽淮危秩就暌粋€(gè)組件就清空這個(gè)全局變量就行
- midway 是一個(gè) Node.js 框架,它也實(shí)現(xiàn)了 hooks 類(lèi)似的 api,具體放在哪我們沒(méi)深入,但是只要有個(gè)上下文存放 hook 鏈表就行
所以,react hooks 必須依賴 fiber 才能實(shí)現(xiàn)么?
明顯不是,搭配 fiber、搭配 vdom、搭配全局變量,甚至任何一個(gè)上下文都可以。在框架中引入 hooks 的 api 并不難。