<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Vue
建立檢視分為倆種情況:
template
轉換成的真實DOM
來替換應用中的根元素template
對應的虛擬節點來建立真實DOM
,而是會用老的虛擬節點和新的虛擬節點進行比對,根據比對結果來更新DOM
第二種情況就是Vue
中經常談到的DOM Diff
,接下來我們將詳細介紹新老節點的比對過程。
老的虛擬節點和新的虛擬節點是倆棵樹,會對倆棵樹每層中的虛擬節點進行比對操作:
在每一層進行對比時,會分別為老節點和新節點設定頭尾指標:
整體的孩子節點比對思路如下:
在我們渲染檢視之前,需要儲存當前渲染的虛擬節點。在下一次渲染檢視時,它就是老的虛擬節點,要和新的虛擬節點進行對比:
// src/lifecycle.js Vue.prototype._update = function (vNode) { const vm = this; const preVNode = vm._vNode; vm._vNode = vNode; if (!preVNode) { // 首次渲染,沒有前一次的虛擬節點 vm.$el = patch(vm.$el, vNode); } else { // vm._vNode中儲存了前一次的虛擬節點,進行dom diff patch(preVNode, vNode); } };
下面我們實現patch
方法中的邏輯
在patch
方法中,首先會判斷oldVNode
是否為真實DOM
。如果不是,會進行DOM diff
。
如果新的虛擬節點和老的虛擬節點標籤不一樣,直接用新的虛擬節點建立真實節點,然後替換老的真實節點即可:
const vm1 = new Vue(); const html1 = ` <div id="app"> 111 </div> `; // 將模板編譯為render函數 const render1 = compileToFunctions(html1); const vNode1 = render1.call(vm1); // 當oldVNode為DOM元素時,會用新節點直接替換老節點 patch(document.getElementById('app'), vNode1); const html2 = ` <span id="app"> 333 </span> `; // 將新的模本編譯為render函數 const render2 = compileToFunctions(html2); // 生成新的虛擬節點 const vNode2 = render2.call(vm1); // 老節點和新節點進行對比 patch(vNode1, vNode2);
上述程式碼會直接通過新的虛擬節點建立的真實節點來替換老的真實節點,patch
中的程式碼如下:
export function patch (oldVNode, vNode) { if (oldVNode.nodeType) { // 舊的節點為真實節點 // some code... } else { // 新舊節點都為虛擬節點,要進行dom diff if (oldVNode.tag !== vNode.tag) { // 標籤不相同,直接用新節點替換老節點 const newEle = createElement(vNode); replaceChild(newEle, oldVNode.el); return newEle; } } }
如果老節點和新節點都是文字標籤,那麼直接用新節點的文字替換老節點即可:
// 老的模板 const html1 = ` <div id="app"> 111 </div> `; // 新的模板 const html2 = ` <div id="app"> 333 </div> `;
上例中的新的文字333
會替換掉老的文字111
,patch
中的實現如下:
export function patch (oldVNode, vNode) { if (oldVNode.nodeType) { // 舊的節點為真實節點 // some code ... } else { // 新舊節點都為虛擬節點,要進行dom diff if (oldVNode.tag !== vNode.tag) { // 不相等直接替換 // some code ... } if (!oldVNode.tag) { // 文位元組點,tag相同,都為undefined oldVNode.el.textContent = vNode.text; return oldVNode.el; } } }
當老節點和新節點的標籤相同時,要更新標籤對應真實元素的屬性,更新規則如下:
function updateProperties (vNode, oldProps = {}) { // 老節點和新節點的屬性 const { el, props } = vNode; // 用新節點替換老節點中的屬性 for (const key in props) { // 為真實DOM設定新節點的所有屬性 if (props.hasOwnProperty(key)) { const value = props[key]; if (key === 'style') { for (const styleKey in value) { if (value.hasOwnProperty(styleKey)) { el.style[styleKey] = value[styleKey]; } } } else { el.setAttribute(key, value); } } } // 如果老節點中有,而新節點中沒有,需要將其刪除 for (const key in oldProps) { if (oldProps.hasOwnProperty(key) && !props.hasOwnProperty(key)) { el.removeAttribute(key); } } const style = oldProps.style || {}; const newStyle = props.style || {}; // 刪除老節點中多餘的樣式 for (const key in style) { if (!newStyle.hasOwnProperty(key) && style.hasOwnProperty(key)) { el.style[key] = ''; } } }
在比對完當前節點後,要繼續比對孩子節點。孩子節點可能有以下情況:
patch中對應的程式碼如下:
export function patch (oldVNode, vNode) { if (oldVNode.nodeType) { // 舊的節點為真實節點 // some code ... } else { // 新舊節點都為虛擬節點,要進行dom diff // 元素相同,需要比較子元素 const el = vNode.el = oldVNode.el; // 更新屬性 updateProperties(vNode, oldVNode.props); const oldChildren = oldVNode.children; const newChildren = vNode.children; // 老的有,新的沒有,將老的設定為空 // 老的沒有,新的有,為老節點插入多有的新節點 // 老的和新的都有,遍歷每一個進行比對 if (!oldChildren.length && newChildren.length) { for (let i = 0; i < newChildren; i++) { const child = newChildren[i]; el.appendChild(createElement(child)); } return; } if (oldChildren.length && !newChildren.length) { return el.innerHTML = ''; } if (oldChildren.length && newChildren.length) { updateChildren(oldChildren, newChildren, el); } return el; } }
下面我們的邏輯便到了updateChildren
中。
在對孩子節點的比對中,對一些常見的DOM
操作通過雙指標進行了優化:
我們在程式碼中先宣告需要的變數:
function updateChildren (oldChildren, newChildren, parent) { let oldStartIndex = 0, // 老孩子的開始索引 oldStartVNode = oldChildren[0], // 老孩子的頭虛擬節點 oldEndIndex = oldChildren.length - 1, // 老孩子的尾索引 oldEndVNode = oldChildren[oldEndIndex]; // 老孩子的尾虛擬節點 let newStartIndex = 0, // 新孩子的開始索引 newStartVNode = newChildren[0], // 新孩子的頭虛擬節點 newEndIndex = newChildren.length - 1, // 新孩子的尾索引 newEndVNode = newChildren[newEndIndex]; // 新孩子的尾虛擬節點 }
當節點的tag
和key
都相同時,我們認為這倆個節點是同一個節點,可以進行復用:
function isSameVNode (oldVNode, newVNode) { return oldVNode.key === newVNode.key && oldVNode.tag === newVNode.tag; }
下面我們分別來講解對應的優化邏輯
我們在老節點孩子的末尾新增一個元素作為新節點,其對應的template
如下:
const template1 = ` <div id="app"> <ul> <li key="A" style="color:red">A</li> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> <li key="D" style="color:green">D</li> </ul> </div> `; const template2 = ` <div id="app"> <ul> <li key="A" style="color:red">A</li> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> <li key="D" style="color:green">D</li> <li key="E" style="color:purple">E</li> </ul> </div> `;
此時oldChildren
中的頭節點和newChildren
中的頭節點相同,其比對邏輯如下:
oldStartVNode
和newStartVNode
執行patch
方法,比對它們的標籤、屬性、文字以及孩子節點oldStartVNode
和newStartVNode
同時後移,繼續進行比對oldEndVNode.el.nextSibling
之前畫圖演示下詳細的比對邏輯:
程式碼如下:
function updateChildren (oldChildren, newChildren, parent) { while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { if (isSameVNode(oldStartIndex, newStartIndex)) { // 頭和頭相等 // 1. 可能是文位元組點:需要繼續比對文位元組點 // 2. 可能是元素:先比對元素的屬性,然後再比對子節點 patch(oldStartVNode, newStartVNode); oldStartVNode = oldChildren[++oldStartIndex]; newStartVNode = newChildren[++newStartIndex]; } } // 新節點中剩餘元素插入到真實節點中 for (let i = newStartIndex; i <= newEndIndex; i++) { const child = newChildren[i]; const refEle = oldChildren[oldEndIndex + 1] || null; parent.insertBefore(createElement(child), refEle); } }
老節點的孩子的頭部新增元素E
,此時新老節點的template
結構如下:
const template1 = ` <div id="app"> <ul> <li key="A" style="color:red">A</li> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> <li key="D" style="color:green">D</li> </ul> </div> `; const template2 = ` <div id="app"> <ul> <li key="E" style="color:purple">E</li> <li key="A" style="color:red">A</li> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> <li key="D" style="color:green">D</li> </ul> </div> `;
其比對邏輯和尾部新增類似,只不過此時是oldEndVNode
和newEndVNode
相同:
patch
比對oldEndVNode
和newEndVNode
的標籤、屬性、文字及孩子節點oldEndVNode
和newEndVNode
同時前移,繼續進行比對oldEndVNode.el.nextSibling
之前該邏輯的示意圖如下:
patch中新增程式碼如下:
function updateChildren (oldChildren, newChildren, parent) { while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { if (isSameVNode(oldStartIndex, newStartIndex)) { // 頭和頭相等 // some code ... } else if (isSameVNode(oldEndVNode, newEndVNode)) { // 尾和尾相等 patch(oldEndVNode, newEndVNode); oldEndVNode = oldChildren[--oldEndIndex]; newEndVNode = newChildren[--newEndIndex]; } } // some code ... }
在新節點中,我們將開始元素A
移動到末尾,對應的template
如下:
const template1 = ` <div id="app"> <ul> <li key="A" style="color:red">A</li> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> <li key="D" style="color:green">D</li> </ul> </div> `; const template2 = ` <div id="app"> <ul> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> <li key="D" style="color:green">D</li> <li key="A" style="color:red">A</li> </ul> </div> `;
此時oldStartVNode
和newEndVNode
相同:
patch
比對oldStartVNode
和newEndVNode
的標籤、屬性、文字及孩子節點oldStartVNode
對應的真實節點插入到oldEndVNode
對應的真實節點之後oldStartVNode
後移,newEndVNode
前移用圖來演示該過程:
在patch方法中編寫程式碼:
function updateChildren (oldChildren, newChildren, parent) { while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { if (isSameVNode(oldStartIndex, newStartIndex)) { // 頭和頭相等 // some code ... } else if (isSameVNode(oldEndVNode, newEndVNode)) { // 尾和尾相等 // some code ... } else if (isSameVNode(oldStartVNode, newEndVNode)) { // 將開頭元素移動到了末尾:尾和頭相同 // 老節點:需要將頭節點對應的元素移動到尾節點之後 parent.insertBefore(oldStartVNode, oldEndVNode.el.nextSibling); patch(oldStartVNode, newEndVNode); oldStartVNode = oldChildren[++oldStartIndex]; newEndVNode = newChildren[--newEndIndex]; } } }
講解到這裡,大家可以先停下閱讀的腳步,參考一下之前的邏輯,想想這裡會如何進行比對?
在新節點中,我們將末尾元素D
移動到開頭,對應的template
如下:
const template1 = ` <div id="app"> <ul> <li key="A" style="color:red">A</li> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> <li key="D" style="color:green">D</li> </ul> </div> `; const template2 = ` <div id="app"> <ul> <li key="D" style="color:green">D</li> <li key="A" style="color:red">A</li> <li key="B" style="color:yellow">B</li> <li key="C" style="color:blue">C</li> </ul> </div> `;
此時oldEndVNode
和newStartVNode
相同:
patch
比對oldEndVNode
和newStartVNode
的標籤、屬性、文字及孩子節點oldEndVNode
對應的真實節點插入到oldStartVNode
對應的真實節點之前oldEndVNode
前移,newStartVNode
後移畫圖來演示該過程:
在patch
方法中新增處理該邏輯的程式碼:
function updateChildren (oldChildren, newChildren, parent) { // 更新子節點: // 1. 一層一層進行比較,如果發現有一層不一樣,直接就會用新節點的子集來替換父節點的子集。 // 2. 比較時會採用雙指標,對常見的操作進行優化 let oldStartIndex = 0, oldStartVNode = oldChildren[0], oldEndIndex = oldChildren.length - 1, oldEndVNode = oldChildren[oldEndIndex]; let newStartIndex = 0, newStartVNode = newChildren[0], newEndIndex = newChildren.length - 1, newEndVNode = newChildren[newEndIndex]; function makeMap () { const map = {}; for (let i = 0; i < oldChildren.length; i++) { const child = oldChildren[i]; child.key && (map[child.key] = i); } return map; } // 將老節點的key和索引進行對映,之後可以直接通過key找到索引,然後通過索引找到對應的元素 // 這樣提前做好對映關係,可以將查詢的時間複雜度降到O(1) const map = makeMap(); while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { if (isSameVNode(oldStartIndex, newStartIndex)) { // 頭和頭相等 // some code ... } else if (isSameVNode(oldEndVNode, newEndVNode)) { // 尾和尾相等 // some code ... } else if (isSameVNode(oldStartVNode, newEndVNode)) { // 將開頭元素移動到了末尾:尾和頭相同 // some code ... } else if (isSameVNode(oldEndVNode, newStartVNode)) { // 將結尾元素移動到了開頭 // 老節點: 將尾指標元素插入到頭指標之前 parent.insertBefore(oldEndVNode.el, oldStartVNode.el); patch(oldEndVNode, newStartVNode); oldEndVNode = oldChildren[--oldEndIndex]; newStartVNode = newChildren[++newStartIndex]; } } }
到這裡,patch
方法中已經完成了所有的優化操作,下面我們來看下如何對比亂序的孩子節點
當進行比對的元素不滿足優化條件時,就要進行亂序對比。下面是倆個亂序的template
,看下它們的具體比對過程:
const html1 = ` <div id="app"> <ul> <li key="D" style="color:red">D</li> <li key="B" style="color:yellow">B</li> <li key="Z" style="color:blue">Z</li> <li key="F" style="color:green">F</li> </ul> </div> `; const html2 = ` <div id="app"> <ul> <li key="E" style="color:green">E</li> <li key="F" style="color:red">F</li> <li key="D" style="color:yellow">D</li> <li key="Q" style="color:blue">Q</li> <li key="B" style="color:#252a34">B</li> <li key="M" style="color:#fc5185">M</li> </ul> </div> `;
亂序比對的邏輯如下:
key
在老節點中進行查詢key
相同的元素,將對應的真實節點移動到oldStartVNode.el
(老虛擬頭節點對應的真實節點)之前,並且將其對應的虛擬節點設定為null
,之後遇到null
跳過即可,不再對其進行比對。patch
方法比對移動的節點和newStartVNode
的標籤、屬性、文字以及孩子節點key
相同的元素,會為新節點的頭節點建立對應的真實節點,將其插入到oldStartVNode.el
之前畫圖演示下template
中節點的比對過程:
在比對開始之前,我們要先遍歷老的孩子節點,生成key
與索引對應的map
:
function updateChildren (oldChildren, newChildren, parent) { function makeMap () { const map = {}; for (let i = 0; i < oldChildren.length; i++) { const child = oldChildren[i]; child.key && (map[child.key] = i); } return map; } const map = makeMap(); }
有了map
之後,便可以很方便的通過key
來找到老孩子節點的索引,然後通過索引直接找到對應的孩子節點,而不用再次進行遍歷操作。
接下來書寫處理亂序節點的程式碼:
function updateChildren (oldChildren, newChildren, parent) { while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { if (oldStartVNode == null) { // 老節點null時跳過該次迴圈 oldStartVNode = oldChildren[++oldStartIndex]; continue; } else if (oldEndVNode == null) { oldEndVNode = oldChildren[--oldEndIndex]; continue; } else if (isSameVNode(oldStartIndex, newStartIndex)) { // 頭和頭相等 // some code ... } else if (isSameVNode(oldStartVNode, newEndVNode)) { // 將開頭元素移動到了末尾:尾和頭相同 // some code ... } else if (isSameVNode(oldEndVNode, newStartVNode)) { // 將結尾元素移動到了開頭 // some code ... } else { // 1. 用key來進行尋找,找到將其移動到頭節點之前 // 2. 沒有找到,將新頭節點插入到老頭節點之前 let moveIndex = map[newStartVNode.key]; // 通過key在map中找到相同元素的索引 if (moveIndex != null) { // 找到了 const moveVNode = oldChildren[moveIndex]; parent.insertBefore(moveVNode.el, oldStartVNode.el); oldChildren[moveIndex] = null; // 將移動這項標記為null,之後跳過,不再進行比對 // 還有對其屬性和子節點再進行比較 patch(moveVNode, newStartVNode); } else { // 為新頭節建立對應的真實節點並插入到老節點的頭節點之前 parent.insertBefore(createElement(newStartVNode), oldStartVNode.el); } newStartVNode = newChildren[++newStartIndex]; } } // some code ... // 老節點中從頭指標到尾指標為多餘的元素,需要刪除掉 for (let i = oldStartIndex; i <= oldEndIndex; i++) { const child = oldChildren[i]; parent.removeChild(child.el); } }
當新節點在老節點中存在時,我們會將找到的真實節點移動到相應的位置。此時老節點中的該節點不需要再被遍歷,為了防止陣列塌陷,便將該節點設定為null
。之後再遍歷時,如果發現節點的值為null
,便跳過本次迴圈。
現在我們便完成了Vue
在陣列更新時所有的DOM Diff
邏輯。
文中主要書寫了patch
方法的程式碼,其主要功能如下:
希望小夥伴在讀完本文之後,可以對Vue
的DOM Diff
過程有更深的理解。
到此這篇關於Vue中的 DOM與Diff詳情的文章就介紹到這了,更多相關Vue DOM與Diff內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45