组件更新

在组件化章节,我们介绍了 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