重学Vue【组件更新和diff算法】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


在前面分析过了Vue的组件的创建过程,并没有说到当组件数据发生变化会发生什么以及如何更新组件,本篇过一下组件更新的细节。

前面讲响应式对象的时候提到,当数据发生变化的时候,会触发渲染 watcher 的回调函数,进而执行组件的更新过程,在 mountComponent 方法中有这样一段:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

通过渲染 watcher 的回调走一个 updateComponent 方法(作为渲染 watcher 的getter传入的),通过 vm._render 拿到vnode,最终组件的更新会调用 vm._update 方法,updateComponent 方法会在 nextTick 之后重新执行:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  // ...
  const prevVnode = vm._vnode
  const preActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  if (!prevVnode) {
     // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  // ...
}

在执行更新的时候这个 preVnode 是存在的,因为在第一次执行 _update 的时候,就把 vnode 赋值给了 vm._vnode,所以下面的逻辑会走到 vm.$el = vm.__patch__(prevVnode, vnode) 里,它仍然会调用 patch 函数,它在 src/core/vdom/patch.js 中:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  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
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    } else {
      if (isRealElement) {
         // ...
      }
      // 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)) {
        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) {
            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)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

这里注意一下,和创建过程不一样的是,此时的参数 oldVnodevnode 都是有值的,来看下它的逻辑:因为这两个参数都有值,所以 isUndef(vnode)isUndef(oldVnode) 是不会走的,接着会走到 !isRealElement && sameVnode(oldVnode, vnode) 判断,由于 oldVnodevnode 都是VNode类型,接下来通过 sameVNode(oldVnode, vnode) 判断它们俩是否是相同的VNode,从而决定走哪个逻辑:

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

sameVnode 里判定,如果两个 vnodekey 不相等(就是平常写的v-for的key),那它们就不一样;否则继续判断对于同步组件的话,就判断 isCommentdatainput 类型是否相同,而对于异步组件,就判断 asyncFactory 是否相同。

所以,新旧 vnode 是否为 sameVnode ,会走到不同的更新逻辑。


新旧节点不同

对于新旧节点不同的情况,本质上就是替换已存在的节点,代码中通过注释就可以看出来分为三步。

1. 创建新节点

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)
)

通过旧节点 oldVnode 拿到旧的DOM oldVnode.elm,然后拿到旧DOM节点的父DOM节点 parentElm,然后把新的 vnode 节点和DOM节点 parentElm 传入,这样新节点 vnode 就知道要挂载到哪个父节点上,通过 createElm 就可以创建一个新的DOM,createElm 在讲解 patch 的时候有分析过它。


2. 更新父的占位节点

// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
  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) {
      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
  }
}

找到当前 vnode 的父占位符节点 vnode.parent,这里的 vnode 是一个渲染 vnode,这个 vnode.parent 其实之前也有提到过,在 render 过程中会将 vnode.parent = _parentVnode,也就是 render 生成的渲染 vnodeparent 指向父的 _parentVnode,也就是指向父占位符节点。

接着如果定义了父占位符节点,就保存到 ancestor 里,然后通过 isPatchable 判断当前的 vnode 是不是一个可挂载的节点:

function isPatchable (vnode) {
  while (vnode.componentInstance) {
    vnode = vnode.componentInstance._vnode
  }
  return isDef(vnode.tag)
}

如果 vnode.componentInstance 存在,就说明 vnode 是一个组件 vnode,也就是占位符vnode,只有组件 vnode 才有 componentInstance 属性。如果传进来的 vnode 既是一个组件 vnode,也是一个渲染 vnode,就会不断while循环,找到一个不是组件 vnode 的真实渲染节点 _vnode (也就是组件中的根vnode),判断是否有tag,如果有tag,说明它是可以被挂载的。

拿到可挂载节点之后,遍历执行 destroy 的钩子函数,然后把占位符DOM节点 ancestor.elm 重新指向新的 vnode.elm 节点,这个节点是在第一步 createElm 生成的,这样相当于做了一次更新。

接着判断如果当前占位符是一个可挂载的节点,就执行 create 钩子函数等操作。

如果占位符节点同时也是一个渲染 vnode 的话(组件的根节点又是一个组件,即引用了一个子组件),就向上查找,直到找到一个可挂载的占位符节点,再做同样的逻辑。


3. 删除旧节点

// destroy old node
if (isDef(parentElm)) {
  removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}

oldVnode 从当前DOM中删除,如果父节点存在,就执行 removeVnodes,否则就是父节点被删掉了,就执行 invokeDestroyHook

function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx]
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else { // Text node
        removeNode(ch.elm)
      }
    }
  }
}
function removeAndInvokeRemoveHook (vnode, rm) {
  if (isDef(rm) || isDef(vnode.data)) {
    let i
    const listeners = cbs.remove.length + 1
    if (isDef(rm)) {
      // we have a recursively passed down rm callback
      // increase the listeners count
      rm.listeners += listeners
    } else {
      // directly removing
      rm = createRmCb(vnode.elm, listeners)
    }
    // recursively invoke hooks on child component root node
    if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
      removeAndInvokeRemoveHook(i, rm)
    }
    for (i = 0; i < cbs.remove.length; ++i) {
      cbs.remove[i](vnode, rm)
    }
    if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
      i(vnode, rm)
    } else {
      rm()
    }
  } else {
    removeNode(vnode.elm)
  }
}
function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}


新旧节点相同

如果是新旧节点相同的话,就会调用 patchVNode 方法:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  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.
  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(oldVnode, vnode)
  }
  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 (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(elm, 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)
  }
}

patchVnode 的作用就是把新的 vnode patch 到旧的 vnode 上,这个稍微有点复杂,大致分为四步。


1. prepatch 更新 vm 实例绑定的属性

首先因为新旧节点相同,所以拿到的 elm 也都是一样的,跳过不重要的逻辑,到后面的 const data = vnode.data,拿到 vnodedata 后,判断 datadata.hook 以及 prepatch,如果这三个都成立,那么 vnode 就是一个组件 vnode,就会执行 prepatch 方法:

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
}

prepatch 就是拿到新的 vnode 的组件配置和组件的实例,去执行 updateChildComponent 方法:

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = true
  }
  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren
  const hasChildren = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slots
    vm.$scopedSlots !== emptyObject // has old scoped slots
  )
  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render
  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren
  // update $attrs and $listeners hash
  // these are also reactive so they may trigger child update if the child
  // used them during render
  vm.$attrs = parentVnode.data.attrs || emptyObject
  vm.$listeners = listeners || emptyObject
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
  // update listeners
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)
  // resolve slots + force update if has children
  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = false
  }
}

updateChildComponent 就是对子组件进行更新,因为更新了 vnode,那对应的 vnode 的实例 vm 的一系列属性也都会发生变化,包括占位符 vm.$vnodeslotlistenersprops 的更新等。


2. 执行 update 钩子函数

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

获取新旧节点的 children,如果是一个组件 vnode 的话,children 就是 undefined,如果 children 有的话,vnode 就是一个普通节点。接着做判断,如果data存在,并且 vnode 是可挂载的,就执行一堆 update 钩子函数和用户自定义的 update 钩子函数。


3. 完成 patch 流程

if (isUndef(vnode.text)) {
  if (isDef(oldCh) && isDef(ch)) {
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  } else if (isDef(ch)) {
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  } else if (isDef(oldCh)) {
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  } else if (isDef(oldVnode.text)) {
    nodeOps.setTextContent(elm, '')
  }
} else if (oldVnode.text !== vnode.text) {
  nodeOps.setTextContent(elm, vnode.text)
}

如果 vnode 是个文本节点(text),并且新旧文本不相同,就直接替换文本,如果不是文本节点,就判断子节点:

  1. oldChch 都存在且不相同时,使用 updateChildren 函数来更新子节点,updateChildren 是Vue的diff精华所在,最后面通过一个例子来分析它。
  2. 只有 ch 的话,说明旧节点不再需要了,如果旧的节点是文本节点就先将文本移除,然后通过 addVnodesch 插入新节点 elm 下。
  3. 如果只有 oldCh 存在,说明更新的是个空节点,需要将旧的节点通过 removeVnodes 全部移除。
  4. 如果旧节点是文本节点,就清除节点文本内容。


4. 执行 postatch 函数

if (isDef(data)) {
  if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}

patch 过程执行结束后,会执行 postpatch 钩子函数,它是组件自定义的钩子函数,有就执行。


组件更新总结

组件更新的过程其实就是新旧节点比对,不同的话就执行:创建新节点 -> 更新父占位节点 -> 删除旧节点,相同的话就执行:prepatch更新vm实例属性 -> 获取children,执行update -> patch进行文本替换 -> 执行postpatch。

如果新旧节点相同,并且都存在子节点就执行 updateChildren 这个方法,下面来分析一下。


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)) {
      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 {
      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)
          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(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

这里举个例子来看下会比较易懂:

<template>
  <div id="app">
    <div>
      <ul>
        <li v-for="item in items" :key="item.id">{{ item.val }}</li>
      </ul>
    </div>
    <button @click="change">change</button>
  </div>
</template>
<script>
  export default {
    name: 'App',
    data() {
      return {
        items: [
          {id: 0, val: 'A'},
          {id: 1, val: 'B'},
          {id: 2, val: 'C'},
          {id: 3, val: 'D'}
        ]
      }
    },
    methods: {
      change() {
        this.items.reverse().push({id: 4, val: 'E'})
      }
    }
  }
</script>

例子中遍历一个 items 数组,然后点击按钮,数组会翻转,并且末尾新增一个对象,那么对应到 updateChildren 里的参数的话就如下图:

网络异常,图片无法展示
|

现在开始diff比对:

  1. 第一次比对,会执行到 sameVnode(oldEndVnode, newStartVnode),也就是旧的末尾和新的开头是同一个,就执行 nodeOps.insertBefore,将旧的末尾插入到旧的开头,然后旧的末尾下标自减一个,新的开头下标自增一个,就变成了:D A B C
    网络异常,图片无法展示
    |

  2. 第二次比对,会执行到 sameVnode(oldEndVnode, newStartVnode),此时和第一步一样,然后旧的末尾下标自减一个,新的开头下标自增一个,就变成了:D C A B
    网络异常,图片无法展示
    |

  3. 第三次比对,会执行到 sameVnode(oldEndVnode, newStartVnode),此时和第一步一样,继续旧的末尾下标自减一个,新的开头下标自增一个,就变成了:D C B A
    网络异常,图片无法展示
    |

  4. 第四次比对,此时新旧的开头都是 A 了,所以执行 sameVnode(oldStartVnode, newStartVnode),然后旧的和新的开头下标都自增一个,旧的顺序就修改完毕了。
    网络异常,图片无法展示
    |

  5. 第五次比对,while 循环就不满足了,因为 oldStartIdx 大于了 oldEndIdx,所以循环结束,执行下面的逻辑,最终形成: D C B A E
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(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
复制代码
  1. 找到参考节点,把剩下的节点(newStartIdxnewEndIdx 之间的节点)通过 addVnodes 插入进去,图例上也就是 E。(另一个判断,如果遍历结束之后,还有新的开始节点大于新的结束节点,也就意味着旧的比新的长,就删除旧的多余的)
    网络异常,图片无法展示
    |
目录
相关文章
|
3天前
|
JavaScript 数据库
ant design vue日期组件怎么清空 取消默认当天日期
ant design vue日期组件怎么清空 取消默认当天日期
|
3天前
|
JavaScript C++
vue高亮显示组件--转载
vue高亮显示组件--转载
8 0
|
3天前
|
JavaScript 前端开发 开发者
Vue的事件处理机制提供了灵活且强大的方式来响应用户的操作和组件间的通信
【5月更文挑战第16天】Vue事件处理包括v-on(@)指令用于绑定事件监听器,如示例中的按钮点击事件。事件修饰符如.stop和.prevent简化逻辑,如阻止表单默认提交。自定义事件允许组件间通信,子组件通过$emit触发事件,父组件用v-on监听并响应。理解这些机制有助于掌握Vue应用的事件控制。
16 4
|
2天前
|
JavaScript 前端开发 UED
Vue class和style绑定:动态美化你的组件
Vue class和style绑定:动态美化你的组件
|
3天前
|
JavaScript
vue封装面包屑组件
vue封装面包屑组件
7 0
|
3天前
|
JavaScript 前端开发 API
深入理解vue组件生命周期,你一定要看到最后,最后有深入探讨
深入理解vue组件生命周期,你一定要看到最后,最后有深入探讨
|
3天前
|
JavaScript Go
VUE3+vite项目中动态引入组件和异步组件
VUE3+vite项目中动态引入组件和异步组件
|
3天前
|
缓存 算法 JavaScript
vue中组件保活<keep-alive>的使用
vue中组件保活<keep-alive>的使用
|
3天前
|
JavaScript 前端开发 UED
教你用vue自定义指令做一个组件的遮罩层loading效果
教你用vue自定义指令做一个组件的遮罩层loading效果
|
3天前
|
存储 JavaScript 前端开发
用Vue写一个简单好看的菜单组件(Vue实战系列)
用Vue写一个简单好看的菜单组件(Vue实战系列)