资讯专栏INFORMATION COLUMN

Virtual DOM的简单实现

sf_wangchong / 412人阅读

摘要:的实现了一套高效的算法来快速的比对更新树。关于的实现原理将在后面的文章中提到。对于子节点的操作将被记录在父节点的上。方法主要对具体的节点进行修改。

之前在看vue的源码时了解了vue关于Virtual DOM的一些想法,Virtual DOM可以帮助我们更高效的操作DOM。它通过实现一个vnode的js对象,vnode的对象与dom的node对象是一一对应的,通过我们对vnode的操作可以实现对dom的操作,这样就可以避免频繁的dom操作带来的效率问题。vue的Virtual DOM实现了一套高效的diff算法来快速的比对更新dom树。

关于vue的Virtual DOM实现原理将在后面的文章中提到。为了方便理解和学习,我写了一个简单的Virtual DOM操作DOM树的demo。这里是完整代码以及DOM

VNode

首先,创建vnode的对象,vnode记录相应的DOM对象的一些属性。

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.

这是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这个数组里。

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上。

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方法。

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组件的轮子工厂

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92517.html

相关文章

  • 一起理解 Virtual DOM

    摘要:具体而言,就是每次数据发生变化,就重新执行一次整体渲染。而给出了解决方案,就是。由于只关注,通过阅读两个库的源码,对于的定位有了更深一步的理解。第二个而且,技术本身不是目的,能够更好地解决问题才是王道嘛。 前言 React 好像已经火了很久很久,以致于我们对于 Virtual DOM 这个词都已经很熟悉了,网上也有非常多的介绍 React、Virtual DOM 的文章。但是直到前不久...

    Tangpj 评论0 收藏0
  • 从零开始,手写一个简易Virtual DOM

    摘要:本文为笔者通过实际操作,实现了一个非常简单的,加深对现今主流前端框架中的理解。用对象表示树是用对象表示,并存储在内存中的。如果类型不一致,那么属性一定是被更新的。如果有不相等的属性,则认为发生改变,需要处理的变化。 众所周知,对前端而言,直接操作 DOM 是一件及其耗费性能的事情,以 React 和 Vue 为代表的众多框架普遍采用 Virtual DOM 来解决如今愈发复杂 Web ...

    forrest23 评论0 收藏0
  • 你不知道Virtual DOM(二):Virtual Dom更新

    摘要:变化的只有种更新和删除。页面的元素的数量随着而变。四总结本文详细介绍如何实现一个简单的算法,再根据计算出的差异去更新真实的。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染...

    testbird 评论0 收藏0
  • 你不知道Virtual DOM(一):Virtual Dom介绍

    摘要:不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名属性和子元素对象。我们先来看下页面的更新一般会经过几个阶段。元素有可能是数组的形式,需要将数组解构一层。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React和Vue,都不约...

    lavor 评论0 收藏0
  • 构建一个使用 Virtual-DOM 前端模版引擎

    摘要:目录前言问题的提出模板引擎和结合的实现编译原理相关模版引擎的词法分析语法分析与抽象语法树代码生成完整的结语前言本文尝试构建一个前端模板引擎,并且把这个引擎和进行结合。于是就构思了一个方案,在前端模板引擎上做手脚。 作者:戴嘉华 转载请注明出处并保留原文链接( https://github.com/livoras/blog/issues/14 )和作者信息。 目录 前言 问题的提出...

    imccl 评论0 收藏0
  • 你不知道Virtual DOM(四):key作用

    摘要:最后里面没有第四个元素了,才会把苹果从移除。四总结本文基于上一个版本的代码,加入了对唯一标识的支持,很好的提高了更新数组元素的效率。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染...

    DirtyMind 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<