首先说一下vdom和真实dom的区别
首先定义一个真实的结构
<div class="container" id="app">
<h1>虚拟dom</h1>
<ul style="color:red">
<li>第一项</li>
<li>第二项</li>
<li>第三项</li>
</ul>
</div>
对应的js结构---vdom
{
tag:'div',
props:{
id:'app',
class:'container'
},
children:[
{
tag:'h1',
children:'虚拟dom'
},
{
tag:'ul',
props:{style:'color:red'},
children:[
{
tag:'li',
children:'第一项'
},
{
tag:'li',
children:'第二项'
},
{
tag:'li',
children:'第三项'
}
]
}
]
}
为什么选中vdom
真实的操作dom会造成大量的重流和重绘。造成性能浪费。
虚拟dom是不会立即更新的,会先进行diff算法的比较在更新。所以真实的操作dom的次数相对减少很多。
所以diff算法是什么呢
diff算法
同级间进行比较
首先比较vnode是否相同,通过标签名和key值的比较。
两者相同(标签和key相同),开始进行下面比较
1.如果新的vnode是text,比较老的,如果老的有children。将旧的children移除。设置成新的text
2.如果新旧node简单的一方有子元素,将旧的node进行增删,并更新视图
3.如果新旧node都有子元素,会进行updateChildren的方法
首先进行四种比较
node的start和oldnode的start对比
node的end和oldnode的end对比
node的start和oldnode的end对比
node的end和oldnode的start对比
生成map的映射。根据old key 记录indexOld.如果indexOld存在,新旧节点相同,就移动旧节点到对应的地方,否则元素不同,就作为新的节点创建。如果indexOld不存在,就创建节点
最后进行遍历,如果老节点遍历完成,则新节点比老节点多将新节点多余的插入老节点,如果新节点遍历完成,则旧节点比新节点多,将多余的节点删除。
下面对应path函数中的方法,具体看下对应的过程。
初始化的时候oldVnode是没有的,所以会创建一个空的vnode,并关联element。
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
}
接下来如果oldvnode已经存在,判断新旧dom是否相同,进行pathVnode
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
接下来我们看下patchVnode对应的方法
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 两者相同的话就直接返回,不做任何的处理
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 将新的vnode的elm设置成oldvnode的elm,这样patch在更新的时候知道是哪一个dom需要更新。
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
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 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)
}
//vnode没有text的情况
if (isUndef(vnode.text)) {
// 新旧vnode有children的情况
if (isDef(oldCh) && isDef(ch)) {
//新旧节点有孩子的情况,执行updateChildren方法
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 新vnode有children,旧vnode没有children
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 旧vnode有text,将旧vnode中的内容删除,同事替换为新的vnode中的内容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 只有旧vnode有children,删除节点内容
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
// 新的vnode没内容,旧的是文本,直接删除文本
} 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)
}
}
我们在稍微复习下:
graph TD
A[patchVnode]--- B2[新vnode没有text]
B2 --- C1[新旧vnode有<br/>children的情况]
C1 ---D1[updateChildren]
B2 --- C2[新vnode有children,<br/>旧vnode没有children]
C2 --- D2[旧vnode有文本,<br/>删除文本]
C2 --- D3[旧vnode没有文本]
D2 --- E[新vnode的<br/>children替换 到旧vnode]
D3 --- E[新vnode的<br/>children替换到旧vnode]
B2 --- C3[新vnode没有children,<br/>旧vnode有children]
C3 --- D4[删除旧的vnode的children]
B2 --- C4[旧vnode只有文本]
C4 --- D5[删除旧vnode中内容]
B2 --- C5[两个都是文本<br/>节点,直接替换]
A[patchVnode]--- B3[两者都是文本<br/>节点,直接替换]
下面看下updateChildren的方法
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 首先定义新旧vnode上的起始位置
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, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 当进行比较的时候,索引进行移动,开始指针向右移动,结束指针向左移动。当两者交替位置循环结束。
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// oldStartVnode不存在
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// oldEndVnode不存在
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// 新旧开始相同(key和标签相等)
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 递归执行patchvnode,start指针向后移动,直到和end相同,结束循环。
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 新结束和旧结束相同
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 新结束和旧开始相同
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 新开始和旧结束相同
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 以上四种情况都不满足
} else {
//获取新newStartVnode中key对应旧children中有没有某个节点对应的当前的key,
//没有的话创建element;有的话,判断两个vnode是否相等,相等执行patchvnode更新、不相同直接创建新元素
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
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(oldCh, oldStartIdx, oldEndIdx)
}
}
所以这个函数主要执行了如下的方法:
将新旧节点中的起始点做对比,看是否匹配,如果都不满足。会进行如下key值的比较,如果旧vnode中有对应key的节点,判断两个vnode是否相等,相等执行patchvnode更新、不相同直接创建新元素。如果没有元素直接创建。最后key也不存在,会依次遍历看旧的vnode和新的vnode长度对比,进行插入活删除操作
如果不相同,直接创建新的dom元素。插入到对应的节点,删除oldnode
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 操作老的dom
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
vue从数据变化到视图的更新都发生了什么
首先数据的变化会通过objectdefine.prototype属性拦截对应数据的变化。订阅者会根据对应的数据的变化更新vdom,在oldvdom和vdom之间做diff算法,从而更新视图的变化。
那nexttick的作用呢?
vue中真实的dom的更新是异步的,数据发生改变,vue就会开启一个事件队列,同一个组件的watcher只会被放在事件队列一次,从而减少不必要的更新和dom操作。在下一个tick中执行对应的事件。同样的nextick中的回调是被放在事件队列中的,当完成dom的更新之后,就会去执行事件队列中的会掉方法