vue源码解读十六: 组件更新(diff算法)

166 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途。

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

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

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

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

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

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  ... 
  let isInitialPatch = false
  const insertedVnodeQueue = []
  // 组件第一次渲染oldVnode为空
  if (isUndef(oldVnode)) {
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    // 如果vnode的tag key等一样,说明是同样的vnode,那么就进行子节点的比较
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    } else {
      // 如果vnode 的tag  key等不同,说明是不同的vnode,直接根据新的vnode创建真实dom即可,子节点都不用继续比较,因为父节点都不一样了
      if (isRealElement) {
         // ...
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // vnode.parent表示该组件的占位符vnode,现在因为组件修改了,所以也要更改它的占位符vnode
      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
          ....
          ancestor = ancestor.parent
        }
      }

      // 删除旧的真实dom
      if (isDef(parentElm)) {
        removeVnodes(parentElm, [oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

这里执行 patch 的逻辑和首次渲染是不一样的,因为 oldVnode 不为空,并且它和 vnode 都是 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 的逻辑非常简单,如果两个 vnode 的 tag 和 key 不相等,则是不同的;否则继续判断对于同步组件,则判断 isCommentdatainput 类型等是否相同,对于异步组件,则判断 asyncFactory 是否相同。

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

新旧节点不同

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

  • 创建新节点

此时会把新的节点插入到原来的节点中,那么在界面上就会同时显示新旧两个节点内容。

const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
  vnode,
  insertedVnodeQueue,
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)
  • 更新组件的占位符节点

找到当前 vnode 的父的占位符节点,先执行各个 module 的 destroy 的钩子函数,如果当前占位符是一个可挂载的节点,则执行 module 的 create 钩子函数。对于这些钩子函数的作用,在之后的章节会详细介绍。

  • 删除旧节点

前面在节点中添加了新的内容,那么需要把就的节点内容删除掉。

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

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

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)) {
    ...
    for (i = 0; i < cbs.remove.length; ++i) {
      cbs.remove[i](vnode, rm)
    }
    ...
  // 如果删除的这个节点是组件占位符节点,就会递归删除组件节点
  if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
      removeAndInvokeRemoveHook(i, 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])
    }
  }
}

删除节点逻辑很简单,就是遍历待删除的 vnodes 做删除,其中 removeAndInvokeRemoveHook 的作用是从 DOM 中移除节点并执行 module 的 remove 钩子函数,并对它的子节点递归调用 removeAndInvokeRemoveHook 函数;这种情况出现在如下代码中:

<div id="app">
  <MyTest v-if="flag" />
  <div v-else>pengchangjun</div>
  <button @click="change">change</button>
</div>

change() {
   this.flag = !this.flag
}

当改变flag的时候,就会比对children,发现节点的key不相等,一个tag是div,一个tag是vue-component-2-mytest,所以会走到removeAndInvokeRemoveHook逻辑,在里面发现是一个组件的占位符节点,那么递归调用removeAndInvokeRemoveHook删除子组件节点。

invokeDestroyHook 是执行 module 的 destory 钩子函数以及 vnode 的 destory 钩子函数,并对它的子 vnode 递归调用 invokeDestroyHook 函数;removeNode 就是调用平台的 DOM API 去把真正的 DOM 节点移除。

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

const componentVNodeHooks = {
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

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

下面看一个例子,当我在父组件改变flag的值后,MyTest会经历销毁到重建的过程:

// app.vue
<template>
  <div id="app" :key="flag">
    <MyTest />
    <button @click="change">change</button>
  </div>
</template>

<script>
change() {
  this.flag = !this.flag
}
</script>

// MyTest.vue
<template>
  <ul>
    <li>1</li>
    <li>1</li>
    <li>1</li>
    <li>1</li>
  </ul>
</template>
export default {
  name: 'MyTest',
  ...
  mounted() {
    console.log('mounted')
  },
  destroyed() {
    console.log('destroyed')
  }
}

当在父组件改变flag的值后,MyTest组件会先执行destroyed钩子,然后再执行mounted钩子,也就说明当父节点不一样的话,即使子节点一样也会全部销毁后重建。

新旧节点相同

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

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  if (oldVnode === vnode) {
    return
  }

  const elm = vnode.elm = oldVnode.elm
  ...

  let i
  // 这段逻辑主要是针对组件的占位符vnode,因为占位符vnode有data.hook
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  // 新旧节点的children
  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)) {
      // 如果都存在chilren,则调用updateChildren,其实这个函数也是递归调用patchVnode
      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 上,这里我们只关注关键的核心逻辑,我把它拆成四步骤:

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

当更新的 vnode 是一个组件 vnode 的时候,会执行 prepatch 的方法(因为children除了普通的vnode,还有组件vnode),它的定义在 src/core/vdom/create-component.js 中:

image.png

const componentVNodeHooks = {
  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
    )
  }
}

vnode.componentOptions有如下的值,里面有新vnode的最新props值,这个值会导致子组件会重新渲染。

image.png

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

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  ...
  // 把新的组件占位符节点赋值给vm.$options._parentVnode和 vm.$vnode
  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render

  if (vm._vnode) {
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren

  // 更新$attrs and $listeners
  vm.$attrs = parentVnode.data.attrs || emptyObject
  vm.$listeners = listeners || emptyObject

  // 更新props,在更新props的时候因为props里面的数据是响应式的,所以会触发set,然后引起子组件的重新渲染
  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?
      // 这个时候会执行setter钩子,触发子组件的重新渲染
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }

  // 更新 listeners
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)
  ...
}

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

特别是如果props更新了,那么就会触发子组件的重新渲染。

  • 执行 update 钩子函数
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)
}

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

  • 完成 patch 过程
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)
}

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

  1. oldCh 与 ch 都存在且不相同时,使用 updateChildren 函数来更新子节点,这个后面重点讲。
  2. 如果只有 ch 存在,表示旧节点不需要了。如果旧的节点是文本节点则先将节点的文本清除,然后通过 addVnodes 将 ch 批量插入到新节点 elm 下。
  3. 如果只有 oldCh 存在,表示更新的是空节点,则需要将旧的节点通过 removeVnodes 全部清除。
  4. 当只有旧节点是文本节点的时候,则清除其节点文本内容。
  • 执行 postpatch 钩子函数
if (isDef(data)) {
  if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}

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

那么在整个 pathVnode 过程中,最复杂的就是 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

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

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

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

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

  1. 第一步:

image.png

  1. 第二步:

image.png

  1. 第三步:

image.png

  1. 第四步: image.png

  2. 第五步:

image.png

  1. 第六步:

image.png

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

原理图: reactive.png

下面以一个例子来完整感受下组件更新的过程:

const CompB = {
  template: `
    <div>
      <p>age: {{age}}</p>
      <p>sex: {{sex}}</p>
      <p>ball: {{hobby.ball}}</p>
      <p>game: {{hobby.game}}</p>
    </div>
  `,
  props: {
    age: Number,
    sex: {
      type: String,
      default: 'female',
      validator(value) {
        return value === 'male' || value === 'female'
      }
    },
    hobby: {
      type: Object,
      default() {
        return {
          ball: 'basketball',
          game: 'dota'
        }
      }
    }
  }
}

new Vue({
  el: '#app',
  components: { CompB },
  data() {
    return {
      age: 18,
      hobby: {
        ball: 'football',
        game: 'car'
      }
    }
  },
  methods: {
    change() {
      this.age = 19
    }
  },
  template: `
    <div>
      <comp-b :age="age" sex="male" :hobby="hobby"></comp-b>
      <button @click="change">change</button>  
    </div>
  `
})
  • 当渲染组件app.vue的时候会访问agehobby,此时这个两个dep会收集app.vue组件的渲染watcher
  • 当点击change事件后,改变了age,会执行dep.notify();

image.png

  • 接着执行queue里面的渲染watcher,此时queue只有一个app.vuewatcher image.png

  • 执行到patchVnode(oldVnode, vnode)函数,此时oldVnode, vnode如下:

image.png

image.png

两者的区别在于oldVnode有一个elm,它是虚拟dom对应的真实DOM节点,后面对dom的修改就是对elm的修改。

  • 首先比较第一个子节点,第一个子节点是组件节点vue-component-1-com-b,然后就走到下面的逻辑

image.png

然后执行updateChildComponent

image.png

在重新的赋值的过程中,就触发了子组件某个属性的setter函数,然后就执行子组件的dep.notify(),此时父组件正在渲染,所以flushingtrue,所以子组件的渲染watcher会插入到queue队列中,当父组件比对完其他的子节点,就会去执行queue队列里面的组件watcher的更新。

image.png

  • 组件渲染watcher执行update操作,这样当queue里面所有的watcher执行完毕后,说明整个组件树都更新完毕。