91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Vue中的虛擬DOM和Diff算法實例分析

發布時間:2022-03-09 09:04:57 來源:億速云 閱讀:150 作者:iii 欄目:編程語言

這篇文章主要介紹了Vue中的虛擬DOM和Diff算法實例分析的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Vue中的虛擬DOM和Diff算法實例分析文章都會有所收獲,下面我們一起來看看吧。

Vue中的虛擬DOM和Diff算法實例分析

簡單介紹一下 虛擬DOM 和 diff算法

先用一個簡單的例子來說一下 虛擬DOMdiff算法:比如有一個戶型圖,現在我們需要對這個戶型圖進行下面的改造,

Vue中的虛擬DOM和Diff算法實例分析

其實,這個就是相當于一個進行找茬的游戲,讓我們找出與原來的不同之處。下面,我已經將不同之處圈了出來,

Vue中的虛擬DOM和Diff算法實例分析

現在,我們已經知道了要進行哪些改造了,但是,我們該如何進行改造呢?最笨的方法就是全部拆了再重新建一次,但是,在我們實際中肯定不會進行拆除再新建,這樣效率太低了,而且代價太昂貴。確實是完成了改造,但是,這不是一個最小量的更新,所以我們想要的是 diff,

Vue中的虛擬DOM和Diff算法實例分析

那么diff是什么呢?其實,diff 在我們計算機中就是代表著最小量更新的一個算法,會進行精細化對比,以最小量去更新。這樣你就會發現,它的代價比較小,也不會昂貴,也會比較優化,所以對應在我們 Vue底層中是非常關鍵的。
好了,現在回歸到我們的Vue中,上面的戶型圖中就相當于vue中的 DOM節點,我們需要對這些節點進行改造(增刪調),然后以最小量去更新DOM,這樣就會避免我們性能上面的開銷。

// 原先DOM
<div class="box">
        <h3>標題</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
    </div>
// 修改后的DOM
<div class="box">
        <h3>標題</h3>
        <span>青峰</span>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
        </ul>
    </div>

在這里,我們就可以利用 diff算法進行精細化對比,實現最小量更新。 上面我們了解了什么是diff,下面再來簡單了解一下什么是虛擬DOM,

<div class="box">
        <h3>標題</h3>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
    </div>
{
    sel: "div",
    elm: undefined, // 表示虛擬節點還沒有上樹
    key: undefined, // 唯一標識
    data: {
        class: { "box" : true}
    },
    children: [
        {
            sel: "h3",
            data: {},
            text: "標題"
        },
        {
            sel: "ul",
            data: {},
            children: [
                { sel: li, data: {}, text: "1"},
                { sel: li, data: {}, text: "2"},
                { sel: li, data: {}, text: "3"}
            ]
        }
    ]
}

通過觀察可以發現,虛擬DOM 是一個 JavsScript對象,里面包含 sel選擇器data數據text文本內容children子標簽等等,一層嵌套一層。這樣就表達了一個 虛擬DOM結構,處理 虛擬DOM 的方式總比處理 真實的DOM 要簡單并且高效,所以 diff算法 是發生在 虛擬DOM 上的。
注意:diff算法 是發生在 虛擬DOM 上的。

為什么需要 Virtual DOM(虛擬DOM)

  • 首先,我們都知道,在前端性能優化的一個秘訣就是盡可能的減少DOM的操作,不僅僅是DOM相對較慢,更是因為變動DOM會造成瀏覽器的回流和重繪,這些都會降低性能,因此,我們需要虛擬DOM,在patch(比較新舊虛擬DOM更新去更新視圖)過程中盡可能的一次性將差異更新到DOM中,這樣就保證了DOM不會出現了性能很差的情況。

  • 其次,使用 虛擬DOM 改變了當前的狀態不需要立即去更新DOM,而是對更新的內容進行更新,對于沒有改變的內容不做任何處理,通過前后兩次差異進行比較

  • 最后,也是Virtual DOM 最初的目的,就是更好的跨平臺,比如node.js就沒有DOM,如果想實現 SSR(服務端渲染),那么一個方式就是借助Virtual DOM,因為 Virtual DOM本身是 JavaScript對象。

h函數(創建虛擬DOM)

作用:h函數 主要用來產生 虛擬節點(vnode)
第一個參數:標簽名字、組件的選項對象、函數
第二個參數:標簽對應的屬性 (可選)
第三個參數:子級虛擬節點,字符串或者是數組形式

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

上面的h函數對應的虛擬節點為:

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

真正的DOM節點為:

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

我們還可以嵌套的使用h函數,比如:

h('ul', {}, [
    h('li', {}, '1'),
    h('li', {}, '2'),
    h('li', {}, '3'),
])

嵌套使用h函數,就會生成一個虛擬DOM樹。

{
            sel: "ul",
            elm: undefined,
            key: undefined,
            data: {},
            children: [
                { sel: li, elm: undefined, key: undefined, data: {}, text: "1"},
                { sel: li, elm: undefined, key: undefined, data: {}, text: "2"},
                { sel: li, elm: undefined, key: undefined, data: {}, text: "3"}
            ]
        }

好了,上面我們已經知道了h函數是怎么使用的了,下面我們手寫一個閹割版的h函數。

手寫 h函數

我們手寫的這個函數只考慮三種情況(帶三個參數),分別如下:

情況①:h('div', {}, '文字')
情況②:h('div', {}, [])
情況③:h('div', {}, h())

在手寫h函數之前,我們需要聲明一個函數,用來創建虛擬節點

// vnode.js 返回虛擬節點
export default function(sel, data, children, text, elm) {
    // sel 選擇器 、data 屬性、children 子節點、text 文本內容、elm 虛擬節點綁定的真實 DOM 節點
    const key = data.key
    return {
        sel,
        data,
        children,
        text,
        elm,
        key
    }
}

聲明好vnode函數之后,我們正式來手寫h函數,思路如下:

  • 判斷第三個參數是否是字符串或者是數字。如果是字符串或數字,直接返回 vnode

  • 判斷第三個參數是否是一個數組。聲明一個數組,用來存儲子節點,需要遍歷數組,這里需要判斷每一項是否是一個對象(因為 vnode 返回一個對象并且一定會有sel屬性)但是不需要執行每一項,因為在數組里面已經執行了h函數。其實,并不是函數遞歸進行調用(自己調自己),而是一層一層的嵌套

  • 判斷都三個參數是否是一個對象。直接將這個對象賦值給 children,并會返回 vnode

// h.js h函數
import vnode from "./vnode";
// 情況①:h('div', {}, '文字')
// 情況②:h('div', {}, [])
// 情況③:h('div', {}, h())
export default function (sel, data, c) {
    // 判斷是否傳入三個參數
    if (arguments.length !== 3) 
        throw Error('傳入的參數必須是三個參數') 
    // 判斷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++) {
            // 子元素必須是h函數
            if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel')))
                throw Error('數組中有一項不是h函數')
            // 收集子節點 不需要執行 因為數組里面已經執行h函數來
            children.push(c[i])
        }
        return vnode(sel, data, children, undefined, undefined)
    } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
        // 直接將子節點放到children中
        let children = [c]
        return vnode(sel, data, children, undefined, undefined)
    } else {
        throw Error('傳入的參數格式不對')
    }
}

通過上面的代碼,我們已經實現了一個簡單 h函數 的基本功能。

感受 diff 算法

在講解 diff算法 之前,我們先來感受一下 diff算法 的強大之處。先利用 snabbdom 簡單來舉一個例子。

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";
//創建出patch函數
const patch = init([
    classModule, 
    propsModule, 
    styleModule, 
    eventListenersModule, 
]);
//讓虛擬節點上樹
const container = document.getElementById("container");
const btn = document.getElementById("btn");
//創建虛擬節點
const myVnode1 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D')
])
patch(container, myVnode1)
const myVnode2 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D'),
    h('li', {}, 'E'),
])
btn.addEventListener('click', () => {
    // 上樹
    patch(myVnode1,myVnode2)
})

Vue中的虛擬DOM和Diff算法實例分析

當我們點接改變DOM的時候,發現會新增一個 li標簽 內容為 E,單單的點擊事件,我們很難看出,是將 舊的虛擬DOM 全部替換掉 新的虛擬DOM,然后再渲染成 真實DOM,還是直接在 舊的虛擬DOM 上直接在后面添加一個節點,所以,在這里我們可以巧妙的打開測試工具,直接將標簽內容進行修改,如果點擊之后是全部拆除,那么標簽的內容就會發生改變,若內容沒有發生改變,則是將最后添加的。

Vue中的虛擬DOM和Diff算法實例分析

點擊改變 DOM 結構:

Vue中的虛擬DOM和Diff算法實例分析

果然,之前修改的內容沒有發生變化,這一點,就可以驗證了是進行了 diff算法精細化的比較,以最小量進行更新。 那么問題就來了,如果我在前面添加一個節點呢?是不是也是像在最后添加一樣,直接在前面添加一個節點。我們不妨也來試一試看看效果:

...
const container = document.getElementById("container");
const btn = document.getElementById("btn");
//創建虛擬節點
const myVnode1 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D')
])
patch(container, myVnode1)
const myVnode2 = h('ul', {}, [
    h('li', {}, 'E'),  // 將E移至前面
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D'),
])
btn.addEventListener('click', () => {
    // 上樹
    patch(myVnode1,myVnode2)
})

Vue中的虛擬DOM和Diff算法實例分析

點擊改變 DOM 結構

Vue中的虛擬DOM和Diff算法實例分析

哦豁!!跟我們想的不一樣,你會發現,里面的文本內容全部發生了變化,也就是說將之前的 DOM 全部拆除,然后將新的重新上樹。這時候,你是不是在懷疑其實 diff算法 沒有那么強大,但是你這樣想就大錯特錯了,回想一下在學習 Vue 的過程中,在遍歷DOM節點 的時候,是不是特別的強調要寫上key唯一標識符,此時,key在這里就發揮了它的作用。 我們帶上key再來看一下效果:

...
const myVnode1 = h('ul', {}, [
    h('li', { key: "A" }, 'A'),
    h('li', { key: "B" }, 'B'),
    h('li', { key: "C" }, 'C'),
    h('li', { key: "D" }, 'D')
])
patch(container, myVnode1)
const myVnode2 = h('ul', {}, [
    h('li', { key: "E" }, 'E'),
    h('li', { key: "A" }, 'A'),
    h('li', { key: "B" }, 'B'),
    h('li', { key: "C" }, 'C'),
    h('li', { key: "D" }, 'D'),
])
...

Vue中的虛擬DOM和Diff算法實例分析

點擊改變 DOM 結構

Vue中的虛擬DOM和Diff算法實例分析

看到上面的結果,此時此刻,你是不是恍然大悟了,頓時知道了key在循環當中有什么作用了吧。我們可以推出的結論一就是: key是當前節點的唯一標識,告訴 diff算法,在更改前后它們是同一個 DOM節點

當我們修改父節點,此時新舊虛擬DOM的父節點不是同一個節點,繼續來觀察一下 diff算法是如何分析的

const myVnode1 = h('ul', {}, [
    h('li', { key: "A" }, 'A'),
    h('li', { key: "B" }, 'B'),
    h('li', { key: "C" }, 'C'),
    h('li', { key: "D" }, 'D')
])
patch(container, myVnode1)
const myVnode2 = h('ol', {}, [
    h('li', { key: "A" }, 'A'),
    h('li', { key: "B" }, 'B'),
    h('li', { key: "C" }, 'C'),
    h('li', { key: "D" }, 'D'),
])

Vue中的虛擬DOM和Diff算法實例分析

點接改變 DOM結構

Vue中的虛擬DOM和Diff算法實例分析

你會發現,這里將舊節點進行了全部的拆除,然后重新將新節點上樹。我們可以推出的結論二就是:
只有是同一個虛擬節點diff算法 才進行精細化比較,否則就是暴力刪除舊的、插入新的。判斷同一個虛擬節點的依據:選擇器(sel)相同且key相同。

那么如果是同一個虛擬節點,但是子節點里面不是同一層在比較的呢?

const myVnode1 = h('div', {}, [
    h('li', { key: "A" }, 'A'),
    h('li', { key: "B" }, 'B'),
    h('li', { key: "C" }, 'C'),
    h('li', { key: "D" }, 'D')
])
patch(container, myVnode1)
const myVnode2 = h('div', {}, h('section', {},
    [
        h('li', { key: "A" }, 'A'),
        h('li', { key: "B" }, 'B'),
        h('li', { key: "C" }, 'C'),
        h('li', { key: "D" }, 'D'),
    ]
))

Vue中的虛擬DOM和Diff算法實例分析

點擊改變DOM結構

Vue中的虛擬DOM和Diff算法實例分析

你會發現,此時DOM結構同多了一層 section標簽 包裹著,然后,文本的內容也發生了變化,所以我們可以推出結論三
diff算法 只進行同層比較,不會進行跨層比較。即使是同一片虛擬節點,但是跨層了,不進行精細化比較,而是暴力刪除舊的、然后插入新的。

綜上,我們得出diff算法的三個結論:

  • key 是當前節點的唯一標識,告訴 diff算法,在更改前后它們是同一個 DOM節點

  • 只有是同一個虛擬節點diff算法 才進行精細化比較,否則就是暴力刪除舊的、插入新的。判斷同一個虛擬節點的依據:選擇器(sel)相同 key相同

  • diff算法 只進行同層比較,不會進行跨層比較。即使是同一片虛擬節點,但是跨層了,不進行精細化比較,而是暴力刪除舊的、然后插入新的。

看到這里,相信你已經對 diff算法 已經有了很大的收獲了。

patch 函數

patch函數 的主要作用就是:判斷是否是同一個節點類型,是就在進行精細化對比,不是就進行暴力刪除,插入新的
我們在可以簡單的畫出patch函數現在的主要流程圖如下:

Vue中的虛擬DOM和Diff算法實例分析

// patch.js    patch函數
import vnode from "./vnode";
import sameVnode from "./sameVnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    // 判斷oldVnode是否是虛擬節點
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // console.log('不是虛擬節點');
        // 創建虛擬DOM
        oldVnode = emptyNodeAt(oldVnode)
    }
    // 判斷是否是同一個節點
    if (sameNode(oldVnode, newVnode)) {
        console.log('是同一個節點');
    } else {
        // 暴力刪除舊節點,插入新的節點
        // 傳入兩個參數,創建的節點 插入到指定標桿的位置
        createElement(newVnode, oldVnode.elm)
    }
}
// 創建虛擬DOM
function emptyNodeAt (elm) {
    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)
}

在進行上DOM上樹之前,我們需要了解一下DOM中的insertBefore()方法、appendChild()方法,因為,只有你真正的知道它們兩者的用法,才會讓你在下面手寫上樹的時候更加的清晰。

appendChild()方法

appendChild() 方法:可以向節點的子節點列表的末尾添加新的子節點。比如:appendChild(newchild)。
注意: appendChild()方法是在父節點中的子節點的末尾添加新的節點。(相對于父節點來說)。

<body>
    <div class="box">
        <span>青峰</span>
        <ul>
            <li>1</li>
            <li>2</li>
        </ul>
    </div>
    <script>
        const box = document.querySelector('.box')
        const appendDom = document.createElement('div')
        appendDom.style.backgroundColor = 'blue'
        appendDom.style.height = 100 + 'px'
        appendDom.style.width = 100 + 'px'
        // 在box里面的末尾追加一個div
        box.appendChild(appendDom)
    </script>
</body>

Vue中的虛擬DOM和Diff算法實例分析

你會發現,創建的div是嵌套在box里面的,div 屬于 box 的子節點,box 是 div 的子節點。

insertBefore()方法

insertBefore() 方法:可在已有的字節點前中插入一個新的子節點。比如:insertBefore(newchild,rechild)。
注意: insertBefore()方法是在已有的節點前添加新的節點。(相對于子節點來說的)。

<body>
    <div class="box">
        <span>青峰</span>
        <ul>
            <li>1</li>
            <li>2</li>
        </ul>
    </div>
    <script>
        const box = document.querySelector('.box')
        const insertDom = document.createElement('p')
        insertDom.innerText = '我是insertDOM'
        // 在body中 box前面添加新的節點
        box.parentNode.insertBefore(insertDom, box)
    </script>
</body>

Vue中的虛擬DOM和Diff算法實例分析

我們發現,box 和 div 是同一層的,屬于兄弟節點。

處理不同節點

sameVnode 函數

作用:比較兩個節點是否是同一個節點

// sameVnode.js
export default function sameVnode(oldVnode, newVnode) {
    return (oldVnode.data ? oldVnode.data.key : undefined) === (newVnode.data ? newVnode.data.key : undefined) && oldVnode.sel == newVnode.sel
}

手寫第一次上樹

理解了上面的 appendChild()方法insertBefore()方法之后,我們正式開始讓 真實DOM 上樹,渲染頁面。

// patch.js    patch函數
import vnode from "./vnode";
import sameVnode from "./sameVnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    // 判斷oldVnode是否是虛擬節點
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // console.log('不是虛擬節點');
        // 創建虛擬DOM
        oldVnode = emptyNodeAt(oldVnode)
    }
    // 判斷是否是同一個節點
    if (sameNode(oldVnode, newVnode)) {
        console.log('是同一個節點');
    } else {
        // 暴力刪除舊節點,插入新的節點
        // 傳入兩個參數,創建的節點 插入到指定標桿的位置
        createElement(newVnode, oldVnode.elm)
    }
}
// 創建虛擬DOM
function emptyNodeAt (elm) {
    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)
}

上面我們已經明確的知道,patch的作用就是判斷是否是同一個節點,所以,我們需要聲明一個createElement函數,用來創建真實DOM。

createElement 函數

createElement主要用來 創建子節點的真實DOM。

// createElement.js
export default function createElement(vnode,pivot) {
    // 創建上樹的節點
    let domNode = document.createElement(vnode.sel)
    // 判斷有文本內容還是子節點
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 文本內容 直接賦值
        domNode.innerText = vnode.text
        // 上樹 往body上添加節點
        // insertBefore() 方法:可在已有的字節點前中插入一個新的子節點。相對于子節點來說的
        pivot.parentNode.insertBefore(domNode, pivot)
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 有子節點
    }
}
// index.js
import patch from "./mysnabbdom/patch";
import h from './mysnabbdom/h'
const container = document.getElementById("container");
//創建虛擬節點
const myVnode1 = h('h2', {}, '文字')
patch(container, myVnode1)

Vue中的虛擬DOM和Diff算法實例分析

我們已經將創建的真實DOM成功的渲染到頁面上去了,但是這只是實現了最簡單的一種情況,那就是 h函數 第三個參數是字符串的情況,所以,當第三個參數是一個數組的時候,是無法進行上樹的,下面我們需要將 createElement函數 再進一步的優化,實現遞歸上樹。

遞歸創建子節點

我們發現,在第一次上樹的時候,createElement函數 有兩個參數,分別是:newVnode (新的虛擬DOM),標桿(用來上樹插入到某個節點的位置),在createElement內部 我們是使用 insertBefore()方法 進行上樹的,使用這個方法我們需要知道已有的節點是哪一個,當然,當有 text(第三個參數是字符串或數字)的時候,我們是可以找到要插入的位置的,但是當有 children(子節點)的時候,我們是無法確定標桿的位置的,所以,我們要將上樹的工作放到 patch函數 中,即 createElement函數只負責創建節點

// index.js
import patch from "./mysnabbdom/patch";
import h from './mysnabbdom/h'
const container = document.getElementById("container");
//創建虛擬節點
const myVnode1 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D')
])
patch(container, myVnode1)
// patch.js
import vnode from "./vnode";
import sameVnode from "./sameVnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    // 判斷oldVnode是否是虛擬節點
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // console.log('不是虛擬節點');
        // 創建虛擬DOM
        oldVnode = emptyNodeAt(oldVnode)
    }
    // 判斷是否是同一個節點
    if (sameNode(oldVnode, newVnode)) {
        console.log('是同一個節點');
    } else {
        // 暴力刪除舊節點,插入新的節點
        // 傳入參數為創建的虛擬DOM節點  返回以一個真實DOM
        let newVnodeElm = createElement(newVnode)
        console.log(newVnodeElm);
        // oldVnode.elm.parentNode 為body 在body中 在舊節點的前面添加新的節點
        if (oldVnode.elm.parentNode && oldVnode.elm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 刪除老節點
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}
// 創建虛擬DOM
function emptyNodeAt (elm) {
    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)
}

完善 createElement 函數

//  createElement.js只負責創建真正節點 
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
        // 上樹 往body上添加節點
        // insertBefore() 方法:可在已有的字節點前中插入一個新的子節點。相對于子節點來說的
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 有子節點
        for(let i = 0; i < vnode.children.length; i++) {
            // console.log(vnode.children[i]);
            let ch = vnode.children[i]
            // 進行遞歸 一旦調用createElement意味著 創建了DOM 并且elm屬性指向了創建好的DOM
            let chDom = createElement(ch)
            // 添加節點 使用appendChild 因為遍歷下一個之前 上一個真實DOM(這里的domVnode)已經生成了 所以可以使用appendChild
            domNode.appendChild(chDom)
        }
    }
    vnode.elm = domNode
    return vnode.elm
}

經過上面的分析,我們已經完成了對createElem函數的完善,可能你對這個遞歸有點不了解,那么大概捋一下進行的過程:

  • 首先,一開始的這個 新的虛擬DOM的sel 屬性為 ul,創建的真實DOM節點為 ul,執行 createElement函數 發現,新的虛擬DOM里面有children屬性,children 屬性里面又包含 h函數。

  • 其次,進入到for循環中,拿到 children 中的第一項,然后再次 調用crateElement函數 創建真實DOM,上面第一次調用createElement的時候已經創建了ul,執行完第一項返回創建的虛擬DOM,然后使用 appendChild方法()追加到 ul中,依次類推,執行后面的數組項。

  • 最后,將創建好的 所有真實DOM 返回出去,在 patch函數 中上樹。

執行上面的代碼,測試結果如下:

Vue中的虛擬DOM和Diff算法實例分析

完美!我們成功的將遞歸子節點完成了,無論嵌套多少層,我們都可以通過遞歸將子節點渲染到頁面上。

前面,我們實現了不是同一個節點的時候,進行刪除舊節點和插入新節點的操作,下面,我們來實現是相同節點時的相關操作,這也是文章中最重要的部分,diff算法 就包含在其中!!!

處理相同節點

上面的 patch函數 流程圖中,我們已經處理了不同節點的時候,進行暴力刪除舊的節點,然后插入新的節點,現在我們進行處理相同節點的時候,進行精細化的比較,繼續完善 patch函數 的主流程圖:

Vue中的虛擬DOM和Diff算法實例分析

看到上面的流程圖,你可能會有點疑惑,為什么不在 newVnode 是否有 Text屬性 中繼續判斷 oldVnode 是否有 children 屬性而是直接判斷兩者之間的 Text 是否相同,這里需要提及一個知識點,當我們進行 DOM操作的時候,文本內容替換DOM的時候,會自動將DOM結構全部銷毀掉,innerText改變了,DOM結構也會隨之被銷毀,所以這里可以不用判斷 oldVnode 是否存在 children 屬性,如果插入DOM節點,此時的Text內容并不會被銷毀掉,所以我們需要手動的刪除。這也是為什么在流程圖后面,我們添加 newVnode 的children 的時候需要將 oldVnode 的 Text 手動刪除,而將 newVnode 的 Text 直接賦值oldVnode.elm.innerText 的原因。
知道上面流程圖是如何工作了,我們繼續來書寫patch函數中是同一個節點的代碼。

// patch.js
import vnode from "./vnode";
import sameVnode from "./sameVnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
    // 判斷oldVnode是否是虛擬節點
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // console.log('不是虛擬節點');
        // 創建虛擬DOM
        oldVnode = emptyNodeAt(oldVnode)
    }
    // 判斷是否是同一個節點
    if (sameNode(oldVnode, newVnode)) {
        console.log('是同一個節點');
        // 是否是同一個對象
        if (oldVnode === newVnode) return
        // newVnode是否有text
        if (newVnode.text && (newVnode.children == undefined || newVnode.children.length == 0)) {
            // 判斷newVnode和oldVnode的text是否相同
            if (!(newVnode.text === oldVnode.text)) {
                // 直接將text賦值給oldVnode.elm.innerText 這里會自動銷毀oldVnode的cjildred的DOM結構
                oldVnode.elm.innerText = newVnode.text
            }
            // 意味著newVnode有children
        } else {
            // oldVnode是否有children屬性
            if (oldVnode.children != undefined && oldVnode.children.length > 0) {
                // oldVnode有children屬性
            } else {
                // oldVnode沒有children屬性
                // 手動刪除 oldVnode的text
                oldVnode.elm.innerText = ''
                // 遍歷
                for (let i = 0; i < newVnode.children.length; i++) {
                    let dom = createElement(newVnode.children[i])
                    // 追加到oldvnode.elm中
                    oldVnode.elm.appendChild(dom)
                }
            }
        }
    } else {
        // 暴力刪除舊節點,插入新的節點
        // 傳入參數為創建的虛擬DOM節點  返回以一個真實DOM
        let newVnodeElm = createElement(newVnode)
        console.log(newVnodeElm);
        // oldVnode.elm.parentNode 為body 在body中 在舊節點的前面添加新的節點
        if (oldVnode.elm.parentNode && oldVnode.elm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 刪除老節點
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}
// 創建虛擬DOM
function emptyNodeAt(elm) {
    return vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)
}
....
//創建虛擬節點
const myVnode1 = h('ul', {}, 'oldVnode有text')

patch(container, myVnode1)
const myVnode2 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D')
])
btn.addEventListener('click', () => {
    patch(myVnode1, myVnode2)
})

oldVnode 有 tex屬性 和 newVnode 有 children屬性 的效果如下:

Vue中的虛擬DOM和Diff算法實例分析

Vue中的虛擬DOM和Diff算法實例分析

...
//創建虛擬節點
const myVnode1 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D')
])

patch(container, myVnode1)
const myVnode2 = h('ul', {}, 'newVnode 有text')
btn.addEventListener('click', () => {
    patch(myVnode1, myVnode2)
})

oldVode 有children屬性 和 newVnode 有 text屬性 的效果如下:

Vue中的虛擬DOM和Diff算法實例分析

Vue中的虛擬DOM和Diff算法實例分析

完美!現在我們就只差最后diff算了。

patchVnode 函數

在patch函數中,我們需要將同同一節點的比較分成一個單獨的模塊patchVnode函數,方便我們在diff算法中進行遞歸運算。

patchVnode函數的主要作用就是:

  • 判斷newVnodeoldVnode是否指向同一個對象,如果是,那么直接return

  • 如果他們都有text并且不相等 或者 oldVnode有子節點而newVnode沒有,那么將oldVnode.elm的文本節點設置為newVnode的文本節點。

  • 如果oldVnode沒有子節點而newVnode有,則將newVnode的子節點真實化之后添加到oldVnode.elm后面,然后刪除 oldVnode.elmtext

  • 如果兩者都有子節點,則執行updateChildren函數比較子節點,這一步很重要

// patchVnode.js
export default function patchVnode(oldVnode, newVnode) {
    // 是否是同一個對象
    if (oldVnode === newVnode) return
    // newVnode是否有text
    if (newVnode.text && (newVnode.children == undefined || newVnode.children.length == 0)) {
        // 判斷newVnode和oldVnode的text是否相同
        if (!(newVnode.text === oldVnode.text)) {
            // 直接將text賦值給oldVnode.elm.innerText 這里會自動銷毀oldVnode的cjildred的DOM結構
            oldVnode.elm.innerText = newVnode.text
        }
        //說明 newVnode有 children
    } else {
        // oldVnode是否有children屬性
        if (oldVnode.children != undefined && oldVnode.children.length > 0) {
            // oldVnode有children屬性
        } else {
            // oldVnode沒有children屬性
            // 手動刪除 oldVnode的text
            oldVnode.elm.innerText = ''
            // 遍歷
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i])
                // 追加到oldvnode.elm中
                oldVnode.elm.appendChild(dom)
            }
        }
    }
}

diff算法

精細化比較:diff算法 四種優化策略這里使用雙指針的形式進行diff算法的比較,分別是舊前、舊后、新前、新后指針,(前指針往下移動,后指針往上移動

四種優化策略:(命中:key 和 sel 都要相同)

  • ①、新前與舊前

  • ②、新后與舊后

  • ③、新后與舊前

  • ④、新前與舊后

注意: 當只有第一種不命中的時候才會采取第二種,依次類推,如果四種都不命中,則需要通過循環來查找。 命中指針才會移動,否則不移動。

①、新前與舊前

Vue中的虛擬DOM和Diff算法實例分析 如果就舊節點先循環完畢,說明需要新節點中有需要插入的節點

②、新后與舊后

Vue中的虛擬DOM和Diff算法實例分析

如果新節點先循環完畢,舊節點還有剩余節點,說明舊節點中有需要刪除的節點。

多刪除情況:Vue中的虛擬DOM和Diff算法實例分析當只有情況①命中,剩下三種都沒有命中,則需要進行循環遍歷,找到舊節點中對應的節點,然后在舊的虛擬節點中將這個節點設置為undefined。刪除的節點為舊前與舊后之間(包含舊前、舊后)。

③、新后與舊前

Vue中的虛擬DOM和Diff算法實例分析③新后與舊前命中的時候,此時要移動節點,移動 新后指向的節點到舊節點的 舊后的后面,并且找到舊節點中對應的節點,然后在舊的虛擬節點中將這個節點設置為undefined

④、新前與舊后

Vue中的虛擬DOM和Diff算法實例分析

④新前與舊后命中的時候,此時要移動節點,移動新前指向的這個節點到舊節點的 舊前的前面,并且找到舊節點中對應的節點,然后在舊的虛擬節點中將這個節點設置為undefined

好了,上面通過動態講解的四種命中方式之后,動態gif圖片有水印,看著可能不是很舒服,但當然能夠理解是最重要的,那么我們開始手寫 diff算法 的代碼。

updateChildren 函數

updateChildren()方法 主要作用就是進行精細化比較,然后更新子節點。這里代碼比較多,需要耐心的閱讀。

import createElement from "./createElement";
import patchVnode from "./patchVnode";
import sameVnode from "./sameVnode";
export default function updateChildren(parentElm, oldCh, newCh) {
    //parentElm 父節點位置 用來移動節點 oldCh舊節點children newCh新節點children
    // console.log(parentElm, oldCh, newCh);
    // 舊前
    let oldStartIndex = 0
    // 舊后
    let oldEndIndex = oldCh.length - 1
    // 新前
    let newStartIndex = 0
    // 舊后
    let newEndIndex = newCh.length - 1
    // 舊前節點
    let oldStartVnode = oldCh[0]
    // 舊后節點
    let oldEndVnode = oldCh[oldEndIndex]
    // 新前節點
    let newStartVnode = newCh[0]
    // 新后節點
    let newEndVnode = newCh[newEndIndex]
    // 存儲mapkey
    let keyMap
    // 循環 條件 舊前 <= 舊后 && 新前 <= 新后
    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        // 首先需要判斷是否已經處理過了 
        if (oldCh[oldStartIndex] == undefined) {
            oldStartVnode = oldCh[++oldStartIndex]
        } else if (oldCh[oldStartIndex] == undefined) {
            oldEndVnode = oldCh[--oldEndIndex]
        } else if (newCh[newStartIndex] == undefined) {
            newStartVnode = newCh[++newStartIndex]
        } else if (newCh[newEndIndex] == undefined) {
            newEndVnode = newCh[--newEndIndex]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // ①、新前與舊前命中
            console.log('①、新前與舊前命中');
            //調用 patchVnode 對比兩個節點的 對象 文本 children
            patchVnode(oldStartVnode, newStartVnode)
            // 指針下移改變節點
            oldStartVnode = oldCh[++oldStartIndex]
            newStartVnode = newCh[++newStartIndex]

        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // ②、新后與舊后命中
            console.log('②、新后與舊后命中');
            //調用 patchVnode 對比兩個節點的 對象 文本 children
            patchVnode(oldStartVnode, newStartVnode)
            // 指針下移并改變節點
            oldEndVnode = oldCh[--oldEndIndex]
            newEndVnode = newCh[--newEndIndex]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // ③、新后與舊前命中
            patchVnode(oldStartVnode, newEndVnode)
            console.log(newEndVnode);
            // 移動節點 當③新后與舊前命中的時候,此時要移動節點,
            // 移動 新后(舊前兩者指向的是同一節點) 指向的節點到舊節點的 舊后的后面,并且找到舊節點中對應的節點,然后在舊的虛擬節點中將這個節點設置為undefined。
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
            // 在上面動畫中 命中③是在舊節點的后面插入的 所以使用nextSibling
            // 指針下移并改變節點
            oldStartVnode = oldCh[++oldStartIndex]
            newEndVnode = newCh[--newEndIndex]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // ④、新前與舊后命中
            patchVnode(oldEndVnode, newStartVnode)
            // 移動節點
            // 當`④新前與舊后`命中的時候,此時要移動節點,移動`新前(舊后指向同一個節點)`指向的這個節點到舊節點的 `舊前的前面`,
            //并且找到`舊節點中對應的節點`,然后在`舊的虛擬節點中將這個節點設置為undefined`
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
            //指針下移并改變節點
            oldEndVnode = oldCh[--oldEndIndex]
            newStartVnode = newCh[++newStartIndex]
        } else {
            // 四種都沒有命中
            console.log(11);
            //kepMap作為緩存不用每次遍歷對象
            if (!keyMap) {
                keyMap = {}
                // 遍歷舊的節點
                for (let i = oldStartIndex; i <= oldEndIndex; i++) {
                    // 獲取舊節點的key
                    const key = oldCh[i].data.key
                    if (key != undefined) {
                        //key不為空 并且將key存放到keyMap對象中
                        keyMap[key] = i
                    }
                }
            }
            // 取出newCh中的的key 并找出在keyMap中的位置 并映射到oldCh中
            const oldIndex = keyMap[newStartVnode.key]
            if (oldIndex == undefined) {
                // 新增
                console.log(111);
                parentElm.insertBefore(createElement(newStartVnode),oldStartVnode.elm)
            } else {
                // 移動位置
                // 取出需要移動的項
                const elmToMove = oldCh[oldIndex]
                // 判斷是選擇器是否一樣
                patchVnode(elmToMove, newStartVnode)
                // 標記已經處理過了
                oldCh[oldIndex] = undefined
                // 移動節點 移動到舊前前面 因為舊前與舊后之間要被刪除
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm)
            }
            // 只移動新的節點
            newStartVnode = newCh[++newStartIndex]
        }
    }
    //循環結束 還有剩余節點沒處理
    if (newStartIndex <= newEndIndex) {
        //說明 新節點還有未處理的節點,意味著需要添加節點
        console.log('新增節點');
        // 創建標桿 
        console.log(newCh[newEndIndex + 1]);
        // 節點插入的標桿位置 官方源碼的寫法 但是我們寫代碼新的虛擬節點中,elm設置了undefined 所以這里永遠都是會在后面插入 小bug
        // let before = newCh[newEndIndex + 1] == null ? null : newCh[newEndIndex + 1].elm
        // 若不想出現bug 可以在插入節點中直接oldCh[oldStartIndex].elm 但是也會出現不一樣的bug 所以重在學習思路
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            // 插入節點 因為舊節點遍歷完之后 新節點還有剩余節點 這里需要使用crateElement函數新建一個真實DOM節點
            // insertBefore會自動識別null
            parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIndex].elm)
        }
    } else if (oldStartIndex <= oldEndIndex) {
        //說明舊節點還有剩余節點還沒有處理 意味著需要刪除節點
        console.log('刪除節點');
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            if(oldCh[i]) parentElm.removeChild(oldCh[i].elm)
        }
    }
}

關于“Vue中的虛擬DOM和Diff算法實例分析”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Vue中的虛擬DOM和Diff算法實例分析”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

澳门| 湘西| 江陵县| 龙井市| 兰溪市| 特克斯县| 达拉特旗| 呼玛县| 张家川| 孟州市| 奇台县| 韶关市| 邯郸县| 兰坪| 金乡县| 高邮市| 吐鲁番市| 延津县| 永善县| 抚远县| 龙门县| 彰化县| 云和县| 高密市| 梁平县| 沿河| 和硕县| 社会| 太谷县| 汝州市| 惠安县| 宁津县| 永吉县| 文安县| 元阳县| 东源县| 五大连池市| 天峻县| 南漳县| 阿坝| 唐河县|