為何采用虛擬DOM
尤雨溪曾在知乎正面的回答這個(gè)問(wèn)題:
- 為函數(shù)式的 UI 編程方式打開(kāi)了大門(mén);
- 可以渲染到 DOM 以外的 backend,比如 ReactNative封寞。
針對(duì)這兩點(diǎn)談?wù)勛约旱睦斫猓?/p>
-
為函數(shù)式的 UI 編程方式打開(kāi)了大門(mén);
- 虛擬DOM是由react引入的概念颖榜,react的核心理念就是函數(shù)式編程,類(lèi)似ui = f (a)煤裙,其中a是數(shù)據(jù)掩完,函數(shù)f則是react的render,每個(gè)a的改動(dòng)硼砰,都會(huì)重新生成新的ui且蓬。
- 每次生成新的ui就需要重新刷新頁(yè)面代價(jià)太過(guò)昂貴,虛擬DOM以及diff算法的引入可以最大限度的復(fù)用舊的DOM题翰,使得渲染性能大幅提升恶阴。
-
可以渲染到 DOM 以外的 backend诈胜,比如 ReactNative。
- 有了虛擬DOM就可以輕松實(shí)現(xiàn)跨平臺(tái)冯事,多平臺(tái)的core都相同焦匈,只是在render到具體平臺(tái)的時(shí)候采取不同的render就好了
真實(shí)DOM的操作
diff算法最后的結(jié)果還是要落到對(duì)真實(shí)DOM的操作上去,這種操作有三種:新增昵仅、刪除缓熟、移位。這三種操作都需要獲取自身DOM和parentDOM摔笤,以vue的源碼來(lái)解釋?zhuān)?/p>
- 新增够滑、移位操作可以通過(guò)如下實(shí)現(xiàn)
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
- 移位操作則通過(guò)insert實(shí)現(xiàn)
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
diff算法
diff算法我認(rèn)為包括三部分:虛擬DOM樹(shù)的遍歷、parent節(jié)點(diǎn)下的children的比較吕世、diff完成之后對(duì)真實(shí)DOM的操作時(shí)機(jī)
虛擬DOM的遍歷:
虛擬DOM說(shuō)到底只是一顆樹(shù)形結(jié)構(gòu)彰触,對(duì)于樹(shù)的遍歷我們知道有深度遍歷和廣度遍歷
深度遍歷需要棧結(jié)構(gòu),可以通過(guò)遞歸(內(nèi)核維護(hù)調(diào)用棧)的方式實(shí)現(xiàn)命辖,也可以采用人為構(gòu)造棧况毅,然后循環(huán)棧完成深度遍歷。通常深度優(yōu)先搜索法不全部保留結(jié)點(diǎn)吮龄,擴(kuò)展完的結(jié)點(diǎn)從棧中彈出刪去俭茧,這樣咆疗,在棧中存儲(chǔ)的結(jié)點(diǎn)數(shù)就是深度值漓帚,因此它占用空間較少。
廣度遍歷則采用隊(duì)列的方式實(shí)現(xiàn)午磁,由于廣度優(yōu)先是按照樹(shù)的層級(jí)來(lái)遍歷的尝抖,在遍歷某層的時(shí)候需要將下一層的數(shù)據(jù)推進(jìn)隊(duì)列里面,所以隊(duì)列的長(zhǎng)度通常會(huì)比樹(shù)的寬度還要寬迅皇。
目前昧辽,不管是vue還是react,采用的都是深度遍歷算法
vue2.0
vue的渲染主要分三部分:
- 虛擬樹(shù)的遍歷
- 子節(jié)點(diǎn)的diff
- 真實(shí)DOM的更新
虛擬樹(shù)的遍歷:
采用遞歸的先序深度遍歷算法
子節(jié)點(diǎn)的diff:
對(duì)于相同的節(jié)點(diǎn)登颓,繼續(xù)比較子節(jié)點(diǎn):
- 同一級(jí)子元素新老虛擬DOM列表分別設(shè)置
startIndex
和endIndex
搅荞,并交叉判斷startIndex
和endIndex
是否是相同元素- 對(duì)比的結(jié)果有三種情況:新增、刪除框咙、移位
- 新增:老的
startIndex
不動(dòng)咕痛,新的startIndex
移位,并在老的startIndex
元素前插入- 移位:
- 新
startIndex
和老startIndex
或者新endIndex
和老endIndex
相同喇嘱,只要移動(dòng)startIndex
或者endIndex
就可以了茉贡;- 新
startIndex
和老endIndex
相同,新startIndex++
者铜,老endIndex--
腔丧,將老endIndex
的ele
插入到老startIndex
的ele
前面- 新
endIndex
和老startIndex
相同放椰,新endIndex--
,老startIndex++
愉粤,將老的startIndex
的ele
插入到老endIndex
的ele
的后面- 新
startIndex
的key
匹配到老的vnode
的key
砾医,將老vnode
的ele
插入到老startIndex
的ele
前面,還有一個(gè)操作:將老vnode
標(biāo)記位undefined
衣厘,(oldCh[idxInOld] = undefined
)藻烤,- 刪除
- 等新
startIndex
和新endIndex
合攏,老startIndex
和老endIndex
之間的非undefined
的vnode
的ele
全部刪除头滔,undefined
的node
代表已經(jīng)處理過(guò)了(移位)
真實(shí)DOM的更新
- 由于采用遞歸的方式處理
vnode
怖亭,所以節(jié)點(diǎn)更新真實(shí)DOM
的時(shí)機(jī)是該節(jié)點(diǎn)下所有子節(jié)點(diǎn)更新完畢后才會(huì)更新,即從下而上 - 節(jié)點(diǎn)的
props
的處理是早于子節(jié)點(diǎn)的diff
坤检,所以props
的更新是從上而下
react16
react的渲染雖然采用深度遍歷兴猩,但是是非遞歸方式,而是采用鏈表的方式早歇,這樣做的原因是方便fiber的引入
react的渲染可以分為兩個(gè)部分:
- reconciliation 階段
- commit 階段
reconciliation 階段:
所謂的reconciliation階段就是虛擬DOM的diff階段倾芝,由于采用了遞歸的鏈表結(jié)構(gòu),所以每個(gè)節(jié)點(diǎn)必然經(jīng)歷兩次的遍歷箭跳,這兩次的遍歷分別為:beginWork
和completeUnitOfWork
晨另。
-
beginWork
:完成對(duì)子節(jié)點(diǎn)的diff過(guò)程(新增,刪除谱姓,移位)借尿,并給相應(yīng)的vnode打上effectTag,返回第一個(gè)子節(jié)點(diǎn)屉来。
通過(guò)唯一 key 可以判斷新老集合中是否存在相同的節(jié)點(diǎn)路翻,if (prevChild === nextChild),如果存在相同節(jié)點(diǎn)茄靠,則進(jìn)行移動(dòng)操作茂契,但在移動(dòng)前需要將當(dāng)前節(jié)點(diǎn)在老集合中的位置與 lastIndex 進(jìn)行比較,if (child._mountIndex < lastIndex)慨绳,則進(jìn)行節(jié)點(diǎn)移動(dòng)操作掉冶,否則不執(zhí)行該操作。這是一種順序優(yōu)化手段脐雪,lastIndex 一直在更新厌小,表示訪問(wèn)過(guò)的節(jié)點(diǎn)在老集合中最右的位置(即最大的位置),如果新集合中當(dāng)前訪問(wèn)的節(jié)點(diǎn)比 lastIndex 大喂江,說(shuō)明當(dāng)前訪問(wèn)節(jié)點(diǎn)在老集合中就比上一個(gè)節(jié)點(diǎn)位置靠后召锈,則該節(jié)點(diǎn)不會(huì)影響其他節(jié)點(diǎn)的位置,因此不用添加到差異隊(duì)列中获询,即不執(zhí)行移動(dòng)操作涨岁,只有當(dāng)訪問(wèn)的節(jié)點(diǎn)比 lastIndex 小時(shí)拐袜,才需要進(jìn)行移動(dòng)操作。
-
completeUnitOfWork
:完成對(duì)當(dāng)前節(jié)點(diǎn)的副作用的收集(主要的props
的改動(dòng))梢薪,并將所有需要改動(dòng)的節(jié)點(diǎn)串成一個(gè)鏈表effect-list
蹬铺,掛到hostRoot
節(jié)點(diǎn)上,返回兄弟節(jié)點(diǎn)或者是父節(jié)點(diǎn)秉撇。
reconciliation階段的最終結(jié)果是產(chǎn)生一個(gè)effect-list
列表甜攀,這個(gè)effect-list
列表里面的節(jié)點(diǎn)的兩個(gè)屬性標(biāo)明接下來(lái)的commit
階段需要對(duì)該節(jié)點(diǎn)進(jìn)行的處理:effectTag
以及updateQueue
,effectTag
表明對(duì)節(jié)點(diǎn)位置的改動(dòng)琐馆,updateQueue
表明對(duì)節(jié)點(diǎn)狀態(tài)的改動(dòng)
commit 階段
commit階段主要是循環(huán)effect-list
來(lái)對(duì)節(jié)點(diǎn)分別處理规阀,并且對(duì)effect-list
進(jìn)行了三次循環(huán)
第一遍:執(zhí)行所有的(
effectTag
&Snapshot
)為true
的節(jié)點(diǎn)的commitBeforeMutationLifeCycles
,即執(zhí)行周期函數(shù)getSnapshotBeforeUpdate
第二遍:為所有(
effectTag
& (Placement
|Update
|Deletion
))即移位瘦麸、更新谁撼、刪除的節(jié)點(diǎn)進(jìn)行commitPlacement
以及commitWork
,commitPlacement
做的就是移位和刪除的動(dòng)作滋饲,commitWork
則將節(jié)點(diǎn)的updateQueue
拿出來(lái)更新第三遍:為所有的(
effectTag
& (Update
|Callback
))的節(jié)點(diǎn)進(jìn)行commitLifeCycles
厉碟,即執(zhí)行周期函數(shù)componentDidUpdate
fiber節(jié)點(diǎn)參考:
const fiber = {
// Instance
tag = tag; // fiber 的類(lèi)型。
- IndeterminateComponent
- FunctionalComponent
- ClassComponent // Menu, Table
- HostRoot // ReactDOM.render 的第二個(gè)參數(shù)
- HostPortal
- HostComponent // div, span
- HostText // 純文本節(jié)點(diǎn)屠缭,即 dom 的 nodeName 等于 '#text'
- CallComponent // 對(duì)應(yīng) call return 中的 call
- CallHandlerPhase // call 中的 handler 階段
- ReturnComponent // 對(duì)應(yīng) call return 中的 return
- Fragment
- Mode // AsyncMode || StrictMode
- ContextConsumer
- ContextProvider
- ForwardRef
key = key; // fiber 的唯一標(biāo)識(shí)
type = null; // 與 react element 里的 type 一致
stateNode = null; // 對(duì)應(yīng)組件或者 dom 的實(shí)例
// Fiber
return = null; // 等價(jià)于棧幀中的函數(shù)調(diào)用后的返回地址箍鼓,這里即是父 fiber
child = null; // 即組件的 render 的返回值,是一個(gè)單鏈表(因?yàn)榉祷刂挡灰欢ㄊ且粋€(gè)單一的元素)
sibling = null; // 單鏈表
index = 0;
ref = null;
// props 等價(jià)于一個(gè)函數(shù)的 arguments
pendingProps = pendingProps; // 新的 props(要么是當(dāng)前的 props呵曹,要么是 wip 的 props)款咖,默認(rèn)就等于 element.props,對(duì)于 Fragment 和 Portal逢并,則等于 props.children
memoizedProps = null; // 舊的 props之剧,等于 wipFiber.pendingProps 或者 wipFiber.pendingProps.children
- 一般 oldProps = workInProgress.memoizedProps
- 一般 newProps = workInProgress.pendingProps
updateQueue = null; // 狀態(tài)更新和回調(diào)的函數(shù)隊(duì)列
memoizedState = null; // 組件實(shí)例的 state
mode = mode; // 用于描述處理 fiber 和它的子樹(shù)的方式郭卫,創(chuàng)建后就不應(yīng)被改變砍聊,如未指定則從父 fiber 繼承。NoContext || AsyncMode || StrictMode
// Effects
effectTag = NoEffect; // 當(dāng)需要變化的時(shí)候贰军,具體需要進(jìn)行的操作的類(lèi)型
- NoEffect // 初始值
- PerformedWork // 開(kāi)始處理后置為 PerformedWork
- Placement // 插入玻蝌,保持原位,移動(dòng) dom 節(jié)點(diǎn)
- Update // 對(duì) dom 結(jié)構(gòu)的改變词疼。mount 或者 update 后置為 Update
- PlacementAndUpdate
- Deletion
- ContentReset // 將一個(gè)只包含字符串的 dom 節(jié)點(diǎn)俯树,或者 textarea,或者 dangerouslySetInnerHTML 替換成其它類(lèi)型的節(jié)點(diǎn)
- Callback // setState 的回調(diào)
- DidCapture // 渲染出錯(cuò)贰盗,準(zhǔn)備捕獲出錯(cuò)信息
- Ref // 準(zhǔn)備執(zhí)行 ref 回調(diào)
- ErrLog // 渲染出錯(cuò)许饿,執(zhí)行 componentDidCatch
- Snapshot // getSnapshotBeforeUpdate
- HostEffectMask // 暫時(shí)沒(méi)用
- Incomplete // 任何造成 fiber 的工作無(wú)法完成的情況
- ShouldCapture // 需要處理錯(cuò)誤
nextEffect = null; // 下一個(gè)需要處理的有副作用的 fiber
// 本 fiber 的子樹(shù)中有副作用的第一個(gè)和最后一個(gè) fiber
firstEffect = null; //
lastEffect = null; //
expirationTime = NoWork; // 將來(lái)的某個(gè)時(shí)間點(diǎn),在那之前必須完成所有工作
alternate = null; // WIP 樹(shù)里面的 fiber舵盈,如果不在更新期間陋率,那么就等于當(dāng)前的 fiber球化,如果是新創(chuàng)建的節(jié)點(diǎn),那么就沒(méi)有 alternate
// dev mode
_debugID; // 自增的標(biāo)識(shí)每一個(gè) fiber 的 id
_debugSource = null; // 文件名和行數(shù)瓦糟,與 React Element 的 source 一致
_debugOwner = null; // 與 React Element 的 owner 一致
_debugIsCurrentlyTiming = false;
}