Virtual DOM 的简单实现
之前在看 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 defaultclassVNode{
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 inthis.props){
setAttr(el,prop,this.props[prop])
}
if(this.children){
this.children.forEach(function(ch,i){
el.appendChild(ch.render())
})
}
} elseif(this.nodeType===3){
el = document.createTextNode(this.text)
} elseif(this.nodeType===8){
el = document.createComment(this.text)
}
el.key = this.key
return el
}
}
function setAttr(node,key,value){
if(key==='style'){
for(let valin value){
node.style[val] = value[val]
}
} else {
node.setAttribute(key,value)
}
}
Diff
diff 主要是用来对比新旧 vnode 的区别,找出区别的元素并记录在 directives 对象上,便于接下来可以通过 directives 的内容对旧的 vnode 进行替换,绘制新的 DOM。
这是 diff 的入口方法,参数是旧的 vnode 和新的 vnode,directives 是用来记录每个节点的改变情况的对象。
export defaultfunctiondiff(oldVNode, newVNode){
directives = {}
diffVNode(oldVNode,newVNode,directives)
return directives
}
我们在 diff 方法中调用 diffVNode 来对节点进行逐一比较。首先,它会比较 oldVNode 和 newVNode 是否是相同的节点。如果相同,就对节点类型进行判断,来选择比较的方法,对于文本和注释节点,只需要比较文本内容是否相同即可,对于元素则要比较元素标签,元素的属性以及子元素是否相同。
functiondiffVNode(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})
}
} elseif(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 这个数组里。
functiondiffProps(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 上。
functiondiffChildren(oldChildren,newChildren,parentKey){
oldChildren = oldChildren || []
newChildren = newChildren || []
let movedItem = []
let oldKeyIndexObject = parseNodeList(oldChildren)
let newKeyIndexObject = parseNodeList(newChildren)
for(let key innewKeyIndexObject){
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 节点进行操作。完整代码
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论