這篇文章帶大家深入了解vue中的虛擬DOM和Diff演算法,詳細分析一下虛擬DOM和Diff演算法,希望對大家有幫助。
最近複習到虛擬DOM與Diff,翻閱了眾多資料,特此總結了這篇長文,加深自己對vue的理解。
這篇文章比較詳細的分析了vue的虛擬DOM,Diff演算法,其中一些關鍵的地方從別處搬運了一些圖進行說明(感謝製圖的大佬),也包含比較詳細的源碼解讀。如果感覺文章不錯,麻煩各位帥比給小弟點個贊,如果有錯誤的地方,也歡迎各位大佬在評論區指出,當然如果有不明白的部分,也歡迎提問。 【相關推薦:《vue.js教學》】
真實DOM的渲染
在講虛擬DOM之前,先說真實DOM的渲染。
瀏覽器真實DOM渲染的過程大概分成以下幾個部分
建構DOM樹 。透過HTML parser解析處理HTML標記,將它們建構成DOM樹(DOM tree),當解析器遇到非阻塞資源(圖片,css),會繼續解析,但是如果遇到script標籤(特別是沒有async 和defer屬性),會阻塞渲染並停止html的解析,這就是為啥最好把script標籤放在body下面的原因。
建構CSSOM樹。與建置DOM類似,瀏覽器也會將樣式規則,建置成CSSOM。瀏覽器會遍歷CSS中的規則集,根據css選擇器建立具有父子,兄弟等關係的節點樹。
建立Render樹。這一步驟將DOM和CSSOM關聯,確定每個 DOM 元素應該套用什麼 CSS 規則。將所有相關樣式配對到DOM樹中的每個可見節點,並根據CSS級聯確定每個節點的計算樣式,不可見節點(head,屬性包括 display:none的節點)不會產生到Render樹中。
佈局/回流(Layout/Reflow)。瀏覽器第一次確定節點的位置以及大小叫佈局,如果後續節點位置以及大小發生變化,這一步驟觸發佈局調整,也就是 回流。
繪製/重繪(Paint/Repaint)。將元素的每個視覺部分繪製到螢幕上,包括文字、顏色、邊框、陰影和替換的元素(如按鈕和圖像)。如果文字、顏色、邊框、陰影等這些元素發生變化時,會觸發重繪(Repaint)。為了確保重繪的速度比初始繪製的速度更快,螢幕上的繪圖通常會分解成數層。將內容提升到GPU層(可以透過tranform,filter,will-change,opacity觸發)可以提高繪製以及重繪的效能。
合成(Compositing)。這一步驟將繪製過程中的分層合併,確保它們以正確的順序繪製到螢幕上顯示正確的內容。
為啥需要虛擬DOM
上面這是一次DOM渲染的過程,如果dom更新,那麼dom需要重新渲染一次,如果存在下面這種情況
<body> <div id="container"> <div class="content" style="color: red;font-size:16px;"> This is a container </div> .... <div class="content" style="color: red;font-size:16px;"> This is a container </div> </div> </body> <script> let content = document.getElementsByClassName('content'); for (let i = 0; i < 1000000; i++) { content[i].innerHTML = `This is a content${i}`; // 触发回流 content[i].style.fontSize = `20px`; } </script>
那麼需要真實的操作DOM100w次,觸發了回流100w次。每次DOM的更新都會依照流程進行無差別的真實dom的更新。所以造成了很大的效能浪費。如果循環裡面是複雜的操作,經常觸發回流與重繪,那麼就很容易就影響性能,造成卡頓。另外這裡要說明的是,虛擬DOM並不是代表比DOM更快,效能需要分場景,虛擬DOM的效能跟模板大小是正相關。虛擬DOM的比較過程是不會區分資料量大小的,在元件內部只有少量動態節點時,虛擬DOM依然是會對整個vdom進行遍歷,相比直接渲染而言是多了一層操作的。
<div class="list"> <p class="item">item</p> <p class="item">item</p> <p class="item">item</p> <p class="item">{{ item }}</p> <p class="item">item</p> <p class="item">item</p> </div>
例如上面這個例子,虛擬DOM。雖然只有一個動態節點,但是虛擬DOM依然需要遍歷diff整個list的class,文本,標籤等信息,最後依然需要進行DOM渲染。如果只是dom操作,就只要操作一個特定的DOM然後進行渲染。虛擬DOM最核心的價值在於,它能透過js描述真實DOM,表達力更強,透過聲明式的語言操作,為開發者提供了更方便快捷開發體驗,而且在沒有手動優化,大部分情景下,保證了性能下限,性價比更高。
虛擬DOM
虛擬DOM本質上是一個js對象,透過物件來表示真實的DOM結構。 tag用來描述標籤,props用來描述屬性,children用來表示嵌套的層級關係。
const vnode = { tag: 'div', props: { id: 'container', }, children: [{ tag: 'div', props: { class: 'content', }, text: 'This is a container' }] } //对应的真实DOM结构 <div id="container"> <div class="content"> This is a container </div> </div>
虛擬DOM的更新不會立即操作DOM,而是會透過diff演算法,找出需要更新的節點,按需更新,並將更新的內容儲存為js對象,更新完成後再掛載到真實dom上,實現真實的dom更新。透過虛擬DOM,解決了操作真實DOM的三個問題。
无差别频繁更新导致DOM频繁更新,造成性能问题
频繁回流与重绘
开发体验
另外由于虚拟DOM保存的是js对象,天然的具有跨平台的能力,而不仅仅局限于浏览器。
优点
总结起来,虚拟DOM的优势有以下几点
小修改无需频繁更新DOM,框架的diff算法会自动比较,分析出需要更新的节点,按需更新
更新数据不会造成频繁的回流与重绘
表达力更强,数据更新更加方便
保存的是js对象,具备跨平台能力
不足
虚拟DOM同样也有缺点,首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
虚拟DOM实现原理
主要分三部分
通过js建立节点描述对象
diff算法比较分析新旧两个虚拟DOM差异
将差异patch到真实dom上实现更新
Diff算法
为了避免不必要的渲染,按需更新,虚拟DOM会采用Diff算法进行虚拟DOM节点比较,比较节点差异,从而确定需要更新的节点,再进行渲染。vue采用的是深度优先,同层比较的策略。
新节点与旧节点的比较主要是围绕三件事来达到渲染目的
创建新节点
删除废节点
更新已有节点
如何比较新旧节点是否一致呢?
function sameVnode(a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) //对input节点的处理 ) || ( isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error) ) ) ) } //判断两个节点是否是同一种 input 输入类型 function sameInputType(a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type //input type 相同或者两个type都是text return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) }
可以看到,两个节点是否相同是需要比较标签(tag),属性(在vue中是用data表示vnode中的属性props), 注释节点(isComment) 的,另外碰到input的话,是会做特殊处理的。
创建新节点
当新节点有的,旧节点没有,这就意味着这是全新的内容节点。只有元素节点,文本节点,注释节点才能被创建插入到DOM中。
删除旧节点
当旧节点有,而新节点没有,那就意味着,新节点放弃了旧节点的一部分。删除节点会连带的删除旧节点的子节点。
更新节点
新的节点与旧的的节点都有,那么一切以新的为准,更新旧节点。如何判断是否需要更新节点呢?
- 判断新节点与旧节点是否完全一致,一样的话就不需要更新
// 判断vnode与oldVnode是否完全一样 if (oldVnode === vnode) { return; }
- 判断新节点与旧节点是否是静态节点,key是否一样,是否是克隆节点(如果不是克隆节点,那么意味着渲染函数被重置了,这个时候需要重新渲染)或者是否设置了once属性,满足条件的话替换componentInstance
// 是否是静态节点,key是否一样,是否是克隆节点或者是否设置了once属性 if ( isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance; return; }
- 判断新节点是否有文本(通过text属性判断),如果有文本那么需要比较同层级旧节点,如果旧节点文本不同于新节点文本,那么采用新的文本内容。如果新节点没有文本,那么后面需要对子节点的相关情况进行判断
//判断新节点是否有文本 if (isUndef(vnode.text)) { //如果没有文本,处理子节点的相关代码 .... } else if (oldVnode.text !== vnode.text) { //新节点文本替换旧节点文本 nodeOps.setTextContent(elm, vnode.text) }
- 判断新节点与旧节点的子节点相关状况。这里又能分为4种情况
新节点与旧节点都有子节点
只有新节点有子节点
只有旧节点有子节点
新节点与旧节点都没有子节点
都有子节点
对于都有子节点的情况,需要对新旧节点做比较,如果他们不相同,那么需要进行diff操作,在vue中这里就是updateChildren方法,后面会详细再讲,子节点的比较主要是双端比较。
//判断新节点是否有文本 if (isUndef(vnode.text)) { //新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } } else if (oldVnode.text !== vnode.text) { //新节点文本替换旧节点文本 nodeOps.setTextContent(elm, vnode.text) }
只有新节点有子节点
只有新节点有子节点,那么就代表着这是新增的内容,那么就是新增一个子节点到DOM,新增之前还会做一个重复key的检测,并做出提醒,同时还要考虑,旧节点如果只是一个文本节点,没有子节点的情况,这种情况下就需要清空旧节点的文本内容。
//只有新节点有子节点 if (isDef(ch)) { //检查重复key if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } //清除旧节点文本 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') //添加新节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } //检查重复key function checkDuplicateKeys(children) { const seenKeys = {} for (let i = 0; i < children.length; i++) { const vnode = children[i] //子节点每一个Key const key = vnode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, vnode.context ) } else { seenKeys[key] = true } } } }
只有旧节点有子节点
只有旧节点有,那就说明,新节点抛弃了旧节点的子节点,所以需要删除旧节点的子节点
if (isDef(oldCh)) { //删除旧节点 removeVnodes(oldCh, 0, oldCh.length - 1) }
都没有子节点
这个时候需要对旧节点文本进行判断,看旧节点是否有文本,如果有就清空
if (isDef(oldVnode.text)) { //清空 nodeOps.setTextContent(elm, '') }
整体的逻辑代码如下
function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 判断vnode与oldVnode是否完全一样 if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // 克隆重用节点 vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 是否是静态节点,key是否一样,是否是克隆节点或者是否设置了once属性 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { //调用update回调以及update钩子 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } //判断新节点是否有文本 if (isUndef(vnode.text)) { //新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { //只有新节点有子节点 if (process.env.NODE_ENV !== 'production') { //重复Key检测 checkDuplicateKeys(ch) } //清除旧节点文本 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') //添加新节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { //只有旧节点有子节点,删除旧节点 removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { //新旧节点都无子节点 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { //新节点文本替换旧节点文本 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
配上流程图会更清晰点
子节点的比较更新updateChildren
新旧节点都有子节点的情况下,这个时候是需要调用updateChildren方法来比较更新子节点的。所以在数据上,新旧节点子节点,就保存为了两个数组。
const oldCh = [oldVnode1, oldVnode2,oldVnode3]; const newCh = [newVnode1, newVnode2,newVnode3];
子节点更新采用的是双端比较的策略,什么是双端比较呢,就是新旧节点比较是通过互相比较首尾元素(存在4种比较),然后向中间靠拢比较(newStartIdx,与oldStartIdx递增,newEndIdx与oldEndIdx递减)的策略。
比较过程
向中间靠拢
这里对上面出现的新前,新后,旧前,旧后做一下说明
新前,指的是新节点未处理的子节点数组中的第一个元素,对应到vue源码中的newStartVnode
新后,指的是新节点未处理的子节点数组中的最后一个元素,对应到vue源码中的newEndVnode
旧前,指的是旧节点未处理的子节点数组中的第一个元素,对应到vue源码中的oldStartVnode
旧后,指的是旧节点未处理的子节点数组中的最后一个元素,对应到vue源码中的oldEndVnode
子节点比较过程
接下来对上面的比较过程以及比较后做的操作做下说明
- 新前与旧前的比较,如果他们相同,那么进行上面说到的patchVnode更新操作,然后新旧节点各向后一步,进行第二项的比较...直到遇到不同才会换种比较方式
if (sameVnode(oldStartVnode, newStartVnode)) { // 更新子节点 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 新旧各向后一步 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }
- 新后与旧后的比较,如果他们相同,同样进行pathchVnode更新,然后新旧各向前一步,进行前一项的比较...直到遇到不同,才会换比较方式
if (sameVnode(oldEndVnode, newEndVnode)) { //更新子节点 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // 新旧向前 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }
- 新后与旧前的比较,如果它们相同,就进行更新操作,然后将旧前移动到所有未处理旧节点数组最后面,使旧前与新后位置保持一致,然后双方一起向中间靠拢,新向前,旧向后。如果不同会继续切换比较方式
if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //将旧子节点数组第一个子节点移动插入到最后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) //旧向后 oldStartVnode = oldCh[++oldStartIdx] //新向前 newEndVnode = newCh[--newEndIdx]
- 新前与旧后的比较,如果他们相同,就进行更新,然后将旧后移动到所有未处理旧节点数组最前面,是旧后与新前位置保持一致,,然后新向后,旧向前,继续向中间靠拢。继续比较剩余的节点。如果不同,就使用传统的循环遍历查找。
if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) //将旧后移动插入到最前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) //旧向前 oldEndVnode = oldCh[--oldEndIdx] //新向后 newStartVnode = newCh[++newStartIdx] }
- 循环遍历查找,上面四种都没找到的情况下,会通过key去查找匹配。
进行到这一步对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index}
// 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index} if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
然后拿新节点的key与旧节点进行比较,找到key值匹配的节点的位置,这里需要注意的是,如果新节点也没key,那么就会执行findIdxInOld方法,从头到尾遍历匹配旧节点
//通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) //findIdxInOld方法 function findIdxInOld(node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] //找到相同节点下标 if (isDef(c) && sameVnode(node, c)) return i } }
如果通过上面的方法,依旧没找到新节点与旧节点匹配的下标,那就说明这个节点是新节点,那就执行新增的操作。
//如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作 if (isUndef(idxInOld)) { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) }
如果找到了,那么说明在旧节点中找到了key值一样,或者节点和key都一样的旧节点。如果节点一样,那么在patchVnode之后,需要将旧节点移动到所有未处理节点之前,对于key一样,元素不同的节点,将其认为是新节点,执行新增操作。执行完成后,新节点向后一步。
//如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作 if (isUndef(idxInOld)) { // 新增元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 在旧节点中找到了key值一样的节点 vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 相同子节点更新操作 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 更新完将旧节点赋值undefined oldCh[idxInOld] = undefined //将旧节点移动到所有未处理节点之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果是相同的key,不同的元素,当做新节点,执行创建操作 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } //新节点向后 newStartVnode = newCh[++newStartIdx]
当完成对旧节点的遍历,但是新节点还没完成遍历,那就说明后续的都是新增节点,执行新增操作,如果完成对新节点遍历,旧节点还没完成遍历,那么说明旧节点出现冗余节点,执行删除操作。
//完成对旧节点的遍历,但是新节点还没完成遍历, 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(oldCh, oldStartIdx, oldEndIdx) }
子节点比较总结
上面就是子节点比较更新的一个完整过程,这是完整的逻辑代码
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] //旧前 let oldEndVnode = oldCh[oldEndIdx] //旧后 let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] //新前 let newEndVnode = newCh[newEndIdx] //新后 let 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 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { 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, newCh, newStartIdx) // 新旧各向后一步 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { //新后与旧后 //更新子节点 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //新旧各向前一步 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // 新后与旧前 //更新子节点 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //将旧前移动插入到最后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) //新向前,旧向后 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // 新前与旧后 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) //将旧后移动插入到最前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) //新向后,旧向前 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index} if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) //通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) //如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作 if (isUndef(idxInOld)) { // 新增元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 在旧节点中找到了key值一样的节点 vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 相同子节点更新操作 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 更新完将旧节点赋值undefined oldCh[idxInOld] = undefined //将旧节点移动到所有未处理节点之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果是相同的key,不同的元素,当做新节点,执行创建操作 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } //新节点向后一步 newStartVnode = newCh[++newStartIdx] } } //完成对旧节点的遍历,但是新节点还没完成遍历, 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(oldCh, oldStartIdx, oldEndIdx) } }
更多编程相关知识,请访问:编程入门!!
以上是深入理解vue中的虛擬DOM和Diff演算法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

Vue.js是一種漸進式JavaScript框架,適用於構建複雜的用戶界面。 1)其核心概念包括響應式數據、組件化和虛擬DOM。 2)實際應用中,可以通過構建Todo應用和集成VueRouter來展示其功能。 3)調試時,建議使用VueDevtools和console.log。 4)性能優化可通過v-if/v-show、列表渲染優化和異步加載組件等實現。

Vue.js適合小型到中型項目,而React更適用於大型、複雜應用。 1.Vue.js的響應式系統通過依賴追踪自動更新DOM,易於管理數據變化。 2.React採用單向數據流,數據從父組件流向子組件,提供明確的數據流向和易於調試的結構。

Vue.js適合中小型項目和快速迭代,React適用於大型複雜應用。 1)Vue.js易於上手,適用於團隊經驗不足或項目規模較小的情況。 2)React的生態系統更豐富,適合有高性能需求和復雜功能需求的項目。

實現 Vue 中 a 標籤跳轉的方法包括:HTML 模板中使用 a 標籤指定 href 屬性。使用 Vue 路由的 router-link 組件。使用 JavaScript 的 this.$router.push() 方法。可通過 query 參數傳遞參數,並在 router 選項中配置路由以進行動態跳轉。

Vue 中實現組件跳轉有以下方法:使用 router-link 和 <router-view> 組件進行超鏈接跳轉,指定 :to 屬性為目標路徑。直接使用 <router-view> 組件顯示當前路由渲染的組件。使用 router.push() 和 router.replace() 方法進行程序化導航,前者保存歷史記錄,後者替換當前路由不留記錄。

Vue 中 div 元素跳轉的方法有兩種:使用 Vue Router,添加 router-link 組件。添加 @click 事件監聽器,調用 this.$router.push() 方法跳轉。

Vue.js提供了三種跳轉方式:原生 JavaScript API:使用 window.location.href 進行跳轉。 Vue Router:使用 <router-link> 標籤或 this.$router.push() 方法進行跳轉。 VueX:通過 dispatch action 或 commit mutation 來觸發路由跳轉。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

記事本++7.3.1
好用且免費的程式碼編輯器

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。