vue中的dom-diff
虚拟dom的概念最早来自react,后面vue也引入了这一编程思想。在javascript中用js对象来描述一个DOM节点来实现。通常我们所说的虚拟dom比真实dom速度快,主要是体现在更新阶段,由于虚拟dom最终还是要转换成真实DOM,因子在创建阶段并不比真实DOM快,但更新阶段,由于我们改动DOM就会立即对DOM进行真实的增删改,而没有办法服复用已有DOM,真实的DOM上是有很多属性方法的,众所周知DOM操作是很大的开销。而虚拟dom可以让我们用js预先处理DOM,明确哪些DOM需要操作修改,减少了真实的DOM操作,因而速度快。
准备阶段
为了更好的看这个diff过程,我们先创建下面的数据,初始化的时候给一组数据,更新的时候我们更换顺序,新增和删除部分数据
var app = new Vue({
template: `<div>
<div v-for="user in userlist" :key="user.id">{{user.name}}</div>
<div>
<button @click="updateUserList">更新用户数据</button>
</div>
</div>`,
// app initial state
data: {
userlist: [
{
name: 'lisi',
id: 1
},
{
name: 'wangwu',
id: 2
},
{
name: 'zhaoliu',
id: 3
},
{
name: 'sunqi',
id: 4
}
]
},
methods: {
updateUserList() {
this.userlist = [
{
name: 'zhangsan',
id: 5
},
{
name: 'lisi',
id: 1
},
{
name: 'zhaoliu',
id: 3
},
{
name: 'wangwu333',
id: 2
},
{
name: 'sunqi444',
id: 6
}
]
}
}
})
// mount
app.$mount('#app')
debugger看dom-diff流程
下面先看一下debugger流程熟悉一下过程,在梳理其中的关键代码。断点直接打到下图位置,也可以从vue开始执行的位置一步步进入到这里。第一次没有prevVnode,进入patch主要是创建元素。
进入patch方法,进入createElm创建元素流程,这里会判断创建的元素是什么类型,元素、注释、文本操作上会不同。
patch流程走完会进入渲染流程,这里不继续展开了。下面看一下更新流程。
更新操作前面一章有介绍,会触发拦截器的set操作,通知对应的watcher进行更新。
会再次进入到update方法中,走更新流程
这里是diff算法的全部过程,后面详细介绍一下
在dom-diff过程中页面也就随着更新了,diff结束页面完全渲染成新的UI界面。
对应源码分析
下面详细看一下源码的位置以及对应代码的实现
// 文件位置 src/core/instance/lifecycle.ts
export function lifecycleMixin(Vue: typeof Component) {
// 这个方法前面也有介绍过,在走更新视图是会调用该方法
// 这次主要看dom-diff相关的patch部分,其他代码就省略了
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// 第一次进入prevVnode为空
if (!prevVnode) {
// 初始化渲染,基本上走的是patch创建流程
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 当更新数据需要再次更新视图时,会进入更新patch流程
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}
}
// 文件目录 src/platforms/web/runtime/index.ts
// 在运行时 如果是浏览器环境会将patch方法挂载上
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 文件目录 src/platforms/web/runtime/patch.ts
export const patch: Function = createPatchFunction({ nodeOps, modules })
// 文件目录 src/core/vdom/patch.ts
// createPatchFunction方法很大包含了很多vnode相关操作方法,
// 下面将方法名留着,先看一下结构,具体代码后面单独罗列
// 核心方法 createElm、createComponent、createChildren、updateChildren、patchVnode
// 主要返回了patch方法,这次重点先分析一下对应逻辑
export function createPatchFunction(backend) {
let i, j
const cbs: any = {}
// 这里依赖运行时,将运行时封装的创建元素方法引入进来
const { modules, nodeOps } = backend
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]])
}
}
}
function emptyNodeAt(elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
function createRmCb(childElm, listeners) {
...
}
function removeNode(el) {
...
}
function isUnknownElement(vnode, inVPre) {
...
}
let creatingElmInVPre = 0
function createElm(
vnode,
insertedVnodeQueue,
parentElm?: any,
refElm?: any,
nested?: any,
ownerArray?: any,
index?: any
) {
...
}
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
...
}
function initComponent(vnode, insertedVnodeQueue) {
...
}
function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
...
}
function insert(parent, elm, ref) {
...
}
function createChildren(vnode, children, insertedVnodeQueue) {
...
}
function isPatchable(vnode) {
...
}
function invokeCreateHooks(vnode, insertedVnodeQueue) {
...
}
// css设置样式作用域
function setScope(vnode) {
...
}
function addVnodes(
parentElm,
refElm,
vnodes,
startIdx,
endIdx,
insertedVnodeQueue
) {
...
}
function invokeDestroyHook(vnode) {
...
}
function removeVnodes(vnodes, startIdx, endIdx) {
...
}
function removeAndInvokeRemoveHook(vnode, rm?: any) {
...
}
// diff算法的核心逻辑
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
...
}
function checkDuplicateKeys(children) {
...
}
function findIdxInOld(node, oldCh, start, end) {
...
}
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
...
}
function hydrate(elm, vnode, insertedVnodeQueue, inVPre?: boolean) {
...
}
function assertNodeMatch(node, vnode, inVPre) {
...
}
// 提供给外部需要比对vnode调用的方法
return function patch(oldVnode, vnode, hydrating, removeOnly) {
//判断等于undefined或者null
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
// 如果oldVnode为undefined或者null,说明没有根元素
if (isUndef(oldVnode)) {
// 创建一个新的根元素
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 判断oldVnode是不是真实的元素
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 不是真实元素,oldVnode和vnode相同则进行patch操作
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 服务端渲染相关,先不细看
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (__DEV__) {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// 创建一个空的node进行替换
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 创建新的节点node
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)
)
// 这段代码的作用是递归地更新父级占位节点的元素(elm),并根据节点类型执行相关的生命周期钩子。
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
// clone insert hooks to avoid being mutated during iteration.
// e.g. for customed directives under transition group.
const cloned = insert.fns.slice(1)
for (let i = 0; i < cloned.length; i++) {
cloned[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
/*
1、key 相等
a.key === b.key
key 是区分节点的主要依据,通常用于优化列表渲染。
2、异步工厂函数相等
a.asyncFactory === b.asyncFactory
用于异步组件,确保是同一个异步组件工厂。
3、标签、注释、数据、类型一致
a.tag === b.tag:标签名一致(如都是 div)。
a.isComment === b.isComment:都是注释节点或都不是。
isDef(a.data) === isDef(b.data):data 都有定义或都没定义。
sameInputType(a, b):如果是 input 标签,还要判断 type 是否一致(比如 text 和 checkbox 不能复用)。
4、异步占位符特殊处理
isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)
如果 a 是异步占位符,并且 b 的异步工厂没有报错,也认为是同一个节点。
*/
function sameVnode(a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
)
}
// 把虚拟节点转化成dom
function createElm(
vnode,
insertedVnodeQueue,
parentElm?: any,
refElm?: any,
nested?: any,
ownerArray?: any,
index?: any
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 如果vnode有对应的DOM元素,并且属于某个数组
// 这时需要克隆vnode,避免后续操作影响到原有vnode,防止patch过程出错
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (__DEV__) {
if (data && data.pre) {
creatingElmInVPre++
}
// 开发环境警告,不认识的自定义组件
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' +
tag +
'> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
// 设置样式作用域
setScope(vnode)
递归创建子元素
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// create钩子(比如指令、事件)
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 插入到父节点
insert(parentElm, vnode.elm, refElm)
if (__DEV__ && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
// 注释节点
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 文本节点
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
dom-diff算法的核心内容,所有的diff对比算法都在下面这个方法中
// 高效的对比和更新父节点下的一组子节点,以最小的DOM操作将旧的vnode列表更新为新的vnode列表
// parentElm:父 DOM 元素
// oldCh:旧的 vnode 子节点数组
// newCh:新的 vnode 子节点数组
// insertedVnodeQueue:插入 vnode 时的回调队列
// removeOnly:仅移除标志,主要用于 <transition-group>
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
// 通过头尾指针法,分别维护新旧数组的头尾索引和节点。
// canMove 控制节点是否允许移动。
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
const canMove = !removeOnly
if (__DEV__) {
// 判断新节点key是否重复了
checkDuplicateKeys(newCh)
}
// 当旧节点的起始位置小于等于旧节点的结束位置
// 且新节点的起始位置小于等于新节点的结束位置
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 跳过被移动或置为 undefined 的节点。
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
// 头头:新旧头节点相同,直接 patch,指针右移
else if (sameVnode(oldStartVnode, newStartVnode)) {
// 主要patch内容和子节点,后面的操作一样就不说明了
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
// 尾尾:新旧尾节点相同,直接 patch,指针左移
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
// 头尾:旧头和新尾相同,patch 并将旧头节点移动到旧尾后面。
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]
}
// 尾头:旧尾和新头相同,patch 并将旧尾节点移动到旧头前面
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 {
if (isUndef(oldKeyToIdx))
// 查找新节点在旧节点中的位置
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 如果新头节点在旧节点中找不到(key 匹配),说明是新节点,直接创建。
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
// 如果找到了,patch 并移动到合适位置。
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 {
// key 相同但节点不同,视为新节点
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)
}
}
到这里vue中的dom-diff相关的流程和核心代码逻辑就看完了,真个dom-diff算法是很高效的,静态分析、diff算法都大大提升了执行效率。总结一下diff算法:主要通过新旧头头节点比较、新旧尾尾节点比较、新旧头尾节点比较、新旧尾头节点比较,将重复dom找到通过移动以达到复用,剩余不同的情况则查找新节点在旧节点中是否存在,存在就移动旧节点到新位置,不存在就创建;当上面逻辑走完还剩余的情况就是新节点还有剩余,遍历创建剩余的新节点,或者旧节点还有剩余,遍历删除旧的节点。