5

一、虚拟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算法是这样做的:循环递归每一个节点
image.png
比如左侧树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]
    }
  }

代码逻辑

  1. 定义初始变量
 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] // 新列表终点值

  1. 定义循环
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终止点向前移动。

  1. oldStartVnode 和 newStartVnode 是 sameVnode
else if (sameVnode(oldStartVnode, newStartVnode)) {
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}

如果oldStartVnode 和 newStartVnode 是sameVnode,则patchVnode,同时彼此向后移动一位

  1. oldEndVnode 和 newEndVnode 是 sameVnode
else if (sameVnode(oldEndVnode, newEndVnode)) {
  patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  oldEndVnode = oldCh[--oldEndIdx]
  newEndVnode = newCh[--newEndIdx]
}

如果oldEndVnode 和 newEndVnode 是sameVnode,则patchVnode,同时彼此向前移动一位

  1. 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向前移动一位

  1. 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向前移动一位

  1. 如果没有相同的 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设置为下一个节点

  1. 如果有相同的 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创建新元素。

  1. 如果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)
}

  1. 如果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
无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

bigtooth
26 声望2 粉丝

« 上一篇
mysql入门
下一篇 »
vue总结