源码分析1:
diff的必要性,src\core\instance\lifecycle.jslifecycle.js - mountComponent()
组件中可能存在很多个data中的key使用
源码分析2:
diff的执行方式,src\core\vdom\patch.js - patchVnode()
patchVnode是diff发生的地方,整体策略:深度优先,同层比较
源码分析3:
diff的高效性,src\core\vdom\patch.js - updateChildren()
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
源码分析1:
diff的必要性,src\core\instance\lifecycle.jslifecycle.js - mountComponent()
组件中可能存在很多个data中的key使用
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 用户 $mount()时,定义 updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
/*
一个组件创建一次 Watcher, Wactcher的实例和组件的实例一一对应
但是一个组件可能存在多个 data 中的key的使用,
在更新时,为了确保知道是哪个 key 发生了变化,只能采用 diff 算法
这也是 diff 存在的必要性
*/
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
源码分析2:
执行方式,src\core\vdom\patch.js - patchVnode()
patchVnode是diff发生的地方,整体策略:深度优先,同层比较
// diff 算法
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)
}
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
// 属性更新 <div style="color: blue"> ==> <div style="color: red">
if (isDef(data) && isPatchable(vnode)) {
// cbs中关于属性更新的数组拿出来[attrFn, classFn, ...]
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(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)
}
}
源码分析3:
高效性,src\core\vdom\patch.js - updateChildren()
// 重排算法
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 4个指针
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) {
// 头尾指针调整
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
// 接下去是新旧数组头尾比较4种情况
// 新旧数组开头相同
else if (sameVnode(oldStartVnode, newStartVnode)) {
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]
}
// 上面4种猜想之后没有找到想同的,不得不开始循环查找
else {
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)
}
}
测试代码:
<body>
<div id="demo">
<h1>虚拟DOM</h1>
<p>{{foo}}</p>
</div>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: { foo: 'foo' },
mounted() {
setTimeout(() => {
this.foo = 'fooooo' }, 1000);
}
});
</script>
</body>
结论:
1.diff算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM作对比(即diff),将变化的地方更新在真 实DOM上;另外,也需要diff高效的执行对比过程,从而降低时间复杂度为O(n)。
2.vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,只有引入diff才能精确找到 发生变化的地方。
3.vue中diff执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果oldVnode和新的渲染 结果newVnode,此过程称为patch。
4.diff过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文 本节点做不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次比对尝试,如果 没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助key通常可以非 常精确找到相同节点,因此整个patch过程非常高效。