1.为什么引入虚拟DOM(可以节省开销) 在vue1.0实现侦测变化的时候,一个状态绑定了好多个依赖,每个依赖代表着一个具体的DOM节点。当状态发生变化时,它在一定程度上会知道哪些节点使用了这些状态。但是这样做会有内存开销和依赖追踪的开销。对于一个大型项目来说,这个开销非常大。因此vue2.0选了一个中等粒度的方案,那就是引入虚拟DOM,并把依赖变成组件级别。当状态发生变化时,只通知到组件,然后组件内部使用虚拟DOM进行对比,根据结果只更新需要更新的真实DOM节点,从而避免不必要的DOM操作,节省开销。 2.什么虚拟DOM 虚拟DOM是将状态映射成视图的一种解决方案。 虚拟DOM的原理或者概念:就是使用状态生成虚拟节点,将新的虚拟节点vnode和旧虚拟节点oldvnode对比,然后更新视图。 3.什么是vnode(虚拟节点) Vnode可以理解成一个节点描述对象。它描述了应该怎样创建真实的DOM节点。 4.vnode的作用/渲染视图的过程 先创建vnode,再使用vnode生成真实的DOM元素,最后插入到页面渲染视图。 5.为什么使用diff/patch算法对比两个节点的差异 因为DOM操作的执行速度远不如js的运算速度快,使用patch算法计算出真正要更新的节点,减少DOM操作,从而提高性能。 6.Patch/diff算法(补丁,修补) 概念:对比新旧两个vnode之间的差异,然后找出需要更新的节点进行更新。并将vnode渲染成真实的DOM。 当旧虚拟节点不存在时,直接使用vnode渲染视图(比如首次渲染视图);当旧虚拟节点与新的虚拟节点都存在但并不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM元素,(先插到旁边,再删除);当旧虚拟节点和新虚拟节点是同一个节点时,使用更详细的对比操作对真实节点进行更新(在后面的流程图里面)。 DOM操作非常昂贵,为了减少对DOM的操作,vue.js将DOM抽象成了一个以js对象为节点的虚拟DOM树,用VNode节点模拟真实DOM节点,可以对虚拟DOM节点进行创建,删除以及修改等操作,这个过程不需要操作真实DOM。当数据发生改变时,Vue.js会使用diff算法比对oldvnode和vnode,得出最小的修改单位,对真实DOM打补丁。 当数据进行更新的时候,会触发update函数,该函数会接收一个VNode对象,利用_patch_方法对比VNode和OldVNode,并对真实的DOM打补丁,_patch_的核心就是diff算法。diff算法是通过同层的树节点进行比较,而不是对树进行逐层搜索,时间复杂度只有log(n)。 diff算法:使用sameVNode函数判断是否为相同节点(对比标签名tag,属性key,是否为注释节点iscomment是否相同),不是相同节点则在真实DOM上创建新的DOM,移除旧的DOM;是相同节点则调用patchVnode函数比较节点。 sameVnode函数:
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
function sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB
}
patchVnode函数:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) {
return
}
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
vnode.elm = oldVnode.elm
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
找到VNode和OldVnode对应的真实DOM,即el,判断Vnode和oldVnode是否指向同一节点,是就返回。如果两者都有文本节点且不相等,将el的文本节点设置为Vnode的文本节点。 如果oldVnode有子节点,Vnode没有,则删除el的子节点;如果oldVnode没有子节点,Vnode有,将Vnode的子节点真实化后添加到el;如果都有子节点,就调用updateChildren函数比较子节点。 updateChildren函数:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
比较子节点的过程,也就是调用了updateChildren函数之后: 将Vnode的子节点Vch和OldVnode的子节点oldch提取出来 vch和oldch都在首尾共设置了startIdx和endIdx变量,2个变量相互比较,若四种比较都未匹配,如果设置了key,利用key进行比较。比较过程中变量会向中间移,出现startIdx>endIdx则表示至少存在一个遍历完成,比较就结束了。 四种比较:(1)比较ns(new startIdx)和os(old startIdx):比较的时候都是用sameVnode来比较,如果返回true,也就是相同节点,调用patchVnode函数对比节点,然后两个指针都向后移。 (2)比较ne(new endIdx)和oe(old endIdx):和第一种相似,然后两个指针都向前移动。 (3)比较os(old startIdx)和ne(new startIdx):比较的时候也用sameVnode,如果返回true,就将os指向的节点移动到oe指向节点的后面,同时对该节点和ne指向的节点调用patchVnode函数对比节点,os的指针向后移动,ne的指针向前移动。 (4)比较oe和ns:和前面的一样 当出现ns>ne或者os>oe就结束比较。 代码实现:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
创建新增节点: 修改要更新的节点: 更新子节点的过程: 为列表渲染设置了key属性,可以标识一个节点唯一的id。Key这个属性主要用在vuejs的虚拟DOM算法中,在更新子节点时,需要从旧虚拟节点列表中查找与新虚拟节点相同的节点进行更新。如果在节点上设置了key属性,那么在oldchildren中找相同节点时,可以直接通过key拿到下标,获取节点。根本不用通过循环来查找节点。查找速度会快很多。
|