React?Hydrate原理源碼解析
引言
React 渲染過(guò)程,即ReactDOM.render
執(zhí)行過(guò)程分為兩個(gè)大的階段:render
階段以及 commit
階段。React.hydrate
渲染過(guò)程和ReactDOM.render
差不多,兩者之間最大的區(qū)別就是,ReactDOM.hydrate
在 render
階段,會(huì)嘗試復(fù)用(hydrate)瀏覽器現(xiàn)有的 dom 節(jié)點(diǎn),并相互關(guān)聯(lián) dom 實(shí)例和 fiber,以及找出 dom 屬性和 fiber 屬性之間的差異。
Demo
這里,我們?cè)?index.html
中直接返回一段 html,以模擬服務(wù)端渲染生成的 html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Mini React</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> </head> <body> <div id="root"><div id="root"><div id="container"><h1 id="A">1<div id="A2">A2</div></h1><p id="B"><span id="B1">B1</span></p><span id="C">C</span></div></div></div> </body> </html>
注意,root
里面的內(nèi)容不能換行,不然客戶端hydrate
的時(shí)候會(huì)提示服務(wù)端和客戶端的模版不一致。
新建 index.jsx:
import React from "react"; import ReactDOM from "react-dom"; class Home extends React.Component { constructor(props) { super(props); this.state = { count: 1, }; } render() { const { count } = this.state; return ( <div id="container"> <div id="A"> {count} <div id="A2">A2</div> </div> <p id="B"> <span id="B1">B1</span> </p> </div> ); } } ReactDOM.hydrate(<Home />, document.getElementById("root"));
對(duì)比服務(wù)端和客戶端的內(nèi)容可知,服務(wù)端h1#A
和客戶端的div#A
不同,同時(shí)服務(wù)端比客戶端多了一個(gè)span#C
在客戶端開(kāi)始執(zhí)行之前,即 ReactDOM.hydrate
開(kāi)始執(zhí)行前,由于服務(wù)端已經(jīng)返回了 html 內(nèi)容,瀏覽器會(huì)立馬顯示內(nèi)容。對(duì)應(yīng)的真實(shí) DOM 樹(shù)如下:
注意,這不是 fiber 樹(shù)??!
ReactDOM.render
先來(lái)回顧一下 React 渲染更新過(guò)程,分為兩大階段,五小階段:
render 階段
- beginWork
- completeUnitOfWork
commit 階段。
- commitBeforeMutationEffects
- commitMutationEffects
- commitLayoutEffects
React 在 render 階段會(huì)根據(jù)新的 element tree 構(gòu)建 workInProgress 樹(shù),收集具有副作用的 fiber 節(jié)點(diǎn),構(gòu)建副作用鏈表。
特別是,當(dāng)我們調(diào)用ReactDOM.render
函數(shù)在客戶端進(jìn)行第一次渲染時(shí),render
階段的completeUnitOfWork
函數(shù)針對(duì)HostComponent
以及HostText
類型的 fiber 執(zhí)行以下 dom 相關(guān)的操作:
- 調(diào)用
document.createElement
為HostComponent
類型的 fiber 節(jié)點(diǎn)創(chuàng)建真實(shí)的 DOM 實(shí)例?;蛘哒{(diào)用document.createTextNode
為HostText
類型的 fiber 節(jié)點(diǎn)創(chuàng)建真實(shí)的 DOM 實(shí)例 - 將 fiber 節(jié)點(diǎn)關(guān)聯(lián)到真實(shí) dom 的
__reactFiber$rsdw3t27flk
(后面是隨機(jī)數(shù))屬性上。 - 將 fiber 節(jié)點(diǎn)的
pendingProps
屬性關(guān)聯(lián)到真實(shí) dom 的__reactProps$rsdw3t27flk
(后面是隨機(jī)數(shù))屬性上 - 將真實(shí)的 dom 實(shí)例關(guān)聯(lián)到
fiber.stateNode
屬性上:fiber.stateNode = dom
。 - 遍歷
pendingProps
,給真實(shí)的dom
設(shè)置屬性,比如設(shè)置 id、textContent 等
React 渲染更新完成后,React 會(huì)為每個(gè)真實(shí)的 dom 實(shí)例掛載兩個(gè)私有的屬性:__reactFiber$
和__reactProps$
,以div#container
為例:
ReactDOM.hydrate
hydrate
中文意思是水合物,這樣理解有點(diǎn)抽象。根據(jù)源碼,我更樂(lè)意將hydrate
的過(guò)程描述為:React 在 render 階段,構(gòu)造 workInProgress 樹(shù)時(shí),同時(shí)按相同的順序遍歷真實(shí)的 DOM 樹(shù),判斷當(dāng)前的 workInProgress fiber 節(jié)點(diǎn)和同一位置的 dom 實(shí)例是否滿足hydrate
的條件,如果滿足,則直接復(fù)用當(dāng)前位置的 DOM 實(shí)例,并相互關(guān)聯(lián) workInProgress fiber 節(jié)點(diǎn)和真實(shí)的 dom 實(shí)例,比如:
fiber.stateNode = dom; dom.__reactProps$ = fiber.pendingProps; dom.__reactFiber$ = fiber;
如果 fiber 和 dom 滿足hydrate
的條件,則還需要找出dom.attributes
和fiber.pendingProps
之間的屬性差異。
遍歷真實(shí) DOM 樹(shù)的順序和構(gòu)建 workInProgress 樹(shù)的順序是一致的。都是深度優(yōu)先遍歷,先遍歷當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),子節(jié)點(diǎn)都遍歷完了以后,再遍歷當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)。因?yàn)橹挥邪聪嗤捻樞?,fiber 樹(shù)同一位置的 fiber 節(jié)點(diǎn)和 dom 樹(shù)同一位置的 dom 節(jié)點(diǎn)才能保持一致
只有類型為HostComponent
或者HostText
類型的 fiber 節(jié)點(diǎn)才能hydrate
。這一點(diǎn)也很好理解,React 在 commit 階段,也就只有這兩個(gè)類型的 fiber 節(jié)點(diǎn)才需要執(zhí)行 dom 操作。
fiber 節(jié)點(diǎn)和 dom 實(shí)例是否滿足hydrate
的條件:
- 對(duì)于類型為
HostComponent
的 fiber 節(jié)點(diǎn),如果當(dāng)前位置對(duì)應(yīng)的 DOM 實(shí)例nodeType
為ELEMENT_NODE
,并且fiber.type === dom.nodeName
,那么當(dāng)前的 fiber 可以混合(hydrate) - 對(duì)于類型為
HostText
的 fiber 節(jié)點(diǎn),如果當(dāng)前位置對(duì)應(yīng)的 DOM 實(shí)例nodeType
為TEXT_NODE
,同時(shí)fiber.pendingProps
不為空,那么當(dāng)前的 fiber 可以混合(hydrate)
hydrate
的終極目標(biāo)就是,在構(gòu)造 workInProgress 樹(shù)的過(guò)程中,盡可能的復(fù)用當(dāng)前瀏覽器已經(jīng)存在的 DOM 實(shí)例以及 DOM 上的屬性,這樣就無(wú)需再為 fiber 節(jié)點(diǎn)創(chuàng)建 DOM 實(shí)例,同時(shí)對(duì)比現(xiàn)有的 DOM 的attribute
以及 fiber 的pendingProps
,找出差異的屬性。然后將 dom 實(shí)例和 fiber 節(jié)點(diǎn)相互關(guān)聯(lián)(通過(guò) dom 實(shí)例的__reactFiber$
以及__reactProps$
,fiber 的 stateNode 相互關(guān)聯(lián))
hydrate 過(guò)程
React 在 render 階段構(gòu)造HostComponent
或者HostText
類型的 fiber 節(jié)點(diǎn)時(shí),會(huì)首先調(diào)用 tryToClaimNextHydratableInstance(workInProgress)
方法嘗試給當(dāng)前 fiber 混合(hydrate)DOM 實(shí)例。如果當(dāng)前 fiber 不能被混合,那當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)在后續(xù)的 render 過(guò)程中都不再進(jìn)行hydrate
,而是直接創(chuàng)建 dom 實(shí)例。等到當(dāng)前節(jié)點(diǎn)所有子節(jié)點(diǎn)都調(diào)用completeUnitOfWork
完成工作后,又會(huì)從當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)開(kāi)始嘗試混合。
以下面的 demo 為例
// 服務(wù)端返回的DOM結(jié)構(gòu),這里為了直觀,我格式化了一下,按理服務(wù)端返回的內(nèi)容,是不允許換行或者有空字符串的 <body> <div id="root"> <div id="container"> <h1 id="A"> 1 <div id="A2">A2</div> </h1> <p id="B"> <span id="B1">B1</span> </p> <span id="C">C</span> </div> </div> </body> // 客戶端生成的內(nèi)容 <div id="container"> <div id="A"> 1 <div id="A2">A2</div> </div> <p id="B"> <span id="B1">B1</span> </p> </div>
render 階段,按以下順序:
div#container
滿足hydrate
的條件,因此關(guān)聯(lián) dom,fiber.stateNode = div#container
。然后使用hydrationParentFiber
記錄當(dāng)前混合的 fiber 節(jié)點(diǎn):hydrationParentFiber = fiber
。獲取下一個(gè) DOM 實(shí)例,這里是h1#A
,保存在變量nextHydratableInstance
中,nextHydratableInstance = h1#A
。
這里,hydrationParentFiber
和 nextHydratableInstance
都是全局變量。
div#A
和h1#A
不能混合,這時(shí)并不會(huì)立即結(jié)束混合的過(guò)程,React 繼續(xù)對(duì)比h1#A
的兄弟節(jié)點(diǎn),即p#B
,發(fā)現(xiàn)div#A
還是不能和p#B
混合,經(jīng)過(guò)最多兩次對(duì)比,React 認(rèn)為 dom 樹(shù)中已經(jīng)沒(méi)有 dom 實(shí)例滿足和div#A
這個(gè) fiber 混合的條件,于是div#A
節(jié)點(diǎn)及其所有子孫節(jié)點(diǎn)都不再進(jìn)行混合的過(guò)程,此時(shí)將isHydrating
設(shè)置為 false 表明div#A
這棵子樹(shù)都不再走混合的過(guò)程,直接走創(chuàng)建 dom 實(shí)例。同時(shí)控制臺(tái)提示:Expected server HTML to contain a matching..
之類的錯(cuò)誤。- beginWork 執(zhí)行到文本節(jié)點(diǎn)
1
時(shí),發(fā)現(xiàn)isHydrating = false
,因此直接跳過(guò)混合的過(guò)程,在completeUnitOfWork
階段直接調(diào)用document.createTextNode
直接為其創(chuàng)建文本節(jié)點(diǎn) - 同樣的,beginWork 執(zhí)行到節(jié)點(diǎn)
div#A2
時(shí),發(fā)現(xiàn)isHydrating = false
,因此直接跳過(guò)混合的過(guò)程,在completeUnitOfWork
階段直接調(diào)用document.createElement
直接為其創(chuàng)建真實(shí) dom 實(shí)例,并設(shè)置屬性 - 由于
div#A
的子節(jié)點(diǎn)都已經(jīng)completeUnitWork
了,輪到div#A
調(diào)用completeUnitWork
完成工作,將hydrationParentFiber
指向其父節(jié)點(diǎn),即div#container
這個(gè) dom 實(shí)例。設(shè)置isHydrating = true
表明可以為當(dāng)前節(jié)點(diǎn)的兄弟節(jié)點(diǎn)繼續(xù)混合的過(guò)程了。div#A
沒(méi)有混合的 dom 實(shí)例,因此調(diào)用document.createElement
為其創(chuàng)建真實(shí)的 dom 實(shí)例。 - 為
p#B
執(zhí)行 beginWork。由于nextHydratableInstance
保存的還是h1#A
dom 實(shí)例,因此p#B
和h1#A
對(duì)比發(fā)現(xiàn)不能復(fù)用,React 嘗試和h1#A
的兄弟節(jié)點(diǎn)p#B
對(duì)比,發(fā)現(xiàn) fiberp#B
和 domp#B
能混,因此將h1#A
標(biāo)記為刪除,同時(shí)關(guān)聯(lián) dom 實(shí)例:fiber.stateNode = p#B
,保存hydrationParentFiber = fiber
,nextHydratableInstance
指向p#B
的第一個(gè)子節(jié)點(diǎn),即span#B1
...省略了后續(xù)的過(guò)程。
從上面的執(zhí)行過(guò)程可以看出,hydrate 的過(guò)程如下:
- 調(diào)用
tryToClaimNextHydratableInstance
開(kāi)始混合 - 判斷當(dāng)前 fiber 節(jié)點(diǎn)和同一位置的 dom 實(shí)例是否滿足混合的條件。
- 如果當(dāng)前位置的 dom 實(shí)例不滿足混合條件,則繼續(xù)比較當(dāng)前 dom 的兄弟元素,如果兄弟元素和當(dāng)前的 fiber 也不能混合,則當(dāng)前 fiber 及其所有子孫節(jié)點(diǎn)都不能混合,后續(xù) render 過(guò)程將會(huì)跳過(guò)混合。直到當(dāng)前 fiber 節(jié)點(diǎn)的兄弟節(jié)點(diǎn) render,才會(huì)繼續(xù)混合的過(guò)程。
事件綁定
React在初次渲染時(shí),不論是ReactDOM.render
還是ReactDOM.hydrate
,會(huì)調(diào)用createRootImpl
函數(shù)創(chuàng)建fiber的容器,在這個(gè)函數(shù)中調(diào)用listenToAllSupportedEvents
注冊(cè)所有原生的事件。
function createRootImpl(container, tag, options) { // ... var root = createContainer(container, tag, hydrate); // ... listenToAllSupportedEvents(container); // ... return root; }
這里container
就是div#root
節(jié)點(diǎn)。listenToAllSupportedEvents
會(huì)給div#root
節(jié)點(diǎn)注冊(cè)瀏覽器支持的所有原生事件,比如onclick
等。React合成事件一文介紹過(guò),React采用的是事件委托的機(jī)制,將所有事件代理到div#root
節(jié)點(diǎn)上。以下面的為例:
<div id="A" onClick={this.handleClick}> button <div>
我們知道React在渲染時(shí),會(huì)將fiber的props關(guān)聯(lián)到真實(shí)的dom的__reactProps$
屬性上,此時(shí)
div#A.__reactProps$ = { onClick: this.handleClick }
當(dāng)我們點(diǎn)擊按鈕時(shí),會(huì)觸發(fā)div#root
上的事件監(jiān)聽(tīng)器:
function onclick(e){ const target = e.target const fiberProps = target.__reactProps$ const clickhandle = fiberProps.onClick if(clickhandle){ clickhandle(e) } }
這樣我們就可以實(shí)現(xiàn)事件的委托。這其中最重要的就是將fiber的props掛載到真實(shí)的dom實(shí)例的__reactProps$屬性上。因此,只要我們?cè)?code>hydrate階段能夠成功關(guān)聯(lián)dom和fiber,就自然也實(shí)現(xiàn)了事件的“綁定”
hydrate 源碼剖析
hydrate 的過(guò)程發(fā)生在 render 階段,commit 階段幾乎沒(méi)有和 hydrate 相關(guān)的邏輯。render 階段又分為兩個(gè)小階段:beginWork
和 completeUnitOfWork
。只有HostRoot
、HostComponent
、HostText
三種類型的 fiber 節(jié)點(diǎn)才需要 hydrate,因此源碼只針對(duì)這三種類型的 fiber 節(jié)點(diǎn)剖析
beginWork
beginWork 階段判斷 fiber 和 dom 實(shí)例是否滿足混合的條件,如果滿足,則為 fiber 關(guān)聯(lián) dom 實(shí)例:fiber.stateNode = dom
function beginWork(current, workInProgress, renderLanes) { switch (workInProgress.tag) { case HostRoot: return updateHostRoot(current, workInProgress, renderLanes); case HostComponent: return updateHostComponent(current, workInProgress, renderLanes); case HostText: return updateHostText(current, workInProgress); } }
HostRoot Fiber
HostRoot
fiber 是容器root
的 fiber 節(jié)點(diǎn)。
這里主要是判斷當(dāng)前 render 是ReactDOM.render
還是ReactDOM.hydrate
,我們調(diào)用ReactDOM.hydrate
渲染時(shí),root.hydrate
為 true。
如果是調(diào)用的ReactDOM.hydrate
,則調(diào)用enterHydrationState
函數(shù)進(jìn)入hydrate
的過(guò)程。這個(gè)函數(shù)主要是初始化幾個(gè)全局變量:
- isHydrating。表示當(dāng)前正處于 hydrate 的過(guò)程。如果當(dāng)前節(jié)點(diǎn)及其所有子孫節(jié)點(diǎn)都不滿足 hydrate 的條件時(shí),這個(gè)變量為 false
- hydrationParentFiber。當(dāng)前混合的 fiber。正常情況下,該變量和
HostComponent
或者HostText
類型的 workInProgress 一致。 - nextHydratableInstance。下一個(gè)可以混合的 dom 實(shí)例。當(dāng)前 dom 實(shí)例的第一個(gè)子元素或者兄弟元素。
注意getNextHydratable
會(huì)判斷 dom 實(shí)例是否是ELEMENT_NODE
類型(對(duì)應(yīng)的 fiber 類型是HostComponent
)或者TEXT_NODE
類型(對(duì)應(yīng)的 fiber 類型是HostText
)。只有ELEMENT_NODE
或者HostText
類型的 dom 實(shí)例才是可以 hydrate 的
function updateHostRoot(current, workInProgress, renderLanes) { if (root.hydrate && enterHydrationState(workInProgress)) { var child = mountChildFibers(workInProgress, null, nextChildren); } return workInProgress.child; } function getNextHydratable(node) { // 跳過(guò) non-hydratable 節(jié)點(diǎn). for (; node != null; node = node.nextSibling) { var nodeType = node.nodeType; if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) { break; } } return node; } function enterHydrationState() { var parentInstance = fiber.stateNode.containerInfo; nextHydratableInstance = getNextHydratable(parentInstance.firstChild); hydrationParentFiber = fiber; isHydrating = true; }
HostComponent
function updateHostComponent(current, workInProgress, renderLanes) { if (current === null) { tryToClaimNextHydratableInstance(workInProgress); } reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; }
HostText Fiber
function updateHostText(current, workInProgress) { if (current === null) { tryToClaimNextHydratableInstance(workInProgress); } return null; }
tryToClaimNextHydratableInstance
假設(shè)當(dāng)前 fiberA 對(duì)應(yīng)位置的 dom 為 domA,tryToClaimNextHydratableInstance
會(huì)首先調(diào)用tryHydrate
判斷 fiberA 和 domA 是否滿足混合的條件:
如果 fiberA 和 domA 滿足混合的條件,則將hydrationParentFiber = fiberA;
。并且獲取 domA 的第一個(gè)子元素賦值給nextHydratableInstance
如果 fiberA 和 domA 不滿足混合的條件,則獲取 domA 的兄弟節(jié)點(diǎn),即 domB,調(diào)用tryHydrate
判斷 fiberA 和 domB 是否滿足混合條件:
- 如果 domB 滿足和 fiberA 混合的條件,則將 domA 標(biāo)記為刪除,并獲取 domB 的第一個(gè)子元素賦值給
nextHydratableInstance
- 如果 domB 不滿足和 fiberA 混合的條件,則調(diào)用
insertNonHydratedInstance
提示錯(cuò)誤:"Warning: Expected server HTML to contain a matching",同時(shí)將isHydrating
標(biāo)記為 false 退出。
這里可以看出,tryToClaimNextHydratableInstance
最多比較兩個(gè) dom 節(jié)點(diǎn),如果兩個(gè) dom 節(jié)點(diǎn)都無(wú)法滿足和 fiberA 混合的條件,則說(shuō)明當(dāng)前 fiberA 及其所有的子孫節(jié)點(diǎn)都無(wú)需再進(jìn)行混合的過(guò)程,因此將isHydrating
標(biāo)記為 false。等到當(dāng)前 fiberA 節(jié)點(diǎn)及其子節(jié)點(diǎn)都完成了工作,即都執(zhí)行了completeWork
,isHydrating
才會(huì)被設(shè)置為 true,以便繼續(xù)比較 fiberA 的兄弟節(jié)點(diǎn)
這里還需要注意一點(diǎn),如果兩個(gè) dom 都無(wú)法滿足和 fiberA 混合,那么nextHydratableInstance
依然保存的是 domA,domA 會(huì)繼續(xù)和 fiberA 的兄弟節(jié)點(diǎn)比對(duì)。
function tryToClaimNextHydratableInstance(fiber) { if (!isHydrating) { return; } var nextInstance = nextHydratableInstance; var firstAttemptedInstance = nextInstance; if (!tryHydrate(fiber, nextInstance)) { // 如果第一次調(diào)用tryHydrate發(fā)現(xiàn)當(dāng)前fiber和dom不滿足hydrate的條件,則獲取dom的兄弟節(jié)點(diǎn) // 然后調(diào)用 tryHydrate 繼續(xù)對(duì)比f(wàn)iber和兄弟節(jié)點(diǎn)是否滿足混合 nextInstance = getNextHydratableSibling(firstAttemptedInstance); if (!nextInstance || !tryHydrate(fiber, nextInstance)) { // 對(duì)比了兩個(gè)dom發(fā)現(xiàn)都無(wú)法和fiber混合,因此調(diào)用insertNonHydratedInstance控制臺(tái)提示錯(cuò)誤 insertNonHydratedInstance(hydrationParentFiber, fiber); isHydrating = false; hydrationParentFiber = fiber; return; } // 如果第一次tryHydrate不滿足,第二次tryHydrate滿足,則說(shuō)明兄弟節(jié)點(diǎn)和當(dāng)前fiber是可以混合的,此時(shí)需要?jiǎng)h除當(dāng)前位置的dom deleteHydratableInstance(hydrationParentFiber, firstAttemptedInstance); } hydrationParentFiber = fiber; nextHydratableInstance = getFirstHydratableChild(nextInstance); } // 將dom實(shí)例保存在 fiber.stateNode上 function tryHydrate(fiber, nextInstance) { switch (fiber.tag) { case HostComponent: { if ( nextInstance.nodeType === ELEMENT_NODE && fiber.type.toLowerCase() === nextInstance.nodeName.toLowerCase() ) { fiber.stateNode = nextInstance; return true; } return false; } case HostText: { var text = fiber.pendingProps; if (text !== "" && nextInstance.nodeType === TEXT_NODE) { fiber.stateNode = nextInstance; return true; } return false; } default: return false; } }
completeUnitOfWork
completeUnitOfWork 階段主要是給 dom 關(guān)聯(lián) fiber 以及 props:dom.__reactProps$ = fiber.pendingProps;dom.__reactFiber$ = fiber;
同時(shí)對(duì)比fiber.pendingProps
和dom.attributes
的差異
function completeUnitOfWork(unitOfWork) { var completedWork = unitOfWork; do { var current = completedWork.alternate; var returnFiber = completedWork.return; next = completeWork(current, completedWork, subtreeRenderLanes); var siblingFiber = completedWork.sibling; if (siblingFiber !== null) { workInProgress = siblingFiber; return; } completedWork = returnFiber; workInProgress = completedWork; } while (completedWork !== null); } function completeWork(current, workInProgress, renderLanes) { switch (workInProgress.tag) { case HostRoot: { if (current === null) { var wasHydrated = popHydrationState(workInProgress); if (wasHydrated) { markUpdate(workInProgress); } } return null; } case HostComponent: // 第一次渲染 if (current === null) { var _wasHydrated = popHydrationState(workInProgress); if (_wasHydrated) { // 如果存在差異的屬性,則將fiber副作用標(biāo)記為更新 if (prepareToHydrateHostInstance(workInProgress)) { markUpdate(workInProgress); } } else { } } case HostText: { var newText = newProps; if (current === null) { var _wasHydrated2 = popHydrationState(workInProgress); if (_wasHydrated2) { if (prepareToHydrateHostTextInstance(workInProgress)) { markUpdate(workInProgress); } } } return null; } } }
popHydrationState
function popHydrationState(fiber) { if (fiber !== hydrationParentFiber) { return false; } if (!isHydrating) { popToNextHostParent(fiber); isHydrating = true; return false; } var type = fiber.type; if ( fiber.tag !== HostComponent || !shouldSetTextContent(type, fiber.memoizedProps) ) { var nextInstance = nextHydratableInstance; while (nextInstance) { deleteHydratableInstance(fiber, nextInstance); nextInstance = getNextHydratableSibling(nextInstance); } } popToNextHostParent(fiber); nextHydratableInstance = hydrationParentFiber ? getNextHydratableSibling(fiber.stateNode) : null; return true; }
以下圖為例:
在 beginWork 階段對(duì) p#B
fiber 工作時(shí),發(fā)現(xiàn) dom 樹(shù)中同一位置的h1#B
不滿足混合的條件,于是繼續(xù)對(duì)比h1#B
的兄弟節(jié)點(diǎn),即div#C
,仍然無(wú)法混合,經(jīng)過(guò)最多兩輪對(duì)比后發(fā)現(xiàn)p#B
這個(gè) fiber 沒(méi)有可以混合的 dom 節(jié)點(diǎn),于是將 isHydrating
標(biāo)記為 false,hydrationParentFiber = fiberP#B
。p#B
的子孫節(jié)點(diǎn)都不再進(jìn)行混合的過(guò)程。
div#B1
fiber 沒(méi)有子節(jié)點(diǎn),因此它可以調(diào)用completeUnitOfWork
完成工作,completeUnitOfWork
階段調(diào)用 popHydrationState
方法,在popHydrationState
方法內(nèi)部,首先判斷 fiber !== hydrationParentFiber
,由于此時(shí)的hydrationParentFiber
等于p#B
,因此條件成立,不用往下執(zhí)行。
由于p#B
fiber 的子節(jié)點(diǎn)都已經(jīng)完成了工作,因此它也可以調(diào)用completeUnitOfWork
完成工作。同樣的,在popHydrationState
函數(shù)內(nèi)部,第一個(gè)判斷fiber !== hydrationParentFiber
不成立,兩者是相等的。第二個(gè)條件!isHydrating
成立,進(jìn)入條件語(yǔ)句,首先調(diào)用popToNextHostParent
將hydrationParentFiber
設(shè)置為p#B
的第一個(gè)類型為HostComponent
的祖先元素,這里是div#A
fiber,然后將isHydrating
設(shè)置為 true,指示可以為p#B
的兄弟節(jié)點(diǎn)進(jìn)行混合。
如果服務(wù)端返回的 DOM 有多余的情況,則調(diào)用deleteHydratableInstance
將其刪除,比如下圖中div#D
節(jié)點(diǎn)將會(huì)在div#A
fiber 的completeUnitOfWork
階段刪除
prepareToHydrateHostInstance
對(duì)于HostComponent
類型的fiber會(huì)調(diào)用這個(gè)方法,這里只要是關(guān)聯(lián) dom 和 fiber:
- 設(shè)置
domInstance.__reactFiber$w63z5ormsqk = fiber
- 設(shè)置
domInstance.__reactProps$w63z5ormsqk = props
- 對(duì)比服務(wù)端和客戶端的屬性
function prepareToHydrateHostInstance(fiber) { var domInstance = fiber.stateNode; var updatePayload = hydrateInstance( domInstance, fiber.type, fiber.memoizedProps, fiber ); fiber.updateQueue = updatePayload; if (updatePayload !== null) { return true; } return false; } function hydrateInstance(domInstance, type, props, fiber) { precacheFiberNode(fiber, domInstance); // domInstance.__reactFiber$w63z5ormsqk = fiber updateFiberProps(domInstance, props); // domInstance.__reactProps$w63z5ormsqk = props // 比較dom.attributes和props的差異,如果dom.attributes的屬性比props多,說(shuō)明服務(wù)端添加了額外的屬性,此時(shí)控制臺(tái)提示。 // 注意,在對(duì)比過(guò)程中,只有服務(wù)端和客戶端的children屬性(即文本內(nèi)容)不同時(shí),控制臺(tái)才會(huì)提示錯(cuò)誤,同時(shí)在commit階段,客戶端會(huì)糾正這個(gè)錯(cuò)誤,以客戶端的文本為主。 // 但是,如果是id不同,則客戶端并不會(huì)糾正。 return diffHydratedProperties(domInstance, type, props); }
這里重點(diǎn)講下diffHydratedProperties
,以下面的demo為例:
// 服務(wù)端對(duì)應(yīng)的dom <div id="root"><div extra="server attr" id="server">客戶端的文本</div></div> // 客戶端 render() { const { count } = this.state; return <div id="client">客戶端的文本</div>; }
在diffHydratedProperties
的過(guò)程中發(fā)現(xiàn),服務(wù)端返回的id和客戶端的id不同,控制臺(tái)提示id不匹配,但是客戶端并不會(huì)糾正這個(gè),可以看到瀏覽器的id依然是server
。
同時(shí),服務(wù)端多返回了一個(gè)extra
屬性,因此需要控制臺(tái)提示,但由于已經(jīng)提示了id不同的錯(cuò)誤,這個(gè)錯(cuò)誤就不會(huì)提示。
最后,客戶端的文本和服務(wù)端的children不同,即文本內(nèi)容不同,也需要提示錯(cuò)誤,同時(shí),客戶端會(huì)糾正這個(gè)文本,以客戶端的為主。
prepareToHydrateHostTextInstance
對(duì)于HostText
類型的fiber會(huì)調(diào)用這個(gè)方法,這個(gè)方法邏輯比較簡(jiǎn)單,就不詳細(xì)介紹了 務(wù)端對(duì)應(yīng)的dom
<div id="root"><div extra="server attr" id="server">客戶端的文本</div></div> // 客戶端 render() { const { count } = this.state; return <div id="client">客戶端的文本</div>; }
在diffHydratedProperties
的過(guò)程中發(fā)現(xiàn),服務(wù)端返回的id和客戶端的id不同,控制臺(tái)提示id不匹配,但是客戶端并不會(huì)糾正這個(gè),可以看到瀏覽器的id依然是server
。
同時(shí),服務(wù)端多返回了一個(gè)extra
屬性,因此需要控制臺(tái)提示,但由于已經(jīng)提示了id不同的錯(cuò)誤,這個(gè)錯(cuò)誤就不會(huì)提示。
最后,客戶端的文本和服務(wù)端的children不同,即文本內(nèi)容不同,也需要提示錯(cuò)誤,同時(shí),客戶端會(huì)糾正這個(gè)文本,以客戶端的為主。
prepareToHydrateHostTextInstance
對(duì)于HostText
類型的fiber會(huì)調(diào)用這個(gè)方法,這個(gè)方法邏輯比較簡(jiǎn)單,就不詳細(xì)介紹了
以上就是React Hydrate原理源碼解析的詳細(xì)內(nèi)容,更多關(guān)于React Hydrate原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react native 文字輪播的實(shí)現(xiàn)示例
這篇文章主要介紹了react native 文字輪播的實(shí)現(xiàn)示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07淺談React-router v6 實(shí)現(xiàn)登錄驗(yàn)證流程
本文主要介紹了React-router v6 實(shí)現(xiàn)登錄驗(yàn)證流程,主要介紹了公共頁(yè)面、受保護(hù)頁(yè)面和登錄頁(yè)面,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05React進(jìn)階學(xué)習(xí)之組件的解耦之道
這篇文章主要給大家介紹了關(guān)于React進(jìn)階之組件的解耦之道,文中通過(guò)詳細(xì)的示例代碼給大家介紹了組件分割與解耦的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08react拖拽組件react-sortable-hoc的使用
本文主要介紹了react拖拽組件react-sortable-hoc的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02React 數(shù)據(jù)獲取與性能優(yōu)化詳解
這篇文章主要為大家介紹了React 數(shù)據(jù)獲取與性能優(yōu)化方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10React?Native集成支付寶支付的實(shí)現(xiàn)方法
這篇文章主要介紹了React?Native集成支付寶支付的實(shí)現(xiàn)現(xiàn),ativeModules是JS代碼調(diào)用原生模塊的橋梁。所以,我們只需要在原生工程中集成支付寶和微信支付的sdk,然后使用NativeModules調(diào)用即可,需要的朋友可以參考下2022-02-02ahooks正式發(fā)布React?Hooks工具庫(kù)
這篇文章主要為大家介紹了ahooks正式發(fā)布值得擁有的React?Hooks工具庫(kù)使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07