vitual-dom原理與簡單實現

B_Cornelius發表於2017-12-01

前言

目前廣為人知的React和Vue都採用了virtual-dom,Virtual DOM憑藉其高效的diff演算法,讓我們不再關心效能問題,可以隨心所欲的修改資料狀態。在實際開發中,我們並不需要關心Virtual DOM是如何實現的,但是理解Virtual DOM的實現原理確實有必要的。本文是參照https://github.com/livoras/simple-virtual-dom 原始碼進行理解vitual DOM。

如果你覺得我寫的不錯,幫我點個star

一、前端應用狀態管理

在日益複雜的前端應用中,狀態管理是一個經常被提及的話題,從早期的刀耕火種時代到jQuery,再到現在流行的MVVM時代,狀態管理的形式發生了翻天覆地的變化,我們再也不用維護茫茫多的事件回撥、監聽來更新檢視,轉而使用使用雙向資料繫結,只需要維護相應的資料狀態,就可以自動更新檢視,極大提高開發效率。

但是,雙向資料繫結也並不是唯一的辦法,還有一個非常粗暴有效的方式:一旦資料發生變化,重新繪製整個檢視,也就是重新設定一下innerHTML。這樣的做法確實簡單、粗暴、有效,但是如果只是因為區域性一個小的資料發生變化而更新整個檢視,價效比未免太低了,而且,像事件,獲取焦點的輸入框等,都需要重新處理。所以,對於小的應用或者說區域性的小檢視,這樣處理完全是可以的,但是面對複雜的大型應用,這樣的做法不可取。所以我們可以採取用JavaScript的方法來模擬DOM樹,用新渲染的物件樹去和舊的樹進行對比,記錄下變化的變化,然後應用到真實的DOM樹上,這樣我們只需要更改與原來檢視不同的地方,而不需要全部重新渲染一次。這就是virtual-DOM的優勢

二、檢視渲染

相對於DOM物件,原生的JavaScript物件處理得更快,而且簡單。DOM樹上的結構,屬性資訊我們都能通過JavaScript進行表示出來,例如:

var element = {
    tagName: 'ul', // 節點標籤名
    props: { // dom的屬性鍵值對
        id: 'list'
    },
    children: [
        {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}
    ]
}
複製程式碼

那麼在html渲染的結果就是:

<ul id="list">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
    <li class="item">Item 3</li>
</ul>
複製程式碼

既然能夠通過JavaScript表示DOM樹的資訊,那麼就可以通過使用JavaScript來構建DOM樹。

然而光是構建DOM樹,沒什麼卵用,我們需要將JavaScript構建的DOM樹渲染到真實的DOM樹上,用JavaScript表現一個dom一個節點非常簡單,我們只需要記錄他的節點型別,屬性鍵值對,子節點:

function Element(tagName, props, children) {
    this.tagName = tagName
    this.props = props
    this.children = children
}
複製程式碼

那麼ul標籤我們就可以使用這種方式來表示

var ul = new Element('ul', {id: 'list'}, [
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}
])
複製程式碼

說了這麼多,他只是用JavaScript表示的一個結構,那該如何將他渲染到真實的DOM結構中呢:

Element.prototype.render = function() {
    let el = document.createElement(this.tagName), // 節點名稱
        props = this.props // 節點屬性

    for (var propName in props) {
        propValue = props[propName]
        el.setAttribute(propName, propValue)
    }

    this.children.forEach((child) => {
        var childEl = (child instanceof Element)
            ? child.render()
            : document.createTextNode(child)
        el.appendChild(childEl)
    })
    return el
}
複製程式碼

如果我們想將ul渲染到DOM結構中,就只需要

ulRoot = ul.render()
document.appendChild(ulRoot)
複製程式碼

這樣就完成了ul到DOM的渲染,也就有了真正的DOM結構

<ul id="list">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
    <li class="item">Item 3</li>
</ul>
複製程式碼

三、比較虛擬DOM樹的差異

React的核心演算法是diff演算法(這裡指的是優化後的演算法)我們來看看diff演算法是如何實現的:

vitual-dom原理與簡單實現

diff只會對相同顏色方框內的DOM節點進行比較,即同一個父節點下的所有子節點。當發現節點不存在,則該節點和子節點會被完全刪除,不會做進一步的比較。

在實際的程式碼中,會對新舊兩棵樹進行深度的遍歷,給每一個節點進行標記。然後在新舊兩棵樹的對比中,將不同的地方記錄下來。

// diff 演算法,對比兩棵樹
function diff(oldTree, newTree) {
    var index = 0   // 當前節點的標誌
    var patches = {} // 記錄每個節點差異的地方
    dfsWalk(oldTree, newTree, index, patches)
    return patches
}
function dfsWalk(oldNode, newNode, index, patches) {
    // 對比newNode和oldNode的差異地方進行記錄
    patches[index] = [...]

    diffChildren(oldNode.children, newNode.children, index, patches)
}
function diffChildren(oldChildren, newChildren, index, patches) {
    let leftNode = null
    var currentNodeIndex = index
    oldChildren.forEach((child, i) => {
        var newChild = newChildren[i]
        currentNodeIndex =  (leftNode && leftNode.count) // 計算節點的標記
                ? currentNodeIndex + leftNode.count + 1
                : currentNodeIndex + 1
        dfsWalk(child, newChild, currentNodeIndex, patches) // 遍歷子節點
        leftNode = child
    })
}
複製程式碼

例如:

vitual-dom原理與簡單實現

在圖中如果div有差異,標記為0,那麼:

patches[0] = [{difference}, {difference}]
複製程式碼

同理,有p是patches[1], ul是patches[3],以此類推 patches指的是差異變化,這些差異包括:1、節點型別的不同,2、節點型別相同,但是屬性值不同,文字內容不同。所以有這麼幾種型別:

var REPLACE = 0,    // replace 替換
    REORDER = 1,    // reorder 父節點中子節點的操作
    PROPS   = 2,    // props 屬性的變化
    TEXT    = 3     // text 文字內容的變化
複製程式碼

如果節點型別不同,就說明是需要替換,例如將div替換成section,就記錄下差異:

patches[0] = [{
    type: REPLACE,
    node: newNode // section
},{
    type: PROPS,
    props: {
        id: 'container'
    }
}]
複製程式碼

四、將差異應用到DOM樹上

在標題二中構建了真正的DOM樹的資訊,所以先對那一棵DOM樹進行深度優先的遍歷,遍歷的時候同 patches物件進行對比,找到其中的差異,然後應用到DOM操作中。

function patch(node, patches) {
    var walker = {index: 0} // 記錄當前節點的標誌
    dfsWalk(node, walker, patches)
}

function dfsWalk(node, walker, patches) {
    var currentPatches = patches[walker.index] // 這是當前節點的差異

    var len = node.childNodes
        ? node.childNodes.length
        : 0

    for (var i = 0; i < len; i++) { // 深度遍歷子節點
        var child = node.childNodes[i]
        walker.index++
        dfsWalk(child, walker, patches)
    }

    if (currentPatches) {
        applyPatches(node, currentPatches) // 對當前節點進行DOM操作
    }
}
// 將差異的部分應用到DOM中
function applyPatches(node, currentPatches) {
    currentPatches.forEach((currentPatch) => {
        switch (currentPatch.type) {
            case REPLACE:
                var newNode = (typeof currentPatch.node === 'string')
                    ? document.createTextNode(currentPatch.node)
                    : currentPatch.node.render()
                node.parentNode.replaceChild(newNode, node)
                break;
            case REORDER:
                reorderChldren(node, currentPatch.moves)
                break
            case PROPS:
                setProps(node, currentPatch.props)
                break
            case TEXT:
                if (node.textContent) {
                    node.textContent = currentPatch.content
                } else {
                    node.nodeValue = currentPatch.content
                }
                break
            default:
                throw new Error('Unknown patch type ' + currentPatch.type)
        }
    })
}

複製程式碼

這次的粗糙的virtual-dom基本已經實現了,具體的情況更加複雜。但這已經足夠讓我們理解virtual-dom。 具體的帶解析的程式碼已經上傳到github

六、 References

https://www.cnblogs.com/justany/archive/2015/04/08/4401118.html https://github.com/livoras/blog/issues/13 https://github.com/y8n/blog/issues/5 https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060 http://www.infoq.com/cn/articles/react-dom-diff

相關文章