1、什么是虚拟DOM
- 虚拟DOM(Virtual DOM)是使用javaScript对象描述真实DOM
- vue.js中的虚拟DOM借鉴snabbdom,并添加了vue.js的特性,例如:指令和组件机制
2、为什么使用虚拟DOM
- 可以避免直接操作DOM,提高开发效率
- 作为一个中间层,可以跨平台
- 虚拟DOM不一定可以提高效率
- 首次渲染的时候会增加开销,在第一次渲染的时候,需要增加一个虚拟DOM
- 复杂视图情况下提升渲染性能,例如:diff算法进行新旧值比较;使key减少DOM更新次数
3、 vue初始化,渲染过程
vm._init()->vm.$mount()->mountComponent()->创建watcher->updateComponent()
- vm._update()(vm._render()),调用_render(),_undate()方法
4、 vm._render()结束,返回vnode
1. 地址:src/core/instance/render.js
- 调用用户传来render或者编译生成的render,如果是用户传入的render,调用vm.$createElement,模板编译的render使用vm._c
- call改变指向,vm._renderProxy是vue实例,vm.$createElement是h函数
vnode = render.call(vm._renderProxy,vm.$createElement)
2. 地址:src/core/instance/render.js
- vm.$createElement()——用户传入的render,调用createElement(vm,a,b,c,d,true)
// 用户传入的render会调用$createElement
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
3. 地址:src/core/instance/render.js
- vm._c()——编译生成的render,调用createElement(vm,a,b,c,d,false)
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
4. 地址: src/core/vdom/create-element.js
- createElement调用_createElement()
- _createElement()创建vnode对象,并返回vnode
// 创建VNode实例,传入,tag,data,children,context是vue实例
vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context)
5、_undate(vnode),返回的vnode传入——负责把虚拟dom渲染成真实dom
1. 地址:src/core/instance/lifecycle.js
- _update()主要通过preVnode来判断,没有就是初次渲染,有就是修改;首次执行,将
vm.$el
真实dom与vnode虚拟dom进行比较,并把结果放到$el中
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}
- 数据更新:将旧的vnode,之前渲染保存下来的preVnode与新的vnode进行比较,并存放到$el中
else {
// updates
// 数据更新
// 旧的vnode-prevVnode与最新的vnode比较,并存放到$el
vm.$el = vm.__patch__(prevVnode, vnode)
}
6、vm.__patch__()
——__patch__()
初始化
1. 地址:src/platforms/web/runtime/index.js
- 初始化
__patch()__
,相当于patch()函数
Vue.prototype.__patch__ = inBrowser ? patch : noop
2. 地址:src/platforms/web/runtime/patch.js
- patch函数主要传入两个参数:modules存储的是与web平台相关的模块;nodeOps操作dom的api
const modules = platformModules.concat(baseModules)
// nodeOps操作dom的api,platformModules是关于生命周期钩子函数,baseModules处理指令和ref
// modules存储的是与web平台相关的模块
export const patch: Function = createPatchFunction({ nodeOps, modules })
3. 地址:src/core/dom/patch.js
- createPatchFunction()返回patch函数
7、createPatchFunction()返回patch()函数——初始加载调用createElm,数据更新调用patchVnode()函数进行diff算法
1. 地址:src/core/dom/patch.js
- 挂载cbs节点的属性/事件/样式操作的钩子函数
- 判断第一个参数是真实dom还是vnode,首次加载第一个是真实dom,将真实dom转为vnode元素,并存储到oldVnode中,调用createElm,将vnode转为真实dom
- 如果是数据更新的时候,新旧节点如果key,tag相同即sameVnode相同,调用patchVnode进行diff算法
- 删除旧节点
export function createPatchFunction (backend) {
......
// 存储的是模块中的钩子函数
const cbs = {}
......
return function patch (oldVnode, vnode, hydrating, removeOnly) {
......
else {
// 获取oldVnode.nodeType,nodeType存在的话,就是真实dom,说明是第一次渲染
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// !!!patchVnode diff算法
// 比较新旧节点的差异
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 不是真实dom,oldVnode,node不是相同节点
// 判断是否是真实dom节点,是的话,说明第一次渲染
if (isRealElement) {
......
// emptyNodeAt把真实dom转为vnode元素,存储到oldVnode中
oldVnode = emptyNodeAt(oldVnode)
}
// elm是为了找parentElm,parentElm是为了将vnode转为真实dom,挂载到父元素上
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// createElm是为了将vnode转为真实dom,挂载到parentElm
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)
)
......
// 删除旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
......
}
}
}
}
8、createPatchFunction()中createElm(vnode,insertedVnodeQueue)——虚拟dom转为真实dom,并插入到父节点上(dom树)触发钩子函数
1. 地址:src/core/dom/patch.js
- 通过对应的tag判断是组件/标签/注释/文本,来转为对应的真实dom,createComponent()/createElement()/createComment()/createTextNode()
- 把虚拟dom的children转为真实dom,插入到dom树
9、createPatchFunction()中 patchVnode(oldVnode,vnode...)——比较新旧vnode及子节点的差异
1. 地址:src/core/dom/patch.js
- 比较新旧vnode,及新旧vnode的子节点的更新差异
- 如果新旧vnode都有子节点,并且子节点不同调用updateChildren(),对比子节点的差异
// 新老节点的子节点存在
if (isDef(oldCh) && isDef(ch)) {
// 对子节点进行diff操作,调用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
10、createPatchFunction()中 updateChildren()——4中方法比较子节点的patchVnode
1. 地址:src/core/dom/patch.js
- 新老节点的子节点传过来都是数组的形式,对比两个数组中的vnode,比较两者的差异
- 设置8个属性值:新老节点开始index,结束index,新老节点开始节点的值,结束节点的值
diff算法:
1. 两个数组没有遍历完时:oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
sameVnode仅仅判断key和tag相同,子节点跟文本是否相同不知道
- 开始开始:判断老节点和新节点的开始值是否相同,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
else if (sameVnode(oldStartVnode, newStartVnode)) {
// 判断老节点和新节点开始值是否相同,相同的话
// sameVnode仅仅判断key和tag相同,子节点跟文本是否相同不知道
// 调用patchVnode来判断子节点是否相同,判断完成比较下一个
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
- 结束结束:判断老节点和新节点结束值是否相同,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
else if (sameVnode(oldEndVnode, newEndVnode)) {
// 判断老节点和新节点结束值是否相同,相同的话
// 调用patchVnode来判断子节点是否相同,判断完成比较下一个
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
- 开始结束:将新的列表翻转,比较老的开始节点值与新的结束节点值,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 将列表翻转,比较老的开始节点值与新的结束节点值,如果相同
// 调用patchVnode比较子节点是否相同
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 把老的开始节点,移动到老的结束节点之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
- 结束开始:将老的列表翻转,比较老的结束节点值与新的开始节点值,相同的话,调用patchVnode来判断子节点是否相同,判断完成比较下一个
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 将老的列表翻转,比较老的结束节点值与新的开始节点值比较,如果相同
// 调用patchVnode比较子节点是否相同
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 把老的结束节点,移动到老的开始节点之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
2. 以上4中情况都不满足:以新节点为基础,从老节点中查找新节点
- 先找新节点的key和老节点的相同索引,如果没有找到再通过sameVnode找
else {
// 从新节点的开始获取一个,去老节点中查找相同节点
// 先找新开始节点的key和老节点的相同索引,如果没有找到再通过sameVnode找
// 把老节点的Key和索引存储到oldKeyToIdx中,然后如果新开始节点有key属性,查找老的节点中的索引
// 如果没有key,去老节点中findIdxInOld依次遍历找到老节点的索引
// 在这体现使用Key会快一点
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新节点对应的dom对象
// 并插入到老的开始节点对应的dom元素前边
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 如果找到新节点开始对应的老节点的索引,把对应老节点取出来存储到vnodeToMove
vnodeToMove = oldCh[idxInOld]
// 然后比较老节点的vnodeToMove与新节点的key,tag是否相同,如果相同的话
if (sameVnode(vnodeToMove, newStartVnode)) {
// 比较两个的子节点是否相同
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 将对应的老节点设为undefined
oldCh[idxInOld] = undefined
// 把对应的老的节点移动到老的开始节点之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
// 如果不相同,也就是key相同,tag不相同,创建createElm新的dom元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// 把下一个新的节点作为开始节点接着处理
newStartVnode = newCh[++newStartIdx]
}
3. 当遍历结束时,老的开始大于老的结束时,老节点遍历完,新节点还未遍历完
- 新几点比老节点多,把剩下的新节点批量插入到老节点后
if (oldStartIdx > oldEndIdx) {
// 说明新节点比老节点多,把剩下的新节点批量插入到新节点后面
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
4. 当遍历结束时,新节点遍历完,老节点还未
- 老节点比新节点多,把老节点剩余的批量删除
else if (newStartIdx > newEndIdx) {
// 新节点遍历完,老节点还未,说明新节点少于老节点,将老节点剩余的批量删除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
11、使用Key的优点:减少dom更新
例如:在a后加入x
data() {
return {
vnodeArr: ['a','b','c']
}
},
methods: {
handler() {
this.vnodeArr.splice(1,0,'x')
}
}
1. 没有使用key时
<ul>
<li v-for="item in vnodeArr">{{ item }}</li>
</ul>
<button @click="handler">添加x</button>
操作步骤:在updateChildren中开始断点
1. 第一轮循环:a——a
- oldStartIdx = 0,oldEndIdx = 2,newStartIdx = 0,newEndIdx = 3
- 老开始节点与新的开始节点比较,进入sameVnode,没有key,key都是undefined,相同;tag都是li,相同;sameVnode为true
- patchVnode比较,判断li中的内容(刚开始vnode.text为undefined),进入
if (isUndef(vnode.text))
,oldCh是数组vnode,ch也是数组vnode,if (oldCh !== ch)
为true,再次进入updateChildren -
sameVnode(oldStartVnode, newStartVnode)
,true - patchVnode比较,
else if (oldVnode.text !== vnode.text)
,两个text都是'a',不执行,所以不更新dom(当节点的内容没有发送变化时,不操作dom)
2. 第二轮循环:b——x
- oldStartIdx = 1,oldEndIdx = 2,newStartIdx = 1,newEndIdx = 3
- 下个节点比较,老节点b,新节点x,
sameVnode(oldStartVnode, newStartVnode)
,key = undefined,tag = li 相同, - patchVnode比较,再找到text,
else if (oldVnode.text !== vnode.text)
,文本不同会更新dom,此时页面中视图发生变化
3. 第三轮循环:c——b
- oldStartIdx = 2,oldEndIdx = 2,newStartIdx = 2,newEndIdx = 3
- 与第二轮循环一样,更新dom
4. 第四轮已遍历结束:——c
- oldStartIdx = 3,oldEndIdx = 2,newStartIdx = 3,newEndIdx = 3
-
if (oldStartIdx > oldEndIdx)
,新节点比老节点多,把剩下的新节点批量插入到老节点中addVnodes,更新dom
更新2次dom,一次插入dom,总共3次dom操作
2. 使用key
<ul>
<li v-for="item in vnodeArr" :key="item">{{ item }}</li>
</ul>
<button @click="handler">添加x</button>
1. 第一轮循环:a——a
- oldStartIdx = 0,oldEndIdx = 2,newStartIdx = 0,newEndIdx = 3
-
sameVnode(oldStartVnode, newStartVnode)
,key: a,a,tag: li,li,相同,为true - patchVnode比较,text文本一样为a,不更新dom
2. 第二轮循环:b——x->c——c
- oldStartIdx = 1,oldEndIdx = 2,newStartIdx = 1,newEndIdx = 3
-
sameVnode(oldStartVnode, newStartVnode)
,key: b,x,tag: li,li,不相同,为false -
sameVnode(oldEndVnode, newEndVnode)
,key: c,c,tag: li,li,相同,为true - patchVnode比较,text文本一样为c,不更新dom
- oldStartIdx = 1,oldEndIdx = 1,newStartIdx = 1,newEndIdx = 2
3. 第三轮循环:b——b
- oldStartIdx = 1,oldEndIdx = 1,newStartIdx = 1,newEndIdx = 2
-
sameVnode(oldEndVnode, newEndVnode)
,key: b,b,tag: li,li,相同,为true - patchVnode比较,text文本一样为b,不更新dom
- oldStartIdx = 1,oldEndIdx = 0,newStartIdx = 1,newEndIdx = 1
4. 第四轮已遍历结束:——x
-
if (oldStartIdx > oldEndIdx)
,新节点比老节点多,把剩下的新节点批量插入到老节点中addVnodes,更新dom
一次插入dom,总共1次dom操作