帮助我们高效操作的Virtual DOM简单实现
作者:AshleyLv
引言
之前在看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节点进行操作。
以上就是Virtual DOM简单实现的详细内容,更多关于Virtual DOM简单实现的资料请关注脚本之家其它相关文章!