vue3 源码学习:组件更新过程
在前一篇文章中,我介绍了 vue3 中组件初次渲染为 DOM 元素的过程,下面介绍一下当组件发生变化时,vue3 更新组件的过程
组件类型的 vnode 挂载的过程时,在 setupRenderEffect 方法中,定义了一个 componentUpdateFn 方法并通过 ReactiveEffect 转换一个副作用的渲染函数,当组件状态发生变化时,会自动触发副作用函数 componentUpdateFn 执行,下面我们来看更新时的函数执行逻辑
可以看到当组件更新时,会进入到 patch 函数中执行更新逻辑
1const componentUpdateFn = () => {
2  // 组件初次挂载
3  if (!instance.isMounted) {
4    // ...
5  }
6  // 组件更新逻辑
7  else {
8    let { next, vnode } = instance
9
10    // next 记录未渲染的父组件 vnode
11    if (next) {
12      next.el = vnode.el
13      updateComponentPreRender(instance, next, optimized)
14    } else {
15      next = vnode
16    }
17
18    // 更新子节点 node
19    const nextTree = renderComponentRoot(instance)
20    const prevTree = instance.subTree
21    instance.subTree = nextTree
22
23    // 核心:进入patch 更新流程
24    patch(/* 相关参数 */)
25
26    next.el = nextTree.el
27  }
28}patch 函数根据不同类型的 vnode 做不同的处理操作,同样我们看核心 processElement 处理元素类型和processComponent 处理组件类型函数过程
组件类型更新过程
由于是更新组件,n1 不为 null,进入到 updateComponent 函数
1const processComponent = (/* 相关参数 */) => {
2  n2.slotScopeIds = slotScopeIds
3  if (n1 == null) {
4    // 挂载操作
5  } else {
6    // 更新组件
7    updateComponent(n1, n2, optimized)
8  }
9}在 updateComponent 函数执行逻辑如下
- 调用 shouldUpdateComponent方法判断是否需要更新组件,如果不用更新组件,则仅更新节点 DOM 元素缓存和 instance 的 vnode
- 如果需要更新组件,将新 vnode 赋值给 instance 的 next 属性,并调用 instance 的 update方法更新组件,update方法就是在挂载组件时,在setupRenderEffect方法中定义的响应式函数
1const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
2  const instance = (n2.component = n1.component)!
3  // 判断函数是否需要更新
4  if (shouldUpdateComponent(n1, n2, optimized)) {
5    instance.next = n2
6    instance.update()
7  }
8  // 无需更新仅复制元素
9  else {
10    n2.el = n1.el
11    instance.vnode = n2
12  }
13}那么组件在什么情况下需要更新呢,shouldUpdateComponent 中定义了下面几种情况都会强制更新组件
- HMR 热更新
- 动态节点,动态节点包括含有 vue 指令的节点(v-if 等)和使用了 transition 的节点
- 优化过 vnode(即存在 patchFlag 标志位)
- 子节点是动态插槽(在 v-for 中)
- 存在完整属性标志位 PatchFlags.FULL_PROPS,比较新旧 props 是否相同,不相同强制更新
- 存在部分属性标志位 PatchFlags.PROPS,新旧节点的动态 props 不同则强制更新
 
- 手动编写 render函数的场景
- 新 props 存在而旧 props 不存在
- 存在 emits 时,新旧 props 不同则强制更新
1export function shouldUpdateComponent(
2  prevVNode: VNode,
3  nextVNode: VNode,
4  optimized?: boolean
5): boolean {
6  const { props: prevProps, children: prevChildren, component } = prevVNode
7  const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
8  const emits = component!.emitsOptions
9
10  // 热更新场景
11  if (nextVNode.dirs || nextVNode.transition) {
12    return true
13  }
14
15  // 当存在优化标志位
16  if (optimized && patchFlag >= 0) {
17    // 子节点是动态插槽
18    if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
19      return true
20    }
21    // 存在完整属性标志位
22    if (patchFlag & PatchFlags.FULL_PROPS) {
23      if (!prevProps) {
24        return !!nextProps
25      }
26      return hasPropsChanged(prevProps, nextProps!, emits)
27    }
28    // 存在部分属性标志位
29    else if (patchFlag & PatchFlags.PROPS) {
30      const dynamicProps = nextVNode.dynamicProps!
31      for (let i = 0; i < dynamicProps.length; i++) {
32        const key = dynamicProps[i]
33        if (
34          nextProps![key] !== prevProps![key] &&
35          !isEmitListener(emits, key)
36        ) {
37          return true
38        }
39      }
40    }
41  } else {
42    // 手动编写 render 函数
43    if (prevChildren || nextChildren) {
44      if (!nextChildren || !(nextChildren as any).$stable) {
45        return true
46      }
47    }
48    // 新旧 props 相同则跳过更新
49    if (prevProps === nextProps) {
50      return false
51    }
52    if (!prevProps) {
53      return !!nextProps
54    }
55    if (!nextProps) {
56      return true
57    }
58    return hasPropsChanged(prevProps, nextProps, emits)
59  }
60
61  return false
62}另一点需要提出的是,在 shouldComponentUpdate 结果为 true 判断需要更新时,会将新节点 vnode 赋值给 next 属性,此处 next 属性的作用是:标记接下来需要渲染的子组件,当 next 属性存在时,通过 updateComponentPreRender 更新实例上的 props、slots、vnode 信息,保证后续组件渲染取值是最新的
1const componentUpdateFn = () => {
2  // 组件初次挂载
3  if (!instance.isMounted) {
4
5  }
6  // 组件更新逻辑
7  else {
8    let { next, vnode } = instance
9
10+   // next 记录未渲染的父组件 vnode
11+    if (next) {
12+     next.el = vnode.el
13+     updateComponentPreRender(instance, next, optimized)
14    } else {
15      next = vnode
16    }
17
18    // 更新子节点 node
19    const nextTree = renderComponentRoot(instance)
20    const prevTree = instance.subTree
21    instance.subTree = nextTree
22
23    // 核心:进入patch 更新流程
24    patch( /* 相关参数 */)
25
26    next.el = nextTree.el
27  }
28}元素更新过程
更新元素时,由于 n1 不为 null,同样进入到 patchElement 更新元素方法
1const processElement = (/* 相关参数 */) => {
2  if (n1 == null) {
3  } else {
4    patchElement(/* 相关参数 */)
5  }
6}patchElement 方法执行过程如下,函数中的核心逻辑是进入 patchChildren 方法对子节点进行完整的 diff 处理过程
- 根据 dynamicChildren 判断是否是动态元素(比如带有 v-for 指令的元素),如果是动态元素调用 patchBlockChildren更新动态子节点,否则调用patchChildren方法更新
- 根据 patchFlag,依次处理 props、class、style 等属性
1const patchElement = (/* 相关参数 */) => {
2  const el = (n2.el = n1.el!)
3  let { patchFlag, dynamicChildren } = n2
4  patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
5  const oldProps = n1.props || EMPTY_OBJ
6  const newProps = n2.props || EMPTY_OBJ
7
8  // 根据 dynamicChildren 判断是否是动态元素
9  if (dynamicChildren) {
10    // 处理动态子节点
11    patchBlockChildren(/* 相关参数 */)
12  } else if (!optimized) {
13    // 全量diff处理子节点
14    patchChildren(/* 相关参数 */)
15  }
16
17  // 根据 patchFlag,依次处理 props、class、style 等属性
18  if (patchFlag > 0) {
19    if (patchFlag & PatchFlags.FULL_PROPS) {
20      // element props contain dynamic keys, full diff needed
21      patchProps(/* 相关参数 */)
22    } else {
23      if (patchFlag & PatchFlags.CLASS) {
24        if (oldProps.class !== newProps.class) {
25          hostPatchProp(el, 'class', null, newProps.class, isSVG)
26        }
27      }
28      if (patchFlag & PatchFlags.STYLE) {
29        hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
30      }
31
32      if (patchFlag & PatchFlags.PROPS) {
33        const propsToUpdate = n2.dynamicProps!
34        for (let i = 0; i < propsToUpdate.length; i++) {
35          const key = propsToUpdate[i]
36          const prev = oldProps[key]
37          const next = newProps[key]
38          // #1471 force patch value
39          if (next !== prev || key === 'value') {
40            hostPatchProp(/* 相关参数 */)
41          }
42        }
43      }
44    }
45
46    if (patchFlag & PatchFlags.TEXT) {
47      if (n1.children !== n2.children) {
48        hostSetElementText(el, n2.children as string)
49      }
50    }
51  } else if (!optimized && dynamicChildren == null) {
52    patchProps(/* 相关参数 */)
53  }
54}在 patchChildren 中,子节点一共分为三种类型:文本类型、数组类型、空节点,数组类型即代表元素还存在子节点。根据新旧节点这三种类型的两两组合,一个会存在 9 种场景,代码中的判断逻辑相对复杂,但整理为表格之后就非常清晰明了。对于新旧节点都是数组类型,进入完成 diff 算法的计划再新写一篇文章
| | 新节点为空 | 新节点文本 | 新节点为数组 | | ------------ | ---------- | ---------------------- | ------------------------ | | 旧节点为空 | 不做操作 | 添加新文本 | 添加新子节点 | | 旧节点为文本 | 删除旧文本 | 更新文本 | 移除旧文本,添加新子节点 | | 旧节点为数组 | 删除旧数组 | 移除旧节点,添加新文本 | 完整 diff 算法 |
1const patchChildren: PatchChildrenFn = (/* 相关参数 */) => {
2  const c1 = n1 && n1.children
3  const prevShapeFlag = n1 ? n1.shapeFlag : 0
4  const c2 = n2.children
5
6  const { patchFlag, shapeFlag } = n2
7  // 如果存在 patchFlag,进行优化算法处理
8  if (patchFlag > 0) {
9    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
10      patchKeyedChildren(/* 相关参数 */)
11      return
12    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
13      patchUnkeyedChildren(/* 相关参数 */)
14      return
15    }
16  }
17
18  // 新旧节点三种类型 9 种组合的处理过程
19  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
20    // 新节点是文本节点,旧节点是数组或文本节点,先卸载旧节点
21    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
22      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
23    }
24    // 新旧节点都是文本节点,更新文本内容
25    if (c2 !== c1) {
26      hostSetElementText(container, c2 as string)
27    }
28  } else {
29    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
30      // 新旧节点都是数组,完整 diff 算法
31      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
32        patchKeyedChildren(/* 相关参数 */)
33      } else {
34        // 新节点不是数组,旧节点是数组,卸载旧节点
35        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
36      }
37    } else {
38      // 新节点是数组,旧节点是文本节点,先卸载旧节点
39      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
40        hostSetElementText(container, '')
41      }
42      // 新节点是数组,挂载新节点
43      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
44        mountChildren(/* 相关参数 */)
45      }
46    }
47  }
48}组件更新过程总结
上面介绍了组件类型和元素类型的更新过程,下面我们举一个具体的例子来总结整体的回顾流程
父组件 App.vue
1<template>
2  <div>
3    父组件
4    <child :msg="msg" />
5    <button @click="handleUpdateMsg">更新 msg</button>
6  </div>
7</template>
8<script setup>
9  import { ref } from 'vue'
10  import Child from './child.vue'
11
12  const msg = ref('初始化')
13
14  function handleUpdateMsg() {
15    msg.value = '更新后'
16  }
17</script>子组件 child.vue
1<template>
2  {{ msg }}
3</template>
4<script setup>
5  import { toRefs } from 'vue'
6  const props = difineProps({
7    msg: {
8      type: String
9    }
10  })
11
12  const { msg } = toRefs(props)
13</script>在父组件 App.vue 中,内部使用了一个 child.vue 子组件,当点击按钮修改 msg 时,两个组件的更新过程如下
- 点击按钮,App 组件自身状态发生变化,进入组件更新逻辑,此时 next 属性为 null,根据新旧 subTree 进入 patch方法
- 此时组件的类型为 div,进入 patchElement更新元素,此时 div 节点下有 child 子组件,进入组件更新流程
- 子组件 child 进入 updateComponent方法,shouldUpdateComponent判断为 true 需要更新组件,将新子组件 vnode 赋值为 instance.next,调用instance.update()方法进入副作用渲染函数
- 此时 next 属性有值,调用 updateComponentPreRender更新实例数据,完成子组件 child 渲染过程
- 子组件更新完成后,父组件 App 根据最新的实例数据,渲染最新的子节点
最后在上次的挂载流程图上,补充组件过程过程的逻辑(蓝色部分)

