一、真實DOM和其解析流程?
瀏覽器渲染引擎工作流程都差不多,大致分為5步,創建DOM樹——創建StyleRules——創建Render樹——布局Layout——繪制Painting
第一步,用HTML分析器,分析HTML元素,構建一顆DOM樹(標記化和樹構建)。
第二步,用CSS分析器,分析CSS文件和元素上的inline樣式,生成頁面的樣式表。
第三步,將DOM樹和樣式表,關聯起來,構建一顆Render樹(這一過程又稱為Attachment)。每個DOM節點都有attach方法,接受樣式信息,返回一個render對象(又名renderer)。這些render對象最終會被構建成一顆Render樹。
第四步,有了Render樹,瀏覽器開始布局,為每個Render樹上的節點確定一個在顯示屏上出現的精確坐標。
第五步,Render樹和節點顯示坐標都有了,就調用每個節點paint方法,把它們繪制出來。
DOM樹的構建是文檔加載完成開始的?構建DOM數是一個漸進過程,為達到更好用戶體驗,渲染引擎會盡快將內容顯示在屏幕上。它不必等到整個HTML文檔解析完畢之后才開始構建render數和布局。
Render樹是DOM樹和CSSOM樹構建完畢才開始構建的嗎?這三個過程在實際進行的時候又不是完全獨立,而是會有交叉。會造成一邊加載,一遍解析,一遍渲染的工作現象。
CSS的解析是從右往左逆向解析的(從DOM樹的下-上解析比上-下解析效率高),嵌套標簽越多,解析越慢。
二、JS操作真實DOM的代價!
用我們傳統的開發模式,原生JS或JQ操作DOM時,瀏覽器會從構建DOM樹開始從頭到尾執行一遍流程。在一次操作中,我需要更新10個DOM節點,瀏覽器收到第一個DOM請求后並不知道還有9次更新操作,因此會馬上執行流程,最終執行10次。例如,第一次計算完,緊接着下一個DOM更新請求,這個節點的坐標值就變了,前一次計算為無用功。計算DOM節點坐標值等都是白白浪費的性能。即使計算機硬件一直在迭代更新,操作DOM的代價仍舊是昂貴的,頻繁操作還是會出現頁面卡頓,影響用戶體驗。
三、為什么需要虛擬DOM,它有什么好處?
Web界面由DOM樹(樹的意思是數據結構)來構建,當其中一部分發生變化時,其實就是對應某個DOM節點發生了變化,
虛擬DOM就是為了解決瀏覽器性能問題而被設計出來的。如前,若一次操作中有10次更新DOM的動作,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內容保存到本地一個JS對象中,最終將這個JS對象一次性attch到DOM樹上,再進行后續操作,避免大量無謂的計算量。所以,用JS對象模擬DOM節點的好處是,頁面的更新可以先全部反映在JS對象(虛擬DOM)上,操作內存中的JS對象的速度顯然要更快,等更新完成后,再將最終的JS對象映射成真實的DOM,交由瀏覽器去繪制。
四、實現虛擬DOM
例如一個真實的DOM節點。
我們用JS來模擬DOM節點實現虛擬DOM。
其中的Element方法具體怎么實現的呢?
第一個參數是節點名(如div),第二個參數是節點的屬性(如class),第三個參數是子節點(如ul的li)。除了這三個參數會被保存在對象上外,還保存了key和count。其相當於形成了虛擬DOM樹。
有了JS對象后,最終還需要將其映射成真實DOM
我們已經完成了創建虛擬DOM並將其映射成真實DOM,這樣所有的更新都可以先反應到虛擬DOM上,如何反應?需要用到Diff算法。
五、傳統diff
計算兩顆樹形結構差異並進行轉換,傳統diff算法是這樣做的:循環遞歸每一個節點
比如左側樹a節點依次進行如下對比,左側樹節點b、c、d、e亦是與右側樹每個節點對比
算法復雜度能達到O(n^2),n代表節點的個數
a->e、a->d、a->b、a->c、a->a
查找完差異后還需計算最小轉換方式,這其中的原理我沒仔細去看,最終達到的算法復雜度是O(n^3)
六、Vue優化的diff策略
既然傳統diff算法性能開銷如此之大,Vue做了什么優化呢?
- 跟react一樣,只進行同層級比較,忽略跨級操作
react以及Vue在diff時,都是在對比虛擬dom節點,下文提到的節點都指虛擬節點。Vue是怎樣描述一個節點的呢?
Vue虛擬節點
// body下的 <div id="v" class="classA"><div> 對應的 oldVnode 就是 { el: div //對真實的節點的引用,本例中就是document.querySelector('#id.classA') tagName: 'DIV', //節點的標簽 sel: 'div#v.classA' //節點的選擇器 data: null, // 一個存儲節點屬性的對象,對應節點的el[prop]屬性,例如onclick , style children: [], //存儲子節點的數組,每個子節點也是vnode結構 text: null, //如果是文本節點,對應文本節點的textContent,否則為null }
patch
diff時調用patch函數,patch接收兩個參數vnode,oldVnode,分別代表新舊節點。
function patch (oldVnode, vnode) { if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) } else { const oEl = oldVnode.el let parentEle = api.parentNode(oEl) createEle(vnode) if (parentEle !== null) { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) api.removeChild(parentEle, oldVnode.el) oldVnode = null } } return vnode }
patch函數內第一個if判斷sameVnode(oldVnode, vnode)就是判斷這兩個節點是否為同一類型節點,以下是它的實現:
function sameVnode(oldVnode, vnode){ //兩節點key值相同,並且sel屬性值相同,即認為兩節點屬同一類型,可進行下一步比較 return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel }
也就是說,即便同一個節點元素比如div,他的className不同,Vue就認為是兩個不同類型的節點,執行刪除舊節點、插入新節點操作。這與react diff實現是不同的,react對於同一個節點元素認為是同一類型節點,只更新其節點上的屬性。
patchVnode
對於同類型節點調用patchVnode(oldVnode, vnode)進一步比較:
patchVnode (oldVnode, vnode) { const el = vnode.el = oldVnode.el //讓vnode.el引用到現在的真實dom,當el修改時,vnode.el會同步變化。 let i, oldCh = oldVnode.children, ch = vnode.children if (oldVnode === vnode) return //新舊節點引用一致,認為沒有變化 //文本節點的比較 if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) { api.setTextContent(el, vnode.text) }else { updateEle(el, vnode, oldVnode) //對於擁有子節點(兩者的子節點不同)的兩個節點,調用updateChildren if (oldCh && ch && oldCh !== ch) { updateChildren(el, oldCh, ch) }else if (ch){ //只有新節點有子節點,添加新的子節點 createEle(vnode) //create el's children dom }else if (oldCh){ //只有舊節點內存在子節點,執行刪除子節點操作 api.removeChildren(el) } } }
updateChildren
patchVnode中有一個重要的概念updateChildren,這是Vue diff實現的核心:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { var oldStartIdx = 0; var newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, vnodeToMove, refElm; // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions var canMove = !removeOnly; { checkDuplicateKeys(newCh); } // 如果索引正常 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 當前的開始舊節點沒有定義,進入下一個節點 if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left // 當前的結束舊節點沒有定義,進入上一個節點 } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]; // 如果舊的開始節點與新的開始節點相同,則開始更新該節點,然后進入下一個節點 } else if (sameVnode(oldStartVnode, newStartVnode)) { // 更新節點 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; // 如果舊的結束節點與新的結束節點相同,則開始更新該節點,然后進入下一個節點 } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; // 如果舊的開始節點與新的結束節點相同,更新節點后把舊的開始節點移置節點末尾 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; // 如果舊的結束節點與新的開始節點相同,更新節點后把舊的結束節點移置節點開頭 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // 如果舊的節點沒有定義key,則創建key if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); // 如果沒有定義index,則創建新的新的節點元素 if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm); } else { vnodeToMove = oldCh[idxInOld]; if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined; canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm); } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm); } } newStartVnode = newCh[++newStartIdx]; } } // 如果舊節點的開始index大於結束index,則創建新的節點 如果新的開始節點index大於新的結束節點則刪除舊的節點 if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
