虛擬DOM與diff演算法

豆豆打醜小鴨發表於2022-04-18

?虛擬DOM與diff演算法

在vue、react等技術出現之前,每次修改DOM都需要通過遍歷查詢DOM樹的方式,找到需要更新的DOM,然後修改樣式或結構,資源損耗十分嚴重。而對於虛擬DOM來說,每次DOM的更改就變成了JS物件的屬性的更改,能方便的查詢JS物件的屬性變化,要比查詢DOM樹的效能開銷小,所以能夠改善瀏覽器的效能問題。

對於vue,從vue2就開始支援虛擬DOM。

diff演算法:簡單來說就是找出兩個物件的差異,只對有差異的一小塊DOM進行更新,而不是整個DOM,從而達到最小量更新的效果

虛擬DOM:內部會把程式碼段解析成一個物件(真實DOM是通過模板編譯變成虛擬DOM的)

用JS物件描述DOM的層次結構,DOM中的一切屬性都在虛擬DOM中有對應的屬性

snabbdom環境搭建

是虛擬DOM庫,diff演算法的鼻祖,vue原始碼借鑑了snabbdom

官方git:https://github.com/snabbdom/snabbdom

git上的snabbdom原始碼是用TypeScript寫的,如果要直接使用編譯出來的Javascript版的snabbdom庫,可以從npm上下載npm i -D snabbdom

snabbdom庫是DOM庫,不能在node js環境執行,需要搭建webpackwebpack-dev-server開發環境。需要注意的是必須安裝webpack@5

npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

配置webpack.config.js檔案,參考官網進行配置:https://webpack.docschina.org/

const path = require('path');

module.exports = {
    // 入口
    entry: './src/index.js',

    // 出口
    output: {
        // 虛擬打包路徑,資料夾不會真正生成,而是在8080埠虛擬生成
        publicPath: 'xuni',
        // 打包出來的檔名
        filename: "bundle.js",
    },
    // 配置webpack-dev-server
    devServer: {
        // 埠號
        port: 8082,
        // 靜態根目錄
        contentBase: 'www',
    },
}

將專案根目錄下的package.json檔案中修改script的配置,就可以通過npm run dev啟動專案

配置完之後將官網的Example進行測試,由於示例要獲取id=container的節點,所以我們需要提前準備一個id為container的div。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="container"></div>
    <script src="/xuni/bundle.js"></script>
</body>
</html>

需要注意的點

頁面出現如下狀態即表示配置完成

虛擬DOM和h函式

diff是發生在虛擬DOM上的

新虛擬DOM和老虛擬DOM進行diff(精細化比較),算出應該如何最小量更新,最後反映到真正的DOM上。

h函式用來產生虛擬節點(vnode)

h('a',{ props: { href: 'https://www.baidu.com' } }, '百度');

會得到這樣的虛擬節點

{ "sel": "a", "data": { props: { href: 'https://www.baidu.com' } }, "text": "百度" }

它表示的真正的DOM節點

<a href="https://www.baidu.com">百度</a>

如果需要讓虛擬節點上樹,需要藉助patch函式

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

// 建立patch函式
var patch = init([classModule, propsModule, styleModule, eventListenersModule]);

//建立虛擬節點
var vNode1 = h('a',{ props: { href: 'https://www.baidu.com' } }, '百度');

// 讓虛擬節點上樹
const container = document.getElementById('container');
patch(container,vNode1);

h函式可以巢狀使用,從而得到虛擬DOM樹

h('ul',{},[
	h('li',{},'可樂');
	h('li',{},'雪碧');
	h('li',{},'椰汁');
])

diff演算法

實現最小量更新。需要keykey是這個節點的唯一標識,告訴diff演算法,在更改前後它們是同一個DOM節點。

只有同一個虛擬節點,才進行精細化比較。否則就是暴力刪除舊的、插入新的。

同一個虛擬節點:選擇器相同且key相同

只進行同層比較,不會進行跨層比較。

比如下面這兩個DOM節點,雖然是同一片虛擬節點,但是跨層了,依舊會暴力刪除舊的、插入新的。

const vnode1 = h('div',{},[
	h('p',{ key: 'A' }, 'A'),
	h('p',{ key: 'B' }, 'B'),
	h('p',{ key: 'C' }, 'C'),
	h('p',{ key: 'D' }, 'D'),
]);

const vnode2 = h('div',{},h('section',{},[
	h('p',{ key: 'A' }, 'A'),
	h('p',{ key: 'B' }, 'B'),
	h('p',{ key: 'C' }, 'C'),
	h('p',{ key: 'D' }, 'D'),
]))

分析原始碼也可以驗證以上所述

首先會去判斷是不是虛擬節點,不是的話會先去把它包裝成虛擬節點

然後判斷是不是同一個節點,不是的話插入新的、刪除舊的,是的話精細化比較

執行的流程圖

patch函式

首先判斷oldVnode是否是虛擬節點,如果是DOM節點的話先把oldVnode包裝成虛擬節點

然後判斷新節點和舊節點是否是同一個節點,判斷key的值是否相同,標籤名是否相同,是否都定義了data(data包含一些具體的資訊,onclick、style等)

如果不是同一節點,新節點直接替換老節點,刪除舊的、插入新的。在原始碼中,建立所有的子節點時,需要遞迴。

如果新舊節點是同一個節點時,會執行patchVnode進行子節點比較

patchVnode函式

首先會找到對應的真實DOM

const elm = (vnode.elm = oldVnode.elm)!;

如果新老節點相同,直接返回 if(oldVnode === vnode) return

  • 如果vnode沒有文字節點(isUndef(vnode.text))

    • 都有children且不相同

      使用updateChildren對比children(diff演算法的核心

    • 只有vnode有children

      則oldVnode是一個空標籤或者是文字節點,如果是文字節點就清空文字節點,然後將vnode的children建立為真實DOM後插入到空標籤內。

    • 只有oldVnode有children

      vnode沒有的東西,在oldVnode內需要刪除掉removeVnodes(oldVnode有且vnode沒有的,都清空或移除)

    • 只有oldVnode有文字

      清空文字

  • 如果vnode有text屬性且不同

    vnode為標準,無論oldVnode是什麼型別節點,直接設定為vnode內的文字

updateChildren函式

updateChildren方法的核心:

  • 提取出新老節點的子節點:新節點子節點ch和老節點子節點oldCh

  • ch和oldCh分別設定StartIdx(頭指標)和EndIdx(尾指標)變數,相互比較。此時就有四個變數:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx(這裡採用雙指標的思想)

    有四種方式來比較:

    1. oldStartIdx和newStartIdx比較

      如果匹配,DOM不用修改,將oldStartIdx和newStartIdx的下標往後移一位

    2. oldEndIdx和newEndIdx比較

      如果匹配,DOM不用修改,將oldEndIdx和newEndIdx的下標往前移一位

    3. oldStartIdx和newEndIdx比較

      如果匹配,DOM不用修改,將oldStartIdx對應的真實DOM插入到最後一位,oldStartIdx的下標後移一位,newEndIdx的下標前移一位。

    4. oldEndIdx和newStartIdx比較

      如果匹配,DOM不用修改,將oldEndIdx對應的真實DOM插入到oldEndIdx對應真實Dom的前面,oldEndIdx的下標前移一位,newStartIdx的下標後移一位。

如果4種方式都沒有匹配成功,如果設定了key就通過key進行比較,在比較過程中startIdx++,endIdx--,一旦StartIdx > EndIdx表明ch或者oldCh至少有一個已經遍歷完成,此時就會結束比較

處理結束後,如果新節點有剩餘,就新增;如果舊節點有剩餘,就刪除

v-for中key作用與原理

key是虛擬DOM物件的標識,當資料發生變化時,Vue會根據【新資料】產生【新的虛擬DOM】,隨後Vue進行【新虛擬DOM】與【舊虛擬DOM】的差異比較,比較規則如下:

(1)舊虛擬DOM中找到了與新虛擬DOM相同的key

​ ①若虛擬DOM中內容沒變,直接使用之前的真實DOM

​ ②若虛擬DOM中內容變了,則生成新的真實DOM,隨後替換掉頁面中之前的真實DOM

(2)舊虛擬DOM中未找到與新虛擬DOM相同的key

​ 建立新的真實DOM,隨後渲染到頁面

所以,如果用index作為key可能會引發的問題:

(1)若對資料進行:逆序新增、逆序刪除等破環順序操作,會產生沒有必要的真實DOM更新==>介面效果沒問題,但效率低

(2)如果結構中還包含輸入類的DOM,會產生錯誤DOM錯誤==>介面有問題

實際開發中如何選擇key

  1. 最好使用每條資料的唯一標識作為key,比如id、手機號、身份證號、學號等唯一值
  2. 如果不存在對資料的逆序新增、逆序刪除等破壞順序操作,僅用於渲染列表用於展示,使用index作為key是沒有問題的

相關文章