VUE虚拟DOM——从出生到更新__Vue.js
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利
DOM树的概念(一个网页的呈现过程):
- 浏览器请求服务器获取页面HTML代码
- 浏览器在内存中,解析dom结构,并在浏览器内存中渲染出一颗dom树。
- 浏览器把dom树,呈现在页面上。
虚拟DOM的概念:
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用 的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
优点:
-
轻量,快速。当发生变换时新旧虚拟DOM比对,可以最小的操作真是Dom,从而提升性能
-
跨平台 :将虚拟dom更新转换为不同运行时特殊操作实现跨平台
-
兼容性:还可以加入兼容性代码增强操作的兼容性
虚拟DOM初始化
初始化代码线路:
// src/core/instance/lifecycle.js
export function mountComponent (
...
// 执行了更新函数
updateComponent = () => {
vm._update(vm._render(), hydrating)
})
vm._render() // 计算最新的虚拟DOM
vm._update() // 更新完成新的DOM
生成虚拟DOM
// src/core/instance/render.js
初始化/更新DOM
// src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
...
// 判断之情有没有虚拟dom 没有则是初始化
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}}
初始化 dom
// src/core/vdom/patch.js
// 进行dom树级别的比较
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
// 初始化过程创建新的dom,追加到body 删除宿主元素
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
...
}
// 将虚拟dom直接创建成真实dom 如下图
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)
}
到此,初始化过程结束。
虚拟DOM更新(diff算法)
代码调用线路
// src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
...
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
prevVnode与vnode进行比较
// src/core/vdom/patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果新dom不存在 则删除老dom if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false const insertedVnodeQueue = []
// 老dom 树不存在 新增
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) } else {
const isRealElement = isDef(oldVnode.nodeType)
// 存在新旧dom 则进行diff 算法
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 俗称打补丁
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else{
// 初始化
...
}
... } }
}
patchVnode 进行dom树形比较
-
新new dom 不存在 就删除dom 树
-
老节点没有直接新增dom 树
-
都存在 diff算法进行比对 (深度优先,同级比较)
// src/core/vdom/patch.js function patchVnode oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 查找新旧节点是否存在子节点 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(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) }
}
updateChildren 进行重排
首先会有四种假设:
-
两两开头交叉比较:相同调用patchVnode()同级则指针往后对比;
-
两两结尾交叉比较:相同调用patchVnode()同级指针往前比;
-
老的开头和新的结尾相同,把老的开头移动的队尾;
-
老的结尾和新的开投相同,把老的开头移动的队首。
// src/core/vdom/patch.js function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 四个指针 let oldStartIdx = 0 let oldEndIdx = oldCh.length - 1 let newStartIdx = 0 let newEndIdx = newCh.length - 1 // 四个节点 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] ... // 重排算法(预定的四种假设) // 循环条件 开始的索隐 不能大于结束索引 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { ... // 四种假设 // 开始节点相同,索引向后 移动一位 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] } // 以上假设都不满足 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) }
}
以上就是虚拟dom更新时的对比。
欢迎各位大神指点。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 半梦半醒 原文链接:https://juejin.im/post/6866322545226809358