Virtual DOM的簡單實現

以前在看vue的源碼時瞭解了vue關於Virtual DOM的一些想法,Virtual DOM能夠幫助咱們更高效的操做DOM。它經過實現一個vnode的js對象,vnode的對象與dom的node對象是一一對應的,經過咱們對vnode的操做能夠實現對dom的操做,這樣就能夠避免頻繁的dom操做帶來的效率問題。vue的Virtual DOM實現了一套高效的diff算法來快速的比對更新dom樹。vue

關於vue的Virtual DOM實現原理將在後面的文章中提到。爲了方便理解和學習,我寫了一個簡單的Virtual DOM操做DOM樹的demo。這裏是完整代碼以及DOMnode

VNode

首先,建立vnode的對象,vnode記錄相應的DOM對象的一些屬性。git

export default class VNode {

    constructor (tag, nodeType,key, props, text, children){
        this.tag = tag //element類型
        this.nodeType = nodeType //node類型,1爲普通節點,3爲文本節點,8爲註釋
        this.key = key
        this.props = props //node的屬性
        this.text = text //文本節點的內容
        this.children = children//子節點
    }
    //將vnode渲染成DOM節點的方法
    render(){
        var el
        if(this.nodeType===1){
            el = document.createElement(this.tag)
            for(let prop in this.props){
                setAttr(el,prop,this.props[prop])
            }
            if(this.children){
                this.children.forEach(function(ch,i){
                    el.appendChild(ch.render())
                })

            }
        } else if(this.nodeType===3){
            el = document.createTextNode(this.text)
        } else if(this.nodeType===8){
            el = document.createComment(this.text)
        }
        el.key = this.key
        return el

    }
}

function setAttr(node,key,value){
    if(key==='style'){
        for(let val in value){
            node.style[val] = value[val]
        }
    } else {
        node.setAttribute(key,value)
    }
}

Diff

diff主要是用來對比新舊vnode的區別,找出區別的元素並記錄在directives對象上,便於接下來能夠經過directives的內容對舊的vnode進行替換,繪製新的DOM.github

這是diff的入口方法,參數是舊的vnode和新的vnode,directives是用來記錄每一個節點的改變狀況的對象。算法

export default function diff(oldVNode, newVNode){
    directives = {}
    diffVNode(oldVNode,newVNode,directives)
    return directives

}

咱們在diff方法中調用diffVNode來對節點進行逐一比較。首先,它會比較oldVNode和newVNode是不是相同的節點。若是相同,就對節點類型進行判斷,來選擇比較的方法,對於文本和註釋節點,只須要比較文本內容是否相同便可,對於元素則要比較元素標籤,元素的屬性以及子元素是否相同。數組

function diffVNode(oldVNode,newVNode){

    if(newVNode && isSameTypeNode(oldVNode,newVNode)){
        if(newVNode.nodeType===3 || newVNode.nodeType===8){
            if(oldVNode.text !== newVNode.text){
                addDirectives(newVNode.key,{type:TEXT, content: newVNode.text})
            }
        } else if(newVNode.nodeType===1){
            if(oldVNode.tag === newVNode.tag && oldVNode.key == newVNode.key){
                var propPatches = diffProps(oldVNode.props, newVNode.props)
                if(Object.keys(propPatches).length>0){
                    addDirectives(newVNode.key,{type:PROP, content: propPatches})
                }
                if(oldVNode.children || newVNode.children)
                    diffChildren(oldVNode.children,newVNode.children,newVNode.key)
            }
        }
    }
    return directives
}

這是比較節點屬性的方法,對於有變化的屬性咱們將變化的部分記在patches這個數組裏。app

function diffProps(oldProps,newProps){
    let patches={}
    if(oldProps){
        Object.keys(oldProps).forEach((prop)=>{
            if(prop === 'style' && newProps[prop]){
                let newStyle = newProps[prop]
                let isSame = true
                Object.keys(oldProps[prop]).forEach((item)=>{
                    if(prop[item] !== newStyle[item]){
                        isSame = false
                    }
                })
                if(isSame){
                    Object.keys(newStyle).forEach((item)=>{
                        if(!prop.hasOwnProperty(item)){
                            isSame = false
                        }
                    })
                }
                if(!isSame)
                    patches[prop] = newProps[prop]
            }
            if(newProps[prop] !== oldProps[prop]){
                patches[prop] = newProps[prop]
            }
        })
    }
    if(newProps){
       Object.keys(newProps).forEach((prop)=>{
        if(!oldProps.hasOwnProperty(prop)){
            patches[prop] = newProps[prop]
        }
    })
   }
   
    return patches
}

下面是比較子節點的方法,子節點的更新分爲增長子節點,刪除子節點和移動子節點三種操做。對於子節點的操做將被記錄在父節點的directives上。dom

function diffChildren(oldChildren,newChildren,parentKey){
    oldChildren = oldChildren || []
    newChildren = newChildren || []
    let movedItem = []
    let oldKeyIndexObject = parseNodeList(oldChildren)
    let newKeyIndexObject = parseNodeList(newChildren)
    for(let key in newKeyIndexObject){
        if(!oldKeyIndexObject.hasOwnProperty(key)){
            addDirectives(parentKey,{type:INSERT,index:newKeyIndexObject[key],node:newChildren[newKeyIndexObject[key]]})
        }
    }
    for(let key in oldKeyIndexObject){
        if(newKeyIndexObject.hasOwnProperty(key)){
            if(oldKeyIndexObject[key] !== newKeyIndexObject[key]){
                let moveObj = {'oldIndex':oldKeyIndexObject[key],'newIndex':newKeyIndexObject[key]}
                movedItem[newKeyIndexObject[key]] = oldKeyIndexObject[key]
            }
            diffVNode(oldChildren[oldKeyIndexObject[key]],newChildren[newKeyIndexObject[key]])
        } else {
            addDirectives(key,{type:REMOVE,index:oldKeyIndexObject[key]})
        }
    }
    if(movedItem.length>0){
        addDirectives(parentKey,{type:MOVE, moved:movedItem})
    }
    
}

在通過Diff方法後,咱們將獲得咱們傳入的oldNode與newNode的比較結果,並記錄在Directives對象中。學習

Patch

Patch主要作的是經過咱們以前的比較獲得的Directives對象來修改Dom樹。在Patch方法中若是該節點涉及到更新,將會調用applyPatch方法。this

export default function patch(node,directives){
    if(node){
        var orderList = []
        for(let child of node.childNodes){

            patch(child,directives)
        }
        if(directives[node.key]){
            applyPatch(node,directives[node.key])
        }
    }

}

applyPatch方法主要對具體的Dom節點進行修改。根據directives的不一樣類型,調用不一樣的方法進行更新。

function applyPatch(node, directives){
    
    for(let directive of directives){
        switch (directive.type){
            case TEXT:
                setContent(node,directive.content)
                break
            case PROP:
                setProps(node,directive.content)
                break
            case REMOVE:
                removeNode(node)
                break
            case INSERT:
                insertNode(node,directive.node,directive.index)
            default:
                break
        }
        
    }
}

具體的更新方法是經過js來操做DOM節點進行操做。
完整代碼

推薦一個找vue,angular組件的輪子工廠

相關文章
相關標籤/搜索