vitual-dom原理與簡單實現

前言

目前廣爲人知的React和Vue都採用了virtual-dom,Virtual DOM憑藉其高效的diff算法,讓咱們再也不關心性能問題,能夠爲所欲爲的修改數據狀態。在實際開發中,咱們並不須要關心Virtual DOM是如何實現的,可是理解Virtual DOM的實現原理確實有必要的。本文是參照https://github.com/livoras/simple-virtual-dom 源碼進行理解vitual DOM。html

若是你以爲我寫的不錯,幫我點個star前端

1、前端應用狀態管理

在日益複雜的前端應用中,狀態管理是一個常常被說起的話題,從早期的刀耕火種時代到jQuery,再到如今流行的MVVM時代,狀態管理的形式發生了翻天覆地的變化,咱們不再用維護茫茫多的事件回調、監聽來更新視圖,轉而使用使用雙向數據綁定,只須要維護相應的數據狀態,就能夠自動更新視圖,極大提升開發效率。node

可是,雙向數據綁定也並非惟一的辦法,還有一個很是粗暴有效的方式:一旦數據發生變化,從新繪製整個視圖,也就是從新設置一下innerHTML。這樣的作法確實簡單、粗暴、有效,可是若是隻是由於局部一個小的數據發生變化而更新整個視圖,性價比未免過低了,並且,像事件,獲取焦點的輸入框等,都須要從新處理。因此,對於小的應用或者說局部的小視圖,這樣處理徹底是能夠的,可是面對複雜的大型應用,這樣的作法不可取。因此咱們能夠採起用JavaScript的方法來模擬DOM樹,用新渲染的對象樹去和舊的樹進行對比,記錄下變化的變化,而後應用到真實的DOM樹上,這樣咱們只須要更改與原來視圖不一樣的地方,而不須要所有從新渲染一次。這就是virtual-DOM的優點react

2、視圖渲染

相對於DOM對象,原生的JavaScript對象處理得更快,並且簡單。DOM樹上的結構,屬性信息咱們都能經過JavaScript進行表示出來,例如:git

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渲染的結果就是:github

<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一個節點很是簡單,咱們只須要記錄他的節點類型,屬性鍵值對,子節點:bash

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

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

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結構中呢: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>
複製代碼

3、比較虛擬DOM樹的差別

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

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
    })
}
複製代碼

例如:

在圖中若是div有差別,標記爲0,那麼:

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

同理,有p是patches[1], ul是patches[3],以此類推 patches指的是差別變化,這些差別包括:一、節點類型的不一樣,二、節點類型相同,可是屬性值不一樣,文本內容不一樣。因此有這麼幾種類型:

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'
    }
}]
複製代碼

4、將差別應用到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

6、 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

相關文章
相關標籤/搜索