Gilt auf Warenkorb
# 1. 前言
在上一篇文章中,我们分析了Vue中Diff算法的核心原理,我们知道,Vue在更新过程中,对于新旧两棵虚拟DOM树会在同一层级进行比较,不会跨层级比较。在比较过程中,首先会对新旧两棵虚拟DOM树从根节点开始依次向下循环每一个节点,然后根据不同的节点类型执行不同的操作,其节点类型判断顺序为:**先判断是否是静态节点,然后判断是否是组件节点,然后判断是否是元素节点**。在上一篇文章中我们分析了静态节点和组件节点的情况,那么本篇文章就来分析元素节点的情况。
# 2. 新旧节点都是元素节点
当新旧节点都是元素节点时,那么会走patchVnode方法,在patchVnode方法中,首先会判断新旧节点是否是同一个节点,如果是,则直接返回,啥也不做;如果不是,则继续往下走,判断新旧节点是否是静态节点,如果是,则也直接返回,啥也不做;如果不是,则继续往下走,到这里,就要开始真正的Diff过程了。
真正的Diff过程分为三步:
1. 更新当前节点的属性;
2. 更新子节点;
3. 根据子节点的更新情况,递归更新父节点;
代码如下:
javascript
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 新旧vnode是完全相同的,直接返回
if (oldVnode === vnode) {
return
}
// 新的vnode是静态节点,直接返回
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 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 (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(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)
}
}
接下来,我们就来逐步分析这三步。
## 2.1 更新当前节点的属性
更新当前节点的属性操作比较简单,就是将新的vnode上的属性更新到真实DOM节点上,同时将旧vnode上已经不存在的属性从真实DOM节点上移除。
具体做法是:首先判断新的vnode上是否有data属性,如果有,则遍历data对象,将里面的属性更新到真实DOM节点上;然后判断旧的vnode上是否有data属性,如果有,则遍历data对象,将里面存在而新的vnode的data对象中不存在的属性从真实DOM节点上移除。
代码如下:
javascript
// 更新属性
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)
}
其中,cbs中定义了一些生命周期钩子,在patch过程中会调用这些钩子,cbs的定义如下:
javascript
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
const cbs = {}
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
可以看到,cbs中的update属性是一个数组,里面存放了一些更新属性的方法,这些方法来自于modules,modules的定义如下:
javascript
const modules = platformModules.concat(baseModules)
其中,platformModules是与平台相关的模块,baseModules是与平台无关的基础模块。在web平台下,platformModules的定义如下:
javascript
const platformModules = [
attrs,
klass,
events,
domProps,
style,
transition
]
baseModules的定义如下:
javascript
const baseModules = [
ref,
directives
]
这些模块中定义了各自在create、activate、update、remove、destroy等生命周期钩子中需要做的事情,这里我们就不展开分析了,有兴趣的同学可以自行查看源码。
## 2.2 更新子节点
更新完当前节点的属性后,接下来就要更新当前节点的子节点了。更新子节点分为以下几种情况:
1. 如果新的vnode没有文本内容(即vnode.text为undefined):
- 如果新的vnode和旧的vnode都有子节点,并且子节点不相同,则调用updateChildren方法更新子节点;
- 如果新的vnode有子节点,而旧的vnode没有子节点,则先清空旧vnode的文本内容(如果有的话),然后调用addVnodes方法添加新的子节点;
- 如果新的vnode没有子节点,而旧的vnode有子节点,则调用removeVnodes方法移除旧的子节点;
- 如果新的vnode和旧的vnode都没有子节点,但是旧的vnode有文本内容,则清空文本内容;
2. 如果新的vnode有文本内容,并且与旧的vnode的文本内容不相同,则更新文本内容。
代码如下:
javascript
// 更新子节点
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(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)
}
其中,updateChildren方法是Diff算法的核心,我们会在下一篇文章中详细分析。
## 2.3 递归更新父节点
在更新完当前节点的属性和子节点后,如果当前节点是组件节点,那么还会调用组件的postpatch钩子,这个钩子是在组件更新完成后调用的,可以用来执行一些后续操作。
代码如下:
javascript
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
# 3. 总结
本篇文章我们分析了Vue中Diff算法的元素节点更新过程,主要包括三个步骤:更新当前节点的属性、更新子节点、递归更新父节点。其中,更新子节点是Diff算法的核心,我们会在下一篇文章中详细分析。