浅析Vue源码(九)——VirtualDOM与path

栏目: JavaScript · 发布时间: 6年前

内容简介:在未出现双向绑定之前,我们需要在各个触发事件方法中直接操作DOM节点来达到修改相应视图的目的。但是当应用一大就会变得难以维护,reflow(回流)很影响性能的。因此就有人提出来,那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢?于是虚拟DOM出现了,它是真实DOM的一层抽象,用属性描述真实DOM的各个特性。当它发生变化的时候,就会去修改视图。可以想象,如果用最简单粗暴的方法将整个DOM结构用innerHTML修改到页面

在未出现双向绑定之前,我们需要在各个触发事件方法中直接操作DOM节点来达到修改相应视图的目的。但是当应用一大就会变得难以维护,reflow(回流)很影响性能的。

因此就有人提出来,那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢?于是虚拟DOM出现了,它是真实DOM的一层抽象,用属性描述真实DOM的各个特性。当它发生变化的时候,就会去修改视图。

可以想象,如果用最简单粗暴的方法将整个DOM结构用innerHTML修改到页面上,那么这样进行重绘整个视图层是相当消耗性能的,那是不是可以考虑每次只更新它修改的部分呢?所以Vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树,以VNode节点模拟真实DOM,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实DOM,只需要操作JavaScript对象后只对差异修改,相对于整块的innerHTML的粗暴式修改,大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的DOM操作,大大提高了性能。

Vue就使用了这样的抽象节点VNode,它是对真实DOM的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至是node平台也可以对这样一棵抽象DOM树进行创建删除修改等操作,这也为前后端同构提供了可能。

具体VNode的细节可以看 浅析Vue源码(七)——render到VNode的生成

如何修改视图呢?

前文已经介绍了Vue是通过数据绑定来修改视图的,当某个数据被修改的时候,set方法会让闭包中的Dep调用notify通知所有订阅者Watcher,Watcher通过get方法执行vm._update(vm._render(), hydrating)。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    /*如果已经该组件已经挂载过了则代表进入这个步骤是个更新的过程,触发beforeUpdate钩子*/
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    /*基于后端渲染Vue.prototype.__patch__被用来作为一个入口*/
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    /*更新新的实例对象的__vue__*/
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
复制代码

update方法的第一个参数是一个VNode对象,在内部会将该VNode对象与之前旧的VNode对象进行__patch_。

那究竟什么是path?

path

patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘。patch的核心在于diff算法,这套算法可以高效地比较virtual DOM的变更,得出变化以修改视图。

那么patch如何工作的呢?

首先说一下patch的核心diff算法,diff算法是通过 同层的树节点 进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。

浅析Vue源码(九)——VirtualDOM与path
浅析Vue源码(九)——VirtualDOM与path

这两张图代表旧的VNode与新VNode进行patch的过程,他们只是在 同层级的VNode 之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。

通过前面的介绍,我们知道需要将VNode转换成真实的DOMe节点,需要通过patch函数来实现:

vm.$el = vm.__patch__(prevVnode, vnode)
复制代码

而__patch__是在platforms/web/runtime/index.js中定义的:

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
复制代码

这里主要是为了判断当前环境是否是在浏览器环境中,也就是是否存在Window对象。这里也是为了做跨平台的处理,如果是在server render环境,那么patch就是一个空操作。 那接下来我们来看看path源码(src/core/vdom/patch.js)。

/*createPatchFunction的返回值,一个patch函数*/
return function patch (oldVnode, vnode, hydrating, removeOnly) {
/*vnode不存在则直接调用销毁钩子*/
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      /*oldVnode未定义的时候,其实也就是root节点,创建一个新的节点*/
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
    /*标记旧的VNode是否有nodeType*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        /*是同一个节点的时候直接修改现有的节点*/
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        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)) {
          /*当旧的VNode是服务端渲染的元素,hydrating记为true*/
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
          /*需要合并到真实DOM上*/
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
             /*调用insert钩子*/
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
           /*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        /*取代现有元素*/
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        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)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
        /*组件根节点被替换,遍历更新父节点element*/
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
            /*调用create回调*/
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
        /*移除老节点*/
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
        /*调用destroy钩子*/
          invokeDestroyHook(oldVnode)
        }
      }
    }
    /*调用insert钩子*/
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
复制代码

这里通过createPatchFunction函数,来创建返回一个patch函数。path接收6个参数:

1.oldVnode: 旧的虚拟节点或旧的真实dom节点

2.vnode: 新的虚拟节点

3.hydrating: 是否要跟真实dom合并

4.removeOnly: 特殊flag,用于组件

5.parentElm:父节点

6.refElm: 新节点将插入到refElm之前

具体解析看代码注释~抛开调用生命周期钩子和销毁就节点不谈,我们发现代码中的关键在于sameVnode、 createElm 和 patchVnode 方法。

sameVnode

我们来看一下sameVnode的实现。

/*
  判断两个VNode节点是否是同一个节点,需要满足以下条件
  key相同
  tag(当前节点的标签名)相同
  isComment(是否为注释节点)相同
  是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义
  当标签是<input>的时候,type必须相同
*/
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

// Some browsers do not support dynamically changing type for <input>
// so they need to be treated as different nodes
/*
  判断当标签是<input>的时候,type是否相同
  某些浏览器不支持动态修改<input>类型,所以他们被视为不同类型
*/
function sameInputType (a, b) {
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
复制代码

createElm

function createElm (vnode,insertedVnodeQueue,parentElm, refElm,nested,ownerArray,index) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    // 用于创建组件,在调用了组件初始化钩子之后,初始化组件,并且重新激活组件。
  // 在重新激活组件中使用 insert 方法操作 DOM
    vnode.isRootInsert = !nested // for transition enter check
    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
          )
        }
      }
      // nodeOps 封装的操作dom的合集
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)
       // weex处理
      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
      // 用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法.
      // 如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(...) 在真实 DOM 中插入文本内容。
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // insert 用于将元素插入真实 DOM 中
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {// 注释
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else { // 文本
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
复制代码

通过以上的注释,我们可以知道:createElm 方法的最终目的就是创建真实的 DOM 对象

patchVnode

还是先来看一下patchVnode的代码。

/*patch VNode节点*/
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
/*两个VNode节点相同则直接返回*/
    if (oldVnode === vnode) {
      return
    }

    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
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    /*
      如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
      并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
      那么只需要替换elm以及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
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    /*i = data.hook.prepatch,如果存在的话,见"./create-component componentVNodeHooks"。*/
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
     /*调用update回调以及update钩子*/
      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)
    }
     /*如果这个VNode节点没有text文本时*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
      /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
      /*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
      /*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
       /*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
     /*当新老节点text不一样时,直接替换这段文本*/
      nodeOps.setTextContent(elm, vnode.text)
    }
    /*调用postpatch钩子*/
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
复制代码

patchVnode的规则是这样的:

1.如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。

2.新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。

3.如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。

4.当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。

5.当新老节点都无子节点的时候,只是文本的替换。

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

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    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)) {
      /*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/
        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)
        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)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        /*
          生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)
          比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  
          结果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
         /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
        /*获取同key的老节点*/
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
          /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
             /*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
            oldCh[idxInOld] = undefined
            /*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
    /*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
    /*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
复制代码

让我们来画张图屡一下大致的流程:

浅析Vue源码(九)——VirtualDOM与path

可能你看到这还是云里雾里有点理不清,没关系,接下来我们一点一点来消化:

浅析Vue源码(九)——VirtualDOM与path

定义初始变量:

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] // 新列表终点值
复制代码

首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。

索引与VNode节点的对应关系: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode

在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。

首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2*2=4种比较方法。

当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。

浅析Vue源码(九)——VirtualDOM与path

如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。

这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。

浅析Vue源码(九)——VirtualDOM与path

如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。

这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。

浅析Vue源码(九)——VirtualDOM与path

如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。

浅析Vue源码(九)——VirtualDOM与path

当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。

浅析Vue源码(九)——VirtualDOM与path

到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。

1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。

浅析Vue源码(九)——VirtualDOM与path

2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。

浅析Vue源码(九)——VirtualDOM与path

总结

到这里,patch的主要功能也基本讲完了,我们发现,在本篇中,大量出现了一个key字段。经过上面的调研,其实我们已经知道Vue的diff算法中其核心是基于两个简单的假设:

1.两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构

2.同一层级的一组节点,他们可以通过唯一的id进行区分 基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n),当页面的数据发生变化时,Diff算法只会比较同一层级的节点:

浅析Vue源码(九)——VirtualDOM与path

所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

感谢 染陌老师 提供的素材。

要是喜欢的话可以给我一个star, github


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C++Templates中文版

C++Templates中文版

David Vandevoorde、Nicolai M.Josuttis / 陈伟柱 / 人民邮电出版社 / 2008-2 / 69.00元

本书是C++模板编程的完全指南,旨在通过基本概念、常用技巧和应用实例3方面的有用资料,为读者打下C++模板知识的坚实基础。 全书共22章。第1章全面介绍了本书的内容结构和相关情况。第1部分(第2~7章)以教程的风格介绍了模板的基本概念,第2部分(第8~13章)阐述了模板的语言细节,第3部分(第14~18章)介绍了C++模板所支持的基本设计技术,第4部分(第19~22章)深入探讨了各种使用模板......一起来看看 《C++Templates中文版》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具