一、虚拟DOM
virtual DOM和真实DOM的区别?
virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。比如dom是这样的:
<div>
<p>123</p>
</div>
对应的virtual DOM:
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '123' }
]
};
渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。
我们先根据真实DOM生成一颗virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后Vnode和oldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode。
diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
二、 diff算法
1.传统diff
计算两颗树形结构差异并进行转换,传统diff算法是这样做的:循环递归每一个节点
比如左侧树a节点依次进行如下对比,左侧树节点b、c、d、e亦是与右侧树每个节点对比
算法复杂度能达到O(n^2),n代表节点的个数
a->e、a->d、a->b、a->c、a->a
查找完差异后还需计算最小转换方式,最终达到的算法复杂度是O(n^3)
2.vue优化的diff算法
在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。
<div>
<p>123</p>
</div>
<div>
<span>456</span>
</div>
上面的代码会分别比较同一层的两个div以及第二层的p和span,但是不会拿div和span作比较。如图:
三、vue中diff实现
vnode分类
- EmptyVNode: 没有内容的注释节点
- TextVNode: 文本节点
- ElementVNode: 普通元素节点
- ComponentVNode: 组件节点
- CloneVNode: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned属性为true
patch
patch函数的定义在src/core/vdom/patch.js中
// createPatchFunction的返回值,一个patch函数
export function createPatchFunction (backend) {
...
//hydration是bool类型,它表示是否直接使用服务器端渲染的DOM元素
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 如果vnode不存在但oldVnode存在,则表示要移除旧的node
// 那么就调用invokeDestroyHook(oldVnode)来进行销毁
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 如果oldVnode不存在,vnode存在,则创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
/// 是否为真实 DOM 元素
const isRealElement = isDef(oldVnode.nodeType)
// 如果oldVnode、vnode都存在且是相同节点就调用patchVnode处理去比较两个节点的差异
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
// 已有真实 DOM 元素,处理 oldVnode
if (isRealElement) {
// 如果存在真实的节点,存在data-server-rendered属性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
// 当旧的VNode是服务端渲染的元素,hydrating记为true
hydrating = true
}
// 需要用hydrate函数将虚拟DOM和真实DOM进行映射
if (isTrue(hydrating)) {
// 需要合并到真实DOM上
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
// 调用insert钩子
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
...
}
// 如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它
oldVnode = emptyNodeAt(oldVnode)
}
// 取代现有元素
// 将oldVnode设置为对应的虚拟dom,找到oldVnode.elm的父节点
// 根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm的位置
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 递归更新父级占位节点元素,
if (isDef(vnode.parent)) {
// 组件根节点被替换,遍历更新父节点element
let ancestor = vnode.parent
while (ancestor) {
ancestor.elm = vnode.elm
ancestor = ancestor.parent
}
if (isPatchable(vnode)) {
// 调用create回调
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode.parent)
}
}
}
// 销毁旧节点
if (isDef(parentElm)) {
// 移除老节点
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
// 调用destroy钩子
invokeDestroyHook(oldVnode)
}
}
}
// 调用insert钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回节点
return vnode.elm
}
}
patch函数接收6个参数:
- oldVnode: 旧的虚拟节点或旧的真实dom节点
- vnode: 新的虚拟节点
- hydrating: 是否要跟真实dom混合
- removeOnly: 特殊的flag,用于<transition-group>
- parentElm: 父节点
- refElm: 新节点将插入到refElm之前
代码逻辑
- 如果vnode不存在,但是oldVnode存在,说明是需要销毁旧节点,则调用invokeDestroyHook(oldVnode)来销毁oldVnode。
- 如果vnode存在,但是oldVnode不存在,说明是需要创建新节点,则调用createElm来创建新节点。
-
当vnode和oldVnode都存在时
- oldVnode和vnode是同一个节点,就调用patchVnode来进行patch
- 当vnode和oldVnode不是同一个节点时, 如果oldVnode是元素节点,需要用hydrate函数将虚拟dom和真是dom进行映射
- 如果oldVnode是真实节点时或vnode和oldVnode不是同一节点时,vnode替换oldVnode。如果组件根节点被替换,遍历更新父节点element。然后移除旧节点。
createElm 创建真实的 DOM 对象
- vnode 根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createElm方法
- insertedVnodeQueue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法
- 新节点将插入到refElm之前
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 当前节点已存在,直接修改可能产生错误,解决办法是复制一份
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested // for transition enter check
// 创建组件,在调用了组件初始化钩子之后,初始化组件,
// 并且重新激活组件。在重新激活组件中使用 insert 方法操作 DOM。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 错误检测,主要用于判断是否正确注册了component,这个错误还是比较常见
if (process.env.NODE_ENV !== 'production') {
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
)
}
}
// 1.创建当前dom节点
// nodeOps 封装的操作dom的合集
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode) // 用于为 scoped CSS 设置作用域 ID 属性
// weex处理
if (__WEEX__) {
...
} else {
// 2.创建子当前节点的子节点
// 用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法.
// 如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(...) 在真实 DOM 中插入文本内容。
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// insert 用于将元素插入真实 DOM 中
insert(parentElm, vnode.elm, refElm)
}
...
} else if (isTrue(vnode.isComment)) { // 3.创建注释dom节点
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else { // 4.创建文本dom节点
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
patchVnode
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新老 vnode 相等
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
}
// 如果新旧 vnode 为静态;新旧 vnode key相同;
// 新 vnode 是克隆所得;新 vnode 有 v-once 的属性
// 即 vnode 的 componentInstance 保持不变。
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
// 执行 data.hook.prepatch 钩子
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)) {
// 遍历调用 cbs.update 钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 执行 data.hook.update 钩子
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 旧 vnode 的 text 选项为 undefined
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新老节点的 children 不同,执行 updateChildren 方法。
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 旧 vnode children 不存在 执行 addVnodes 方法
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 新 vnode children 不存在 执行 removeVnodes 方法
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果新旧 vnode 都是 undefined,且老节点存在 text,清空文本
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新老节点文本不同,更新文本内容
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 执行 data.hook.postpatch 钩子,至此 patch 完成
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
代码逻辑
从分析patch方法中,我们知道当vnode和oldVnode都存在,并且vnode和oldVnode是同一节点时,才会调用patchVnode进行patch。
下面来看来patchVnode执行原理
- 如果oldVnode和vnode完全一致,则可认为没有变化,return;
- 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return;
- 如果oldVnode跟vnode都是静态节点(实例不会发生变化),且具有相同的key,并且当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其他操作,return;
-
否则,如果vnode不是文本节点或注释节点
- 如果vnode和oldVnode都有子节点并且两者的子节点不一致时,就调用updateChildren更新子节点
- 如果只有vnode有子节点,则调用addVnodes创建子节点
- 如果只有oldVnode有子节点,则调用removeVnodes把这些子节点都删除
- 如果vnode文本为undefined,则清空vnode.elm文本;
- 如果vnode是文本节点但是vnode.text != oldVnode.text时只需要更新vnode.elm的文本内容就可以。
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, 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
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} 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)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// parentElm的子元素oldEndVnode.elm后一个元素前(即oldEndVnode.elm所有的位置)插入oldStartVnode.elm
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)
// parentElm的子元素oldStartVnode.elm前插入oldEndVnode.elm
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 获取一个{key: index}关系表
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)
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
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(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
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)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
代码逻辑
- 定义初始变量
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] // 新列表终点值
- 定义循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
...
}
进行循环遍历,遍历条件为 oldStartIdx <= oldEndIdx 和 newStartIdx <= newEndIdx,在遍历过程中,oldStartIdx 和 newStartIdx 递增,oldEndIdx 和 newEndIdx 递减。当条件不符合跳出遍历循环
3.oldStartVnode、oldEndVnode 存在检测
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
如果oldStartVnode不存在,oldCh起始点向后移动。如果oldEndVnode不存在,oldCh终止点向前移动。
- oldStartVnode 和 newStartVnode 是 sameVnode
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
如果oldStartVnode 和 newStartVnode 是sameVnode,则patchVnode,同时彼此向后移动一位
- oldEndVnode 和 newEndVnode 是 sameVnode
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
如果oldEndVnode 和 newEndVnode 是sameVnode,则patchVnode,同时彼此向前移动一位
- oldStartVnode 和 newEndVnode 是 sameVnode
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
如果oldStartVnode 和 newEndVnode 是 sameVnode,则先 patchVnode,然后把oldStartVnode移到oldCh最后的位置即可,然后oldStartIdx向后移动一位,newEndIdx向前移动一位
- oldEndVnode 和 newStartVnode 是 sameVnode
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
如果oldEndVnode 和 newStartVnode 是 sameVnode,则先 patchVnode,然后把oldEndVnode移到oldCh最前的位置即可,然后newStartIdx向后移动一位,oldEndIdx向前移动一位
- 如果没有相同的 key,执行 createElm 方法创建元素。
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)
}
如果以上都不匹配,就尝试在oldCh中寻找跟newStartVnode具有相同key的节点,如果找不到相同key的节点,说明newStartVnode是一个新节点,就创建一个,然后把newStartVnode设置为下一个节点
- 如果有相同的 key,就判断这两个节点是否为sameNode
vnodeToMove = oldCh[idxInOld] // newStartVnode
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
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)
}
newStartVnode = newCh[++newStartIdx]
若为相同节点就调用patchVnode,把newStartVnode插入到oldStartVnode之前,newStartIdx继续向后移动。如果不是相同节点,需要执行 createElm创建新元素。
- 如果oldStartIdx > oldEndIdx,说明oldch先遍历完,执行 addVnodes 方法添加newStartIdx到newEnd
Idx的vnode
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
- 如果newStartIdx > newEndIdx,说明newCh先遍历完,执行 removeVnodes 方法移除oldStartIdx到oldEndIdx的vnode。
else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
key的作用
不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
举个例子:
我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的
有key的情况:
无key的情况:
https://www.cnblogs.com/wind-lanyan/p/9061684.html
https://github.com/muwoo/blogs/blob/master/src/Vue/11.md#4-oldstartvnode-%E5%92%8C-newstartvnode-%E6%98%AF-samevnode
https://www.jianshu.com/p/550c553143ef
https://www.jb51.net/article/129296.htm
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。