vue虛擬dom和diff演算法

来源:https://www.cnblogs.com/cuteyuchen/archive/2022/06/13/16372713.html
-Advertisement-
Play Games

vue的虛擬dom和diff演算法 1.虛擬dom 虛擬dom,我的理解就是通過js對象的方式來具體化每一個節點,把dom樹上面的每個節點都變為對象里的一個元素,元素的子元素變為子節點,節點上面的class、id、attribute等屬性變為data內的值,然後通過dom上面的createElemen ...


vue的虛擬dom和diff演算法

1.虛擬dom

虛擬dom,我的理解就是通過js對象的方式來具體化每一個節點,把dom樹上面的每個節點都變為對象里的一個元素,元素的子元素變為子節點,節點上面的class、id、attribute等屬性變為data內的值,然後通過dom上面的createElement、appendChild、insertBefore等方法進行生成dom樹。

let VNode = {
    sel:'div',
    data:{
        key:0,
        props:{},
        attrs:{},
        class:{},
        style:{},
        fn:{}
    },
    text:'虛擬dom',
    elm:'<div>虛擬dom</div>'
    children:[
        {
            sel:'div',
            data:{
                key:0,
                props:{},
                attrs:{},
                class:{},
                style:{},
                fn:{}
            },
            text:'虛擬dom children',
            elm:'<div>虛擬dom children</div>'
            children:[]
        }
    ]
}

 2.diff演算法

看了diff演算法後感覺寫的真是巧妙,真正做到了最小量更新 。

diff是當父節點相同時用來對子節點進行最小量更新的演算法。

diff演算法採用四個指針:舊節點開始指針,舊節點結束指針,新節點開始指針,新節點結束指針;

(上方虛擬節點中的key就是為了在進行diff演算法時判斷是否是同一個節點便於最小量更新)

 

while(舊節點開始指針<=舊節點結束指針&&新節點開始指針<=新節點結束指針){

分為以下五種情況:(前四種情況)

  當進行下麵5種判斷後可能會出現新節點[新節點開始指針]   舊節點[舊節點開始指針]   新節點[新節點結束指針]   舊節點[舊節點結束指針]為空值的情況,如果出現空值則代表當前節點已經處理過了,所以就需要將指針++或者--

if(舊節點[舊節點開始指針] ==null){

舊節點開始指針++

}else if(舊節點[舊節點結束指針]==null){

舊節點結束指針--

}else if(新節點[新節點開始指針] ==null){

新節點開始指針++

}else if(新節點[新節點結束指針] ==null){

新節點結束指針--

}

1、新節點[新節點開始指針] 對比  舊節點[舊節點開始指針]

  如果符合此種情況,則代表新節點[新節點開始指針] 舊節點[舊節點開始指針] 為同一個節點,實行最小量更新,即只更新節點內的屬性而不進行dom銷毀創建操作,完成更新後 新節點開始指針++舊節點開始指針++

2、新節點[新節點結束指針] 對比 舊節點[舊節點結束指針]

  如果符合此種情況,則代表新節點[新節點結束指針] 舊節點[舊節點結束指針] 為同一個節點,實行最小量更新,即只更新節點內的屬性而不進行dom銷毀創建操作,完成更新後 新節點結束指針--舊節點結束指針--

3、新節點[新節點結束指針] 對比 舊節點[舊節點開始指針]

  如果符合此種情況,則代表新節點[新節點結束指針] 舊節點[舊節點開始指針] 為同一個節點,實行最小量更新,先更新節點內的屬性,然後使用insertBefore將舊節點[舊節點開始指針] 移動到舊節點[舊節點結束指針] 之後,(註意:此處要移動到舊節點[舊節點結束節點] 後,而不是所有舊節點後,因為這裡的舊節點結束指針是會變化的),

    父節點.insertBefore(舊節點[舊節點開始指針].elm, 舊節點[舊節點結束指針].elm.nextSibling)

  完成操作後 新節點結束指針--舊節點開始指針++

4、新節點[新節點開始指針] 對比 舊節點[舊節點結束指針] 如果符合此種情況,則代表新節點[新節點開始指針]舊節點[舊節點結束指針] 為同一個節點,實行最小量更新,先更新節點內的屬性,然後使用insertBefore將舊節點[舊節點結束指針] 移動到舊節點[舊節點開始指針] 前,(註意:此處要移動到舊節點[舊節點開始指針] 前,而不是所有舊節點前,因為舊節點開始指針也是會發生變化的)

    父節點.insertBefore(舊節點[舊節點結束指針].elm, 舊節點[舊節點開始指針].elm)

   完成操作後,舊節點結束指針--新節點開始指針++

5、遍歷舊節點數組,生成一個以key為鍵,index為值的對象為舊節點keyIndexMap,然後查詢新節點[新節點開始指針]中的key是否在舊節點keyIndexMap中存在;

  如果不存在,則證明新節點[新節點開始指針]在舊節點列表中不存在,此時需要創建新節點[新節點開始指針]為真實dom,並將其插入至舊節點[舊節點開始指針]前(因為此時新節點[新節點開始指針]一定處於全部未處理的舊節點前)

父節點.insertBefore(創建dom(新節點[新節點開始指針]), 舊節點[舊節點開始指針].elm)

  如果存在則先需要判斷舊節點[舊節點keyIndexMap[新節點[新節點開始指針][key]]]新節點[新節點開始指針]的sel(標簽)是否相同:

    如果相同則代表為同一個標簽,則進行最小量更新,先更新節點內的屬性,然後insertBefore將舊節點[舊節點keyIndexMap[新節點[新節點開始指針][key]]]移動到舊節點[舊節點開始指針] 前,然後將舊節點[舊節點keyIndexMap[新節點[新節點開始指針][key]]]設置為undefined,代表當前節點處理過了;

    如果不同則代表不是同一個標簽,則只創建新節點[新節點開始指針]的真實dom,然後將其插入到舊節點[舊節點開始節點]

  最後新節點開始指針++

}

當以上迴圈完成後可能還會出現沒有處理到的節點,所以還需要再查找沒有處理到的節點:

  如果是新節點開始指針<=新節點結束指針,則代表新節點列表內還有沒有處理的節點,沒有處理的節點全部為新增節點,此時需要遍歷新節點[新節點開始指針](包含)至新節點[新節點結束指針](包含)之間的節點,然後將其添加至新節點[新節點結束指針+1]之前(新節點[新節點結束指針+1]可能為空,新節點[新節點結束指針+1]為空時可添加到最後)

        for (let i = 新節點開始節點; i <= 新節點結束節點; i++) {
          //insertBefore可以自動識別空值,如果是空值,則插入到最後
          父節點.insertBefore(創建dom(新節點[i]), 新節點[新節點結束節點-1]?.elm)
        }

   如果是舊節點開始指針<=舊節點結束指針,則代表舊節點內還有沒有處理的節點,沒有處理的節點全部為需要刪除節點,此時需要遍歷舊節點[舊節點開始指針](包含)至舊節點[舊節點結束指針](包含) 之間的節點,然後將其全部刪除。

        for (let i = 舊節點開始指針; i <= 舊節點結束指針; i++) {
          舊節點[i] && (父節點.removeChild(舊節點[i].elm))
        }

 

以上就是我對diff演算法的理解,下麵貼上代碼(閹割版,部分情況沒有考慮,旨在學習diff演算法,可能會有bug):

//updateChildren文件
import { sameVnode } from './is'
import patchVnode from './patchVnode'
import createElement from './createElement'

export default functionupdateChildren  (parentElm, oldCh, newCh) {
  console.log('updateChildren')
  console.log(parentElm, oldCh, newCh)

  //舊前
  let oldStartIdx = 0
  //新前
  let newStartIdx = 0
  //舊後
  let oldEndIdx = oldCh.length - 1
  //新後
  let newEndIdx = newCh.length - 1
  //舊節點
  let oldStartVnode = oldCh[0]
  //舊後節點
  let oldEndVnode = oldCh[oldEndIdx]
  //新節點
  let newStartVnode = newCh[0]
  //新後節點
  let newEndVnode = newCh[newEndIdx]

  let keyMap = {}
//  開始迴圈
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // debugger
    console.log('while')
    if (oldStartVnode === undefined || oldCh[oldStartIdx] === undefined) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (oldEndVnode === undefined || oldCh[oldEndIdx] === undefined) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newStartVnode === undefined || newCh[newStartIdx] === undefined) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newEndVnode === undefined || newCh[newEndIdx] === undefined) {
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      //新前和舊前是同一個節點
      console.log('新前和舊前是同一個節點')
      patchVnode(oldStartVnode, newStartVnode)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {//舊後和新後是同一個節點
      console.log('舊後和新後是同一個節點')
      patchVnode(oldEndVnode, newEndVnode)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) {//新後和舊前是同一個節點
      console.log('新後和舊前是同一個節點')
      patchVnode(oldStartVnode, newEndVnode)
      //當新後節點是舊前節點時,此時需要移動節點,移動舊前節點到舊後的後面
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) {//舊後和新前是同一個節點
      console.log('舊後和新前是同一個節點')
      //  當舊後和新前是同一個節點時,此時需要移動舊後節點到舊前節點的前面
      patchVnode(oldEndVnode, newStartVnode)
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      //前四種都沒有命中
      if (Object.keys(keyMap).length === 0) {
        keyMap = {}
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
          const key = oldCh[i].key
          if (key) {
            keyMap[key] = i
          }
        }
      }
      console.log(keyMap)
      //尋找當前節點在keyMap中的位置
      const idxInOld = keyMap[newStartVnode.key]
      console.log(idxInOld)
      if (!idxInOld) {
        //新節點不在舊節點中
        parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
      } else {
        //  新節點在舊節點中,需要移動
        const elmToMove = oldCh[idxInOld]
        if (elmToMove.sel !== newStartVnode.sel) {
          parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
        } else {
          patchVnode(elmToMove, newStartVnode)
          //  把這項設置為undefined,表示已經移動過了
          oldCh[idxInOld] = undefined
          parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
        }
      }
      //指針向後移動
      newStartVnode = newCh[++newStartIdx]
    }
  }
// 繼續查詢是否有剩餘節點
  if (newStartIdx <= newEndIdx) {
    console.log('新節點還有剩餘節點沒有處理', newStartIdx, newEndIdx)
    const before = newCh[newEndIdx + 1]?.elm
    console.log(before)
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      //insertBefore可以自動識別undefined,如果是undefined,則插入到最後
      parentElm.insertBefore(createElement(newCh[i]), before)
    }
  } else if (oldStartIdx <= oldEndIdx) {
    console.log('舊節點還有剩餘節點沒有處理', oldStartIdx, oldEndIdx)
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      oldCh[i] && (parentElm.removeChild(oldCh[i].elm))
    }
  }
}

let arr = [1, 1, 2, 35, 9, 2, 9]
arr.reduce((p, n) => {
  return p ^ n
}, 0)

 

//is文件
export function sameVnode (vnode1, vnode2) {
  return vnode1.sel === vnode2.sel && vnode1.key === vnode2.key;
}
//createElement文件
//真正創建dom
export default function createElement (vnode) {
  let domNode = document.createElement(vnode.sel);
  if (
    vnode.text !== "" &&
    (vnode.children === undefined || vnode.children.length === 0)
  ) {
    domNode.innerText = vnode.text;
    //  補充elm

  } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
    for (let i = 0; i < vnode.children.length; i++) {
      domNode.appendChild(createElement(vnode.children[i]));
    }
  }
  vnode.elm = domNode;
  return vnode.elm
}
//patchVnode文件
import createElement from './createElement'
import updateChildren from './updateChildren'

export default function patchVnode (oldVnode, newVnode) {
  // console.log('patchVnode')
  if (oldVnode === newVnode) return
  if (newVnode.text && (!newVnode.children || newVnode.children.length === 0)) {//判斷newVnode的text是否為空,且不等於oldVnode的text,如果滿足以上條件,則更新text
    oldVnode.text !== newVnode.text && (oldVnode.elm.innerText = newVnode.text);
  } else {//newVnode的text為空,則判斷newVnode的children是否為空,如果不為空,則更新children
    //  新節點沒有text屬性
    if (oldVnode.children && oldVnode.children.length > 0) {
      //  老節點有children,新節點也有children
      updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
    } else {
      //  老的沒有children,新的有children
      oldVnode.elm.innerHTML = '';
      for (let i = 0; i < newVnode.children.length; i++) {
        let dom = createElement(newVnode.children[i])
        oldVnode.elm.appendChild(dom)
      }
    }
  }
}
//patch文件
import vnode from "./vnode";
import createElement from "./createElement";
import patchVnode from './patchVnode'
import { sameVnode } from './is'

export default function (oldVnode, newVnode) {
  // console.log(oldVnode, newVnode)
  //判斷傳入的第一個參數,是dom節點還是vnode
  if (oldVnode.sel === "" || oldVnode.sel === undefined) {
    //傳入的如果是dom節點需要包裝為虛擬節點
    oldVnode = vnode(
      oldVnode.tagName.toLowerCase(),
      {},
      [],
      undefined,
      oldVnode,
    );
  }
  //  判斷oldVnode和newVnode是否是同一個節點
  if (sameVnode(oldVnode, newVnode)) {
    // console.log("是同一個節點");
    patchVnode(oldVnode, newVnode);
  } else {
    // console.log("不是同一個節點");
    let newVnodeElm = createElement(newVnode, oldVnode.elm);
    if (oldVnode.elm.parentNode && newVnodeElm) {
      oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
    }
    oldVnode.elm.parentNode.removeChild(oldVnode.elm);
  }
}
//VNode文件
export default function (sel, data, children, text, elm) {
  const key = data.key
  return {
    sel,
    data,
    children,
    text,
    elm,
    key,
  }
}
//h文件

import vnode from './vnode.js'

//h('div',{},'文字')
//h('div',{},'[]')
//h('div',{},h())
export default function (sel, data, c) {
//檢查參數個數
  if (arguments.length !== 3) {
    throw new Error('h()參數個數不正確')

  }
//    檢查C類型
  if (typeof c === 'string' || typeof c === 'number') {
    return vnode(sel, data, undefined, c, undefined)
  } else if (Array.isArray(c)) {
    let children = []
    for (let i = 0; i < c.length; i++) {
      if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
        throw new Error('傳入的數組參數中有項不是h函數')
      }
      children.push(c[i])
    }
    //  迴圈結束,children收集完畢
    return vnode(sel, data, children, undefined, undefined)
  } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
    let children = [c]
    return vnode(sel, data, children, undefined, undefined)
  } else {
    throw new Error('h()參數類型不正確')
  }
}

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 鏡像下載、功能變數名稱解析、時間同步請點擊 阿裡雲開源鏡像站 ifconfig 命令可以用於查看、配置、啟用或禁用指定的網路介面,還可以用來配置網卡的IP地址、掩碼、廣播地址、網關等,功能很豐富 功能雖然豐富,但是如果你沒有安裝呢? 嘿嘿嘿 如果沒有安裝ifconfig管理命令的話,直接運行ifconfig ...
  • 好消息!6月13日,騰訊雲資料庫三篇論文再次入選資料庫行業頂會SIGMOD,被SIGMOD 2022 Research Full Paper(研究類長文)收錄。 本次被收錄的研究成果中,新型數據結構設計、AI智能調參優化等均屬於業界首次提出。騰訊雲資料庫多次入選SIGMOD,表明騰訊雲資料庫在存儲、 ...
  • 本文將會和大家一起學習 SQL 的搜索條件中不可或缺的工具——謂詞(predicate)。SQL 謂詞就是返回值為 TRUE、FALSE 或 UNKNOWN 的表達式。 謂詞用於 WHERE 子句 和 HAVING 子句 的搜索條件中,還用於 FROM 子句的聯接條件以及需要布爾值的其他構造中。 本 ...
  • Apache ShardingSphere 助力噹噹 3.5 億用戶量級顧客系統重構,由 PHP+SQL Server 技術棧無縫轉型為 Java+ShardingSphere+MySQL,性能、可用性及維護性均得到顯著提升,是 ShardingSphere 異構遷移最佳實踐。 ...
  • 導讀: 作為一種基礎的數據結構,圖數據的應用場景無處不在,如社交、風控、搜廣推、生物信息學中的蛋白質分析等。如何高效地對海量的圖數據進行存儲、查詢、計算及分析,是當前業界熱門的方向。本文將介紹位元組跳動自研的圖資料庫ByteGraph及其在位元組內部的應用和挑戰。 本文將圍繞以下五點展開: 瞭解圖資料庫 ...
  • 新 能 力 3D Engine 3D Engine提供高性能、高畫質、高擴展性的實時3D引擎,並提供便捷高效的可視化開發工具。開發者可基於華為的3D Studio開發工具,通過圖形和渲染、動畫、UI等功能模塊,打造高品質的3D應用。同時引擎也開放三方插件的能力,致力協同全球圖形領域的伙伴研究創新技術 ...
  • 前言 ​ 採集應用程式崩潰信息,主要分為以下兩種場景: ​ NSException 異常 ​ Unix 信號異常 一、NSException 異常 ​ NSException 異常是 Objective-C 代碼拋出的異常。在 iOS 應用程式中,最常見就是通過 @throw 拋出的異常。比如,常見 ...
  • 當用戶有跨語種交流或內容翻譯的需求時,應用需要能自動檢測文本的語種再進行翻譯。 HMS Core機器學習服務的語種檢測服務提供線上語種檢測和離線語種檢測,既支持檢測單語種文本,也支持檢測混合語種文本,涵蓋南非荷蘭語、阿拉伯語等百種語言。接入語種檢測服務,App可以輕鬆實現翻譯語種檢測、網頁語種檢測, ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...