重学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。(另一个判断,如果遍历结束之后,还有新的开始节点大于新的结束节点,也就意味着旧的比新的长,就删除旧的多余的)
    网络异常,图片无法展示
    |
目录
相关文章
|
25天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
126 64
|
25天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
29 8
|
25天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
1月前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
2月前
|
缓存 JavaScript UED
Vue 的动态组件与 keep-alive
【10月更文挑战第19天】总的来说,动态组件和 `keep-alive` 是 Vue.js 中非常实用的特性,它们为我们提供了更灵活和高效的组件管理方式,使我们能够更好地构建复杂的应用界面。深入理解和掌握它们,以便在实际开发中能够充分发挥它们的优势,提升我们的开发效率和应用性能。
49 18
|
1月前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
28天前
|
JSON 算法 数据挖掘
基于图论算法有向图PageRank与无向图Louvain算法构建指令的方式方法 用于支撑qwen agent中的统计相关组件
利用图序列进行数据解读,主要包括节点序列分析、边序列分析以及结合节点和边序列的综合分析。节点序列分析涉及节点度分析(如入度、出度、度中心性)、节点属性分析(如品牌、价格等属性的分布与聚类)、节点标签分析(如不同标签的分布及标签间的关联)。边序列分析则关注边的权重分析(如关联强度)、边的类型分析(如管理、协作等关系)及路径分析(如最短路径计算)。结合节点和边序列的分析,如子图挖掘和图的动态分析,可以帮助深入理解图的结构和功能。例如,通过子图挖掘可以发现具有特定结构的子图,而图的动态分析则能揭示图随时间的变化趋势。这些分析方法结合使用,能够从多个角度全面解读图谱数据,为决策提供有力支持。
|
2月前
|
前端开发 UED
vue3知识点:Suspense组件
vue3知识点:Suspense组件
39 4
|
2月前
|
JavaScript 前端开发 测试技术
组件化开发:创建可重用的Vue组件
【10月更文挑战第21天】组件化开发:创建可重用的Vue组件
28 1