帶你簡單理解diff算法

關於virtual dom

咱們知道不論是vue仍是react當中,都是利用virtual dom(下面簡稱vd)來表示真實的dom,由於操做真實的dom的代價是昂貴的,即便是查找dom節點的操做都是昂貴的,因此在優化的方法當中,就有緩存dom的查找結果的一個優化,那麼既然真實dom的操做是昂貴的,因此若是咱們在使用diff算法來比較兩個dom之間的差別的時候,就要遍歷全部的dom來進行對比,若是是按照真實的dom來進行diff算法的比較的話,那麼就至關消耗性能了,所以vd應運而生。那麼怎麼將真實的dom和vd對應起來呢?咱們知道,dom不外乎三個特性:
一、標籤名
二、各類屬性
三、孩子節點
所以,若是要用vd來表示dom的話咱們就能夠這樣定義。vue

class VNode {
    constructor(tagName, attributes, children) {
        this.tagName = tagName
        this.attributes = attributes
        this.children = children
    }
}
複製代碼

好比有這樣的domnode

<div id="div" class="classVal">
    <span>child</span>
</div>
複製代碼

那麼vd就是這樣的react

{
    tagName: 'div',
    attributes: {
        'id': 'div',
        'class': 'classVal'
    },
    children: [{
        tagName: 'span',
        attributes: null,
        children: ['child']
    }]
}
複製代碼

固然,這裏vd的定義少了TEXT節點,因此咱們加上TEXT節點,TEXT節點直接返回裏面的innerText/textContent,就像上面的children: ['child'],咱們定義一個叫h的函數,用來建立vd,包括TEXT節點,它接受四個參數,分別以下:git

tagName: 標籤名  
text: 若是是TEXT節點,那麼就是TEXT的內容,即innerText/textContent
attributes: dom屬性的價值對  
children: dom的孩子vd
複製代碼
function h(tagName, text, attributes, children) {
    // 判斷到是TEXT節點,直接返回TEXT裏面的內容
    if(text) {
        return text
    }
    return new VNode(tagName, attributes, children)
}
複製代碼

好了,VD大概就是這樣子表示,那麼咱們若是根據vd還原成真實的dom呢,其實很簡單,就是根據一一對應關係還原唄:github

function createElement(vnode) {
    var el = null;
    // 文本元素
    if(typeof vnode === "string") {
        el = document.createTextNode(vnode);
        return el;
    }
    // 還原dom
    el = document.createElement(vnode.tagName);
    // 還原attribute
    for(var key in attributes) {
        el.setAttribute(key, attributes[key]);
    }
    // 還原孩子節點
    var children = vnode.children.map(createElement);
    children.forEach(function(child) {
        el.appendChild(child);
    });
    return el;
}
複製代碼

關於vd的理解差很少就這樣,若是有須要補充的或者指正的,望不吝賜教。算法

diff算法

有了vd後,咱們要怎麼比較兩個dom樹之間的不一樣呢,固然不能無腦的使用innerHTML對整塊樹更新(backbone就是這樣),而是針對更改的地方進行更新或者替換,那麼咱們就須要依賴diff來找出兩棵樹之間的不一樣。
傳統的diff算法,是須要跨級對比兩個樹之間的不一樣,時間複雜度爲O(n^3),這樣的對比是沒法接受的,因此react提出了一個簡單粗暴的diff算法,只對比同級元素,這樣算法複雜度就變成了O(n)了,雖然不能作到最優的更新,可是時間複雜度大大減小,是一種平衡的算法,下面會提到。緩存

image

那麼怎麼理解它是隻對比同級和具體它是怎麼對比的呢?
基於diff算法的同級對比,咱們先講下對比的過程當中,它主要分爲四種類型的對比,分別爲:
一、新建create: 新的vd中有這個節點,舊的沒有
二、刪除remove: 新的vd中沒有這個節點,舊的有
三、替換replace: 新的vd的tagName和舊的tagName不一樣
四、更新update: 除了上面三點外的不一樣,具體是比較attributes先,而後再比較children
寫成代碼就是這樣:bash

diff(newVnode, oldVNode) {
    
    if(!newVNode) {
        // 新節點中沒有,說明是刪除舊節點的
        return {
            type: 'remove'
        }
    } else if(!oldVNode) {
        // 新節點中有舊節點沒有的,說明是刪除
        return {
            type: 'create',
            newVNode
        }
    } else if(isDiff(newVNode, oldVNode)) {
        // 只要對比出兩個節點的tagName不一樣,說明是替換
        return {
            type: 'replace',
            newVNode
        }
    } else {
        // 其餘狀況是更新節點,要對比兩個節點的attributes和孩子節點
        return {
            type: 'update',
            attributes: diffAttributes(newVNode, oldVNode),
            children: diffChildren(newVNode, oldVNode)
        }
    }
}

// 對比孩子節點,其實就是遍歷全部的孩子節點,而後調用diff對比
function diffChildren(newVnode, oldVNode) {
    var patches = []
    // 這裏要獲取兩個節點中的最大孩子數,而後再進行對比 
    var len = Math.max(newVnode.children.length, oldVNode.children.length);
    for(let i = 0; i <len; i++) {
        patches[i] = diff(newVnode.children[i], oldVnode.children[i])
    }
    return patches
}

// 對比attribute,只有兩種狀況,要不就是值改變/新建,要不就是刪除值,對比dom只有setAttribute和removeAttribute就知道了
function diffAttributes(newVnode, oldVNode) {
    var patches = []
    // 獲取新舊節點的全部attributes
    var attrs = Object.assign({}, oldVNode.attributes, newVNode.attributes)
    for(let key in attrs) {
        let value = attrs[key]
        // 只要新節點的屬性值和久節點的屬性值不一樣,就判斷爲新建,不論是更新和真正的新建都是調用setAttribute來更新
        if(oldVNode.attributes[key] !== value) {
            patches.push({
                type: 'create',
                key,
                value: newVnode.attributes[key]
            })
        } else if(!newVNode.attributes[key]) {
            patches.push({
                key,
                type: 'remove'
            })
        }
    }
    return patches
}

// 判斷兩個節點是否不一樣
function isDiff(newVNode, oldVNode) {
    // 正常狀況下,只對比tagName,可是text節點對比沒有tagName,因此要考慮text節點
    return (typeof newVNode === 'string' && newVNode !== oldVNode) 
    || (typeof oldVNode === 'string' && newVNode !== oldVNode) 
    || newVNode.tagName !== oldVNode.tagName
}
複製代碼

結合代碼,你們對比下面的圖,圖裏面remove沒有列出來,remove和create差很少緣由,相信你們知道什麼狀況下是remove。 mvc

image
上圖,按傳統的方法只是在span和p直接插入了一個div,可是diff算法不是這麼來更新的,它只對比同一級別的,即會以爲舊節點的p和新節點的div纔是同一級,他們的tagName不一樣,因此定義爲replace,接着把舊節點的div當作和新節點的p是同一級,依舊是replace,最後舊節點沒有div,因此create了。能夠看到,其實這個更新代價仍是比較大的,可是比對的過程卻簡單和快速,所以是一種相對平衡的算法。

完整的代碼你們能夠看下個人git地址:github.com/VikiLee/XLM…app

總結

咱們更新dom的時候,儘可能不要整棵樹進行更新,須要作到細顆粒的更新,要作到細顆粒地更新就必須知道兩棵樹直接的不一樣,因此須要使用diff算法來進行對比,可是傳統的diff算法雖然能作到細顆粒準確地更新,可是它須要花銷大量的時間來進行比對,因此有來react的改版的diff算法,只比較同一級的元素,這樣能夠作到快速的比對,爲O(n),即便這樣,在對比兩棵樹的時候,咱們仍是須要遍歷全部的節點,咱們知道dom的操做是昂貴的,即便是查找,也是昂貴的一個過程,特別是在節點不少的donm樹下,因此虛擬dom應運而生,虛擬dom避開了直接操做dom的缺點,而是直接對比內存中vd,使得對比速度進一步獲得質地提高。

相關文章
相關標籤/搜索