(二)面试题训练——虚拟DOM的Diff算法

163 阅读3分钟

面试中和VDOM一起考察的经常是Diff算法,以前看别人的面经可能搞得不是很清楚,这次想自己总结一下,看的是Vue源码中关于Diff的部分,在我之前也在我的Vue源码阅读系列中曾经提到过patch函数(四)小菜鸡的Vue源码阅读——Vue是如何编译解析模板的并挂载组件,但是没有深入,这一篇面试准备刚好可以补充到那里去,本篇写作前参考了(完整版)快速掌握虚拟DOM和diff算法【Vue】

0. _update

这个方法是将VDOM渲染成真实DOM的,这里面自然也就涉及到了一个初始化更新真实DOM两种情况,这里说明一下vue实例上挂载的一些属性

  • $el: 真实DOM
  • _vnode: 虚拟DOM
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    // 这个初始化的时候是mountComponent函数中赋值,是根DOM元素
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    vm._vnode = vnode
    if (!prevVnode) {
      // 第一次渲染,初始化,这个时候第一个参数是真实DOM
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 不是第一次渲染,更新
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // ...
  }

1.patch

__patch__方法在web/runtime中被注入,它是由一个函数构造器传入参数返回的,不过这和整个diff核心并不重要,重要的是看patch函数的实现

// platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

// platforms/web/runtime/patch.js
import { createPatchFunction } from 'core/vdom/patch'
export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction作为一个创建器函数,闭包了很多方法,然后返回一个patch函数

export function createPatchFunction (backend) {
    return function patch(){}
}

下面开始介绍主要的patch函数

  • createElm函数就是通过vnode生成真实DOM,然后挂载到vnode上elm属性上,且会通过父元素插入的到真实的DOM中去
  • oldVnode参数的类型为VNode|Element,第一次渲染的时候传的就是$el,是根DOM元素
  • 当前后的vnode都存在且为同类节点时才符合patch的条件
function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 新的vnode未定义说明是需要销毁了
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    // 旧vnode不存在
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      // 是原来的是虚拟DOM,且是同一个vnode,patch
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 原来是真实DOM
        if (isRealElement) {
          // ...
          // 创建一个空的Vnode,并把这个真实DOM挂载上去
          oldVnode = emptyNodeAt(oldVnode)
        }
        // 获取到当前真实DOM的父元素
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        // 创建新的DOM,并插入到真实的DOM
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // ...
        // remove原来的DOM
      }
    }
    return vnode.elm
  }

2.patchVnode

该函数通过判断新旧vnodechtext来判断

  • 最复杂的新旧都有ch,那么执行updateChildren来更新,这里也是diff的核心
  • 新有ch,旧有ch,那么增加
  • 新没有ch,旧有ch,那么移除
  • 如果text不一样,更新text 我们仔细发现if...else的判断和我们预期想的不一样,那是因为vuevdom生成中有一个算法就是,如果当前有ch,那么必然不会有text,因为text会合并到ch中去,所以多了这个条件,条件语句可以做一些优化,这种情况的出现大多数的博客没有做说明,我刚开始看的时候也很疑惑
 function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 没变化但就不需要patch了
    if (oldVnode === vnode) {
      return
    }
    // 把原来的真实DOM先拿到,因为新的VDOM的elm是undfined,我们需要知道原来的真实DOM渲染在哪
    const elm = vnode.elm = oldVnode.elm
    // 省略一些hook相关操作
    
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 根据新老节点的children判断
    
    // 大条件,新节点无text
    if (isUndef(vnode.text)) {
      // 新老节点都有ch
      if (isDef(oldCh) && isDef(ch)) {
        // 更新ch
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 新有,旧没有
        // 旧节点有text,则清除
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 增加新节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 旧有新没有
        // 旧有ch,也就意味着旧没有text
        // 移除旧的
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 新旧都没有ch,把文字清除
        nodeOps.setTextContent(elm, '')
      }
    // 新节点有text,必然意味着节点无ch
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    
    // hook相关
  }

3. updateChildren

  • 首先定义四个指针,和四个指针指向的节点,接下来的循环中,每一次指针移动的时候,他们指向的节点也会移动
    1. oldStartIdx:oldStartVnode
    2. newStartIdx:newStartVnode
    3. oldEndIdx:oldEndVnode
    4. newEndIdx:newEndVnode
  • 对新旧两个children做循环,循环终止的条件是oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,新旧两端的指针有一个重合了就结束
  • 每一次循环依次做五种比较,且如果比较成功就递归patchVnode,然后让指针往中间移动
    1. 旧头——新头
    2. 旧尾——新尾
    3. 旧头——新尾
    4. 旧尾——新头
    5. 比较新头指向的节点和旧节点中key是否有相同
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

    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, 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 {
            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)
    }
  }