由于在浏览器中操作DOM的代价是非常大的,所以在Vue引入了虚拟DOM,虚拟DOM是对真实DOM的一种抽象描述。将虚拟DOM渲染成真实DOM并挂载到页面中,将虚拟DOM渲染成真实DOM的逻辑也比较简单。
主要步骤就是:创建节点→设置节点属性→添加子节点
DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。
作用: DOM-diff 算法主要功能是收集新老两个节点之间的变化,然后根据节点的变化来更新DOM。
// diff.js
function diff(oldNode, newNode) {
// 收集两个VDOM节点之间的不同
// 递归比较两个虚拟DOM,将比较结果放入Pathes中
let patches = walk(oldNode, newNode)
return patches
}
function walk(oldNode, newNode) {
let currentPatch = {} // 每一个DOM元素都有一个patch
if (!newNode) {
// 如果不存在newNode,说明该Node被移除
currentPatch.diff = {type: 'REMOVE', oldNode}
} else if(newNode && !oldNode) {
// 说明是新结点
currentPatch.diff = {type: 'ADD', newNode}
} else if (isString(oldNode) && isString(newNode)) {
// 如果是文本, 则判断文本是否一致
if (oldNode !== newNode) {
currentPatch.diff = {type: 'TEXT', newNode}
}
} else if (oldNode.type === newNode.type) {
// 如果是虚拟DOM元素
// 比较属性是否相同
let attrDiff = diffAttr(oldNode.props, newNode.props)
if (Object.keys(attrDiff).length > 0) {
currentPatch.diff = {type: 'ATTR', attrDiff}
} else {
// 说明该元素的属性相同, 可以理解为node没有被修改
currentPatch.diff = {}
}
// 比较子节点是否相同
currentPatch.children = []
diffChildren(oldNode.children, newNode.children, currentPatch.children)
} else if(oldNode.type !== newNode.type) {
// 否则说明节点被替换了
currentPatch.diff = {type: 'REPLACE', newNode}
} else {
// 说明没有被修改, 添加一个空对象
currentPatch.diff = {}
}
return currentPatch
}
function isString(obj) {
return obj && typeof obj === 'string'
}
function diffAttr(oldAttrs, newAttrs) {
let patch = {}
// 新老属性是否相同
for (let key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key]
}
}
// 老节点中没有但新结点中有的属性
for (let key in newAttrs) {
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key]
}
}
return patch
}
function diffChildren(oldChildren, newChildren, childrenPatch) {
// 比较子节点
newChildren.forEach((item, i) => {
childrenPatch.push(walk(oldChildren[i], item))
})
}
主要工作就是收集每个节点是否产生了变化,主要做了一下工作来得到两个节点的不同, 最终得到的patch的结构和虚拟DOM结构一样都是一个树状的结构。
节点更新
// patch.js
function patch(node, patch) {
run(node, patch)
}
/**
*
* @param {HTMLElement} node 节点
* @param {Object} patches 节点之间的不同
* @param {Number} index 节点树的第几层
* @param {Number} i 该层的第几个节点
*/
function run(node, patch) {
let childNodes = node.childNodes
let patchChildren = patch.children
// 先修改每个子节点
for (let i = 0; i < childNodes.length; ++i) {
run(childNodes[i], patchChildren[i])
}
// patcheChildren的长度大于childNodes的长度, 说明有添加了新的节点
if (patchChildren && patchChildren.length && patchChildren.length > childNodes.length) {
for (let i = childNodes.length; i < patchChildren.length; ++i) {
doPatch(node, patchChildren[i])
}
}
if (patch) {
doPatch(node, patch)
}
}
function doPatch(node, patch) {
console.log(Object.keys(patch))
if (Object.keys(patch).length > 0) {
let { diff } = patch
switch(diff.type) {
case 'ADD':
var newNode = diff.newNode
newNode = newNode instanceof Element ? render(newNode) : document.createTextNode(newNode)
node.appendChild(newNode)
break
case 'ATTR':
for (let key in diff.attrDiff) {
let value = diff.attrDiff[key]
setAttr(node, key, value)
}
break
case 'TEXT':
node.textContent = diff.newNode
break
case 'REPLACE':
var newNode = diff.newNode
let replaceNode = newNode instanceof Element ? render(newNode) : document.createTextNode(newNode)
node.parentNode.replaceNode(replaceNode, node)
break
case 'REMOVE':
node.parentNode.removeChild(node)
break
}
}
}
patch内部只是简单的调用了run方法
run方法中做了两件事
深度优先当前节点子节点先对子节点进行修改
如果patch.children的长度大于当前节点的子节点的长度,说明添加了一个新的元素,调用doPatch方法将新添加的元素加入当前节点
然后调用doPatch方法对当前节点进行处理
doPatch方法主要是根据修改的类型对node节点进行处理。
总结:
整个DOM-diff的过程:
用JS对象模拟DOM(虚拟DOM)
把此虚拟DOM转成真实DOM并插入页面中(render)
如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
把差异对象应用到真正的DOM树上(patch)