• 组件更新
    • 新旧节点不同
    • 新旧节点相同
    • updateChildren
    • 总结

    组件更新

    在组件化章节,我们介绍了 Vue 的组件化实现过程,不过我们只讲了 Vue 组件的创建过程,并没有涉及到组件数据发生变化,更新组件的过程。而通过我们这一章对数据响应式原理的分析,了解到当数据发生变化的时候,会触发渲染 watcher 的回调函数,进而执行组件的更新过程,接下来我们来详细分析这一过程。

    1. updateComponent = () => {
    2. vm._update(vm._render(), hydrating)
    3. }
    4. new Watcher(vm, updateComponent, noop, {
    5. before () {
    6. if (vm._isMounted) {
    7. callHook(vm, 'beforeUpdate')
    8. }
    9. }
    10. }, true /* isRenderWatcher */)

    组件的更新还是调用了 vm._update 方法,我们再回顾一下这个方法,它的定义在 src/core/instance/lifecycle.js 中:

    1. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    2. const vm: Component = this
    3. // ...
    4. const prevVnode = vm._vnode
    5. if (!prevVnode) {
    6. // initial render
    7. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    8. } else {
    9. // updates
    10. vm.$el = vm.__patch__(prevVnode, vnode)
    11. }
    12. // ...
    13. }

    组件更新的过程,会执行 vm.$el = vm.patch(prevVnode, vnode),它仍然会调用 patch 函数,在 src/core/vdom/patch.js 中定义:

    1. return function patch (oldVnode, vnode, hydrating, removeOnly) {
    2. if (isUndef(vnode)) {
    3. if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    4. return
    5. }
    6. let isInitialPatch = false
    7. const insertedVnodeQueue = []
    8. if (isUndef(oldVnode)) {
    9. // empty mount (likely as component), create new root element
    10. isInitialPatch = true
    11. createElm(vnode, insertedVnodeQueue)
    12. } else {
    13. const isRealElement = isDef(oldVnode.nodeType)
    14. if (!isRealElement && sameVnode(oldVnode, vnode)) {
    15. // patch existing root node
    16. patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    17. } else {
    18. if (isRealElement) {
    19. // ...
    20. }
    21. // replacing existing element
    22. const oldElm = oldVnode.elm
    23. const parentElm = nodeOps.parentNode(oldElm)
    24. // create new node
    25. createElm(
    26. vnode,
    27. insertedVnodeQueue,
    28. // extremely rare edge case: do not insert if old element is in a
    29. // leaving transition. Only happens when combining transition +
    30. // keep-alive + HOCs. (#4590)
    31. oldElm._leaveCb ? null : parentElm,
    32. nodeOps.nextSibling(oldElm)
    33. )
    34. // update parent placeholder node element, recursively
    35. if (isDef(vnode.parent)) {
    36. let ancestor = vnode.parent
    37. const patchable = isPatchable(vnode)
    38. while (ancestor) {
    39. for (let i = 0; i < cbs.destroy.length; ++i) {
    40. cbs.destroy[i](ancestor)
    41. }
    42. ancestor.elm = vnode.elm
    43. if (patchable) {
    44. for (let i = 0; i < cbs.create.length; ++i) {
    45. cbs.create[i](emptyNode, ancestor)
    46. }
    47. // #6513
    48. // invoke insert hooks that may have been merged by create hooks.
    49. // e.g. for directives that uses the "inserted" hook.
    50. const insert = ancestor.data.hook.insert
    51. if (insert.merged) {
    52. // start at index 1 to avoid re-invoking component mounted hook
    53. for (let i = 1; i < insert.fns.length; i++) {
    54. insert.fns[i]()
    55. }
    56. }
    57. } else {
    58. registerRef(ancestor)
    59. }
    60. ancestor = ancestor.parent
    61. }
    62. }
    63. // destroy old node
    64. if (isDef(parentElm)) {
    65. removeVnodes(parentElm, [oldVnode], 0, 0)
    66. } else if (isDef(oldVnode.tag)) {
    67. invokeDestroyHook(oldVnode)
    68. }
    69. }
    70. }
    71. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    72. return vnode.elm
    73. }

    这里执行 patch 的逻辑和首次渲染是不一样的,因为 oldVnode 不为空,并且它和 vnode 都是 VNode 类型,接下来会通过 sameVNode(oldVnode, vnode) 判断它们是否是相同的 VNode 来决定走不同的更新逻辑:

    1. function sameVnode (a, b) {
    2. return (
    3. a.key === b.key && (
    4. (
    5. a.tag === b.tag &&
    6. a.isComment === b.isComment &&
    7. isDef(a.data) === isDef(b.data) &&
    8. sameInputType(a, b)
    9. ) || (
    10. isTrue(a.isAsyncPlaceholder) &&
    11. a.asyncFactory === b.asyncFactory &&
    12. isUndef(b.asyncFactory.error)
    13. )
    14. )
    15. )
    16. }

    sameVnode 的逻辑非常简单,如果两个 vnodekey 不相等,则是不同的;否则继续判断对于同步组件,则判断 isCommentdatainput 类型等是否相同,对于异步组件,则判断 asyncFactory 是否相同。

    所以根据新旧 vnode 是否为 sameVnode,会走到不同的更新逻辑,我们先来说一下不同的情况。

    新旧节点不同

    如果新旧 vnode 不同,那么更新的逻辑非常简单,它本质上是要替换已存在的节点,大致分为 3 步

    • 创建新节点
    1. const oldElm = oldVnode.elm
    2. const parentElm = nodeOps.parentNode(oldElm)
    3. // create new node
    4. createElm(
    5. vnode,
    6. insertedVnodeQueue,
    7. // extremely rare edge case: do not insert if old element is in a
    8. // leaving transition. Only happens when combining transition +
    9. // keep-alive + HOCs. (#4590)
    10. oldElm._leaveCb ? null : parentElm,
    11. nodeOps.nextSibling(oldElm)
    12. )

    以当前旧节点为参考节点,创建新的节点,并插入到 DOM 中,createElm 的逻辑我们之前分析过。

    • 更新父的占位符节点
    1. // update parent placeholder node element, recursively
    2. if (isDef(vnode.parent)) {
    3. let ancestor = vnode.parent
    4. const patchable = isPatchable(vnode)
    5. while (ancestor) {
    6. for (let i = 0; i < cbs.destroy.length; ++i) {
    7. cbs.destroy[i](ancestor)
    8. }
    9. ancestor.elm = vnode.elm
    10. if (patchable) {
    11. for (let i = 0; i < cbs.create.length; ++i) {
    12. cbs.create[i](emptyNode, ancestor)
    13. }
    14. // #6513
    15. // invoke insert hooks that may have been merged by create hooks.
    16. // e.g. for directives that uses the "inserted" hook.
    17. const insert = ancestor.data.hook.insert
    18. if (insert.merged) {
    19. // start at index 1 to avoid re-invoking component mounted hook
    20. for (let i = 1; i < insert.fns.length; i++) {
    21. insert.fns[i]()
    22. }
    23. }
    24. } else {
    25. registerRef(ancestor)
    26. }
    27. ancestor = ancestor.parent
    28. }
    29. }

    我们只关注主要逻辑即可,找到当前 vnode 的父的占位符节点,先执行各个 moduledestroy 的钩子函数,如果当前占位符是一个可挂载的节点,则执行 modulecreate 钩子函数。对于这些钩子函数的作用,在之后的章节会详细介绍。

    • 删除旧节点
    1. // destroy old node
    2. if (isDef(parentElm)) {
    3. removeVnodes(parentElm, [oldVnode], 0, 0)
    4. } else if (isDef(oldVnode.tag)) {
    5. invokeDestroyHook(oldVnode)
    6. }

    oldVnode 从当前 DOM 树中删除,如果父节点存在,则执行 removeVnodes 方法:

    1. function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
    2. for (; startIdx <= endIdx; ++startIdx) {
    3. const ch = vnodes[startIdx]
    4. if (isDef(ch)) {
    5. if (isDef(ch.tag)) {
    6. removeAndInvokeRemoveHook(ch)
    7. invokeDestroyHook(ch)
    8. } else { // Text node
    9. removeNode(ch.elm)
    10. }
    11. }
    12. }
    13. }
    14. function removeAndInvokeRemoveHook (vnode, rm) {
    15. if (isDef(rm) || isDef(vnode.data)) {
    16. let i
    17. const listeners = cbs.remove.length + 1
    18. if (isDef(rm)) {
    19. // we have a recursively passed down rm callback
    20. // increase the listeners count
    21. rm.listeners += listeners
    22. } else {
    23. // directly removing
    24. rm = createRmCb(vnode.elm, listeners)
    25. }
    26. // recursively invoke hooks on child component root node
    27. if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
    28. removeAndInvokeRemoveHook(i, rm)
    29. }
    30. for (i = 0; i < cbs.remove.length; ++i) {
    31. cbs.remove[i](vnode, rm)
    32. }
    33. if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
    34. i(vnode, rm)
    35. } else {
    36. rm()
    37. }
    38. } else {
    39. removeNode(vnode.elm)
    40. }
    41. }
    42. function invokeDestroyHook (vnode) {
    43. let i, j
    44. const data = vnode.data
    45. if (isDef(data)) {
    46. if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    47. for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    48. }
    49. if (isDef(i = vnode.children)) {
    50. for (j = 0; j < vnode.children.length; ++j) {
    51. invokeDestroyHook(vnode.children[j])
    52. }
    53. }
    54. }

    删除节点逻辑很简单,就是遍历待删除的 vnodes 做删除,其中 removeAndInvokeRemoveHook 的作用是从 DOM 中移除节点并执行 moduleremove 钩子函数,并对它的子节点递归调用 removeAndInvokeRemoveHook 函数;invokeDestroyHook 是执行 moduledestory 钩子函数以及 vnodedestory 钩子函数,并对它的子 vnode 递归调用 invokeDestroyHook 函数;removeNode 就是调用平台的 DOM API 去把真正的 DOM 节点移除。

    在之前介绍组件生命周期的时候提到 beforeDestroy & destroyed 这两个生命周期钩子函数,它们就是在执行 invokeDestroyHook 过程中,执行了 vnodedestory 钩子函数,它的定义在 src/core/vdom/create-component.js 中:

    1. const componentVNodeHooks = {
    2. destroy (vnode: MountedComponentVNode) {
    3. const { componentInstance } = vnode
    4. if (!componentInstance._isDestroyed) {
    5. if (!vnode.data.keepAlive) {
    6. componentInstance.$destroy()
    7. } else {
    8. deactivateChildComponent(componentInstance, true /* direct */)
    9. }
    10. }
    11. }
    12. }

    当组件并不是 keepAlive 的时候,会执行 componentInstance.$destroy() 方法,然后就会执行 beforeDestroy & destroyed 两个钩子函数。

    新旧节点相同

    对于新旧节点不同的情况,这种创建新节点 -> 更新占位符节点 -> 删除旧节点的逻辑是很容易理解的。还有一种组件 vnode 的更新情况是新旧节点相同,它会调用 patchVNode 方法,它的定义在 src/core/vdom/patch.js 中:

    1. function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    2. if (oldVnode === vnode) {
    3. return
    4. }
    5. const elm = vnode.elm = oldVnode.elm
    6. if (isTrue(oldVnode.isAsyncPlaceholder)) {
    7. if (isDef(vnode.asyncFactory.resolved)) {
    8. hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    9. } else {
    10. vnode.isAsyncPlaceholder = true
    11. }
    12. return
    13. }
    14. // reuse element for static trees.
    15. // note we only do this if the vnode is cloned -
    16. // if the new node is not cloned it means the render functions have been
    17. // reset by the hot-reload-api and we need to do a proper re-render.
    18. if (isTrue(vnode.isStatic) &&
    19. isTrue(oldVnode.isStatic) &&
    20. vnode.key === oldVnode.key &&
    21. (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    22. ) {
    23. vnode.componentInstance = oldVnode.componentInstance
    24. return
    25. }
    26. let i
    27. const data = vnode.data
    28. if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    29. i(oldVnode, vnode)
    30. }
    31. const oldCh = oldVnode.children
    32. const ch = vnode.children
    33. if (isDef(data) && isPatchable(vnode)) {
    34. for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    35. if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    36. }
    37. if (isUndef(vnode.text)) {
    38. if (isDef(oldCh) && isDef(ch)) {
    39. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    40. } else if (isDef(ch)) {
    41. if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    42. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    43. } else if (isDef(oldCh)) {
    44. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    45. } else if (isDef(oldVnode.text)) {
    46. nodeOps.setTextContent(elm, '')
    47. }
    48. } else if (oldVnode.text !== vnode.text) {
    49. nodeOps.setTextContent(elm, vnode.text)
    50. }
    51. if (isDef(data)) {
    52. if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    53. }
    54. }

    patchVnode 的作用就是把新的 vnodepatch 到旧的 vnode 上,这里我们只关注关键的核心逻辑,我把它拆成四步骤:

    • 执行 prepatch 钩子函数
    1. let i
    2. const data = vnode.data
    3. if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    4. i(oldVnode, vnode)
    5. }

    当更新的 vnode 是一个组件 vnode 的时候,会执行 prepatch 的方法,它的定义在 src/core/vdom/create-component.js 中:

    1. const componentVNodeHooks = {
    2. prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    3. const options = vnode.componentOptions
    4. const child = vnode.componentInstance = oldVnode.componentInstance
    5. updateChildComponent(
    6. child,
    7. options.propsData, // updated props
    8. options.listeners, // updated listeners
    9. vnode, // new parent vnode
    10. options.children // new children
    11. )
    12. }
    13. }

    prepatch 方法就是拿到新的 vnode 的组件配置以及组件实例,去执行 updateChildComponent 方法,它的定义在 src/core/instance/lifecycle.js 中:

    1. export function updateChildComponent (
    2. vm: Component,
    3. propsData: ?Object,
    4. listeners: ?Object,
    5. parentVnode: MountedComponentVNode,
    6. renderChildren: ?Array<VNode>
    7. ) {
    8. if (process.env.NODE_ENV !== 'production') {
    9. isUpdatingChildComponent = true
    10. }
    11. // determine whether component has slot children
    12. // we need to do this before overwriting $options._renderChildren
    13. const hasChildren = !!(
    14. renderChildren || // has new static slots
    15. vm.$options._renderChildren || // has old static slots
    16. parentVnode.data.scopedSlots || // has new scoped slots
    17. vm.$scopedSlots !== emptyObject // has old scoped slots
    18. )
    19. vm.$options._parentVnode = parentVnode
    20. vm.$vnode = parentVnode // update vm's placeholder node without re-render
    21. if (vm._vnode) { // update child tree's parent
    22. vm._vnode.parent = parentVnode
    23. }
    24. vm.$options._renderChildren = renderChildren
    25. // update $attrs and $listeners hash
    26. // these are also reactive so they may trigger child update if the child
    27. // used them during render
    28. vm.$attrs = parentVnode.data.attrs || emptyObject
    29. vm.$listeners = listeners || emptyObject
    30. // update props
    31. if (propsData && vm.$options.props) {
    32. toggleObserving(false)
    33. const props = vm._props
    34. const propKeys = vm.$options._propKeys || []
    35. for (let i = 0; i < propKeys.length; i++) {
    36. const key = propKeys[i]
    37. const propOptions: any = vm.$options.props // wtf flow?
    38. props[key] = validateProp(key, propOptions, propsData, vm)
    39. }
    40. toggleObserving(true)
    41. // keep a copy of raw propsData
    42. vm.$options.propsData = propsData
    43. }
    44. // update listeners
    45. listeners = listeners || emptyObject
    46. const oldListeners = vm.$options._parentListeners
    47. vm.$options._parentListeners = listeners
    48. updateComponentListeners(vm, listeners, oldListeners)
    49. // resolve slots + force update if has children
    50. if (hasChildren) {
    51. vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    52. vm.$forceUpdate()
    53. }
    54. if (process.env.NODE_ENV !== 'production') {
    55. isUpdatingChildComponent = false
    56. }
    57. }

    updateChildComponent 的逻辑也非常简单,由于更新了 vnode,那么 vnode 对应的实例 vm 的一系列属性也会发生变化,包括占位符 vm.$vnode 的更新、slot 的更新,listeners 的更新,props 的更新等等。

    • 执行 update 钩子函数
    1. if (isDef(data) && isPatchable(vnode)) {
    2. for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    3. if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    4. }

    回到 patchVNode 函数,在执行完新的 vnodeprepatch 钩子函数,会执行所有 moduleupdate 钩子函数以及用户自定义 update 钩子函数,对于 module 的钩子函数,之后我们会有具体的章节针对一些具体的 case 分析。

    • 完成 patch 过程
    1. const oldCh = oldVnode.children
    2. const ch = vnode.children
    3. if (isDef(data) && isPatchable(vnode)) {
    4. for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    5. if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    6. }
    7. if (isUndef(vnode.text)) {
    8. if (isDef(oldCh) && isDef(ch)) {
    9. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    10. } else if (isDef(ch)) {
    11. if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    12. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    13. } else if (isDef(oldCh)) {
    14. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    15. } else if (isDef(oldVnode.text)) {
    16. nodeOps.setTextContent(elm, '')
    17. }
    18. } else if (oldVnode.text !== vnode.text) {
    19. nodeOps.setTextContent(elm, vnode.text)
    20. }

    如果 vnode 是个文本节点且新旧文本不相同,则直接替换文本内容。如果不是文本节点,则判断它们的子节点,并分了几种情况处理:

    • oldChch 都存在且不相同时,使用 updateChildren 函数来更新子节点,这个后面重点讲。
      2.如果只有 ch 存在,表示旧节点不需要了。如果旧的节点是文本节点则先将节点的文本清除,然后通过 addVnodesch 批量插入到新节点 elm 下。

    3.如果只有 oldCh 存在,表示更新的是空节点,则需要将旧的节点通过 removeVnodes 全部清除。

    4.当只有旧节点是文本节点的时候,则清除其节点文本内容。

    • 执行 postpatch 钩子函数
    1. if (isDef(data)) {
    2. if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    3. }

    再执行完 patch 过程后,会执行 postpatch 钩子函数,它是组件自定义的钩子函数,有则执行。

    那么在整个 pathVnode 过程中,最复杂的就是 updateChildren 方法了,下面我们来单独介绍它。

    updateChildren

    1. function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    2. let oldStartIdx = 0
    3. let newStartIdx = 0
    4. let oldEndIdx = oldCh.length - 1
    5. let oldStartVnode = oldCh[0]
    6. let oldEndVnode = oldCh[oldEndIdx]
    7. let newEndIdx = newCh.length - 1
    8. let newStartVnode = newCh[0]
    9. let newEndVnode = newCh[newEndIdx]
    10. let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    11. // removeOnly is a special flag used only by <transition-group>
    12. // to ensure removed elements stay in correct relative positions
    13. // during leaving transitions
    14. const canMove = !removeOnly
    15. if (process.env.NODE_ENV !== 'production') {
    16. checkDuplicateKeys(newCh)
    17. }
    18. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    19. if (isUndef(oldStartVnode)) {
    20. oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    21. } else if (isUndef(oldEndVnode)) {
    22. oldEndVnode = oldCh[--oldEndIdx]
    23. } else if (sameVnode(oldStartVnode, newStartVnode)) {
    24. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
    25. oldStartVnode = oldCh[++oldStartIdx]
    26. newStartVnode = newCh[++newStartIdx]
    27. } else if (sameVnode(oldEndVnode, newEndVnode)) {
    28. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
    29. oldEndVnode = oldCh[--oldEndIdx]
    30. newEndVnode = newCh[--newEndIdx]
    31. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    32. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
    33. canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
    34. oldStartVnode = oldCh[++oldStartIdx]
    35. newEndVnode = newCh[--newEndIdx]
    36. } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    37. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
    38. canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
    39. oldEndVnode = oldCh[--oldEndIdx]
    40. newStartVnode = newCh[++newStartIdx]
    41. } else {
    42. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    43. idxInOld = isDef(newStartVnode.key)
    44. ? oldKeyToIdx[newStartVnode.key]
    45. : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    46. if (isUndef(idxInOld)) { // New element
    47. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    48. } else {
    49. vnodeToMove = oldCh[idxInOld]
    50. if (sameVnode(vnodeToMove, newStartVnode)) {
    51. patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
    52. oldCh[idxInOld] = undefined
    53. canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    54. } else {
    55. // same key but different element. treat as new element
    56. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    57. }
    58. }
    59. newStartVnode = newCh[++newStartIdx]
    60. }
    61. }
    62. if (oldStartIdx > oldEndIdx) {
    63. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    64. addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    65. } else if (newStartIdx > newEndIdx) {
    66. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    67. }
    68. }

    updateChildren 的逻辑比较复杂,直接读源码比较晦涩,我们可以通过一个具体的示例来分析它。

    1. <template>
    2. <div id="app">
    3. <div>
    4. <ul>
    5. <li v-for="item in items" :key="item.id">{{ item.val }}</li>
    6. </ul>
    7. </div>
    8. <button @click="change">change</button>
    9. </div>
    10. </template>
    11. <script>
    12. export default {
    13. name: 'App',
    14. data() {
    15. return {
    16. items: [
    17. {id: 0, val: 'A'},
    18. {id: 1, val: 'B'},
    19. {id: 2, val: 'C'},
    20. {id: 3, val: 'D'}
    21. ]
    22. }
    23. },
    24. methods: {
    25. change() {
    26. this.items.reverse().push({id: 4, val: 'E'})
    27. }
    28. }
    29. }
    30. </script>

    当我们点击 change 按钮去改变数据,最终会执行到 updateChildren 去更新 li 部分的列表数据,我们通过图的方式来描述一下它的更新过程:

    第一步:组件更新 - 图1

    第二步:组件更新 - 图2

    第三步:组件更新 - 图3

    第四步:组件更新 - 图4

    第五步:组件更新 - 图5

    第六步:组件更新 - 图6

    总结

    组件更新的过程核心就是新旧 vnode diff,对新旧节点相同以及不同的情况分别做不同的处理。新旧节点不同的更新流程是创建新节点->更新父占位符节点->删除旧节点;而新旧节点相同的更新流程是去获取它们的 children,根据不同情况做不同的更新逻辑。最复杂的情况是新旧节点相同且它们都存在子节点,那么会执行 updateChildren 逻辑,这块儿可以借助画图的方式配合理解。

    原文: https://ustbhuangyi.github.io/vue-analysis/reactive/component-update.html