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
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...