VUE虚拟DOM——从出生到更新__Vue.js
发布于 4 年前 作者 banyungong 1802 次浏览 来自 分享
粉丝福利 : 关注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

回到顶部