> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/vue-3-ji-shu-jie-mi/15.-bian-yi-qi-bian-yi-guo-cheng-zhong-de-you-hua-xi-jie.md).

# 15.编译器：编译过程中的优化细节

### 前言

在开启本篇章之前，我们先来思考一个问题，假设有以下模板：

```html
<template>
  <p>hello world</p>
  <p>{{ msg }}</p>
</template>>
```

其中一个 `p` 标签的节点是一个静态的节点，第二个 `p` 标签的节点是一个动态的节点，如果当 `msg` 的值发生了变化，那么理论上肉眼可见最优的更新方案应该是只做第二个动态节点的 `diff`，而无需进行第一个 `p` 标签节点的 `diff`。

如果熟悉 `Vue 2.x` 的小伙伴可能会知道，在 `Vue 2.x` 版本中在编译过程中有一个叫做 `optimize` 的阶段，会进行标记静态根节点的操作，被标记为静态根节点的节点，一方面会生成一个 `staticRenderFns`，首次渲染会以这个静态根节点 `vnode` 进行缓存，后续渲染会直接取缓存中的，从而避免重复渲染；另一方面生成的 `vnode` 会带有 `isStatic = true` 的属性，将会在 `diff` 过程中被跳过。但 `Vue 2.x` 对静态节点进行缓存就是一种空间换时间的优化策略，为了避免过度优化，在 `Vue 2.x` 中，识别静态根节点是需要满足：

1. 子节点是静态节点；
2. 子节点不是只有一个静态文本节点的节点。

所以，上面的示例第一个 `p` 标签在 `Vue 2.x` 中不会被判定位静态根节点，也就无法进行优化。

> 关于 `Vue 2.x` 如何做的编译时优化，这里只是简单进行了介绍，想了解更多的小伙伴可以参考这里：[入口开始，解读 Vue2 源码（七）—— $mount 内部实现 --- compile optimize标记节点](https://github.com/muwoo/blogs/blob/master/src/Vue/8.md)。

那么 `Vue 3` 呢？还是和 `Vue 2` 一样吗？答案显然是否定的，首先我们前面介绍了对于静态的节点，`Vue 3` 首先会进行静态提升，也就是相当于缓存了静态节点的 `vnode`，那 `diff` 过程呢？会跳过吗？本小节我们来一探究竟。

### PatchFlags

#### 是什么？

首先，我们需要认识一个 `PatchFlags` 这个属性，它是一个枚举类型，里面是一些二进制操作的值，用来标记在节点的 `patch` 类型。具体的枚举内容如下：

```js
export const enum PatchFlags {
  // 动态文本的元素
  TEXT = 1,
  
  // 动态 class 的元素
  CLASS = 1 << 1,
  
  // 动态 style 的元素
  STYLE = 1 << 2,
  
  // 动态 props 的元素
  PROPS = 1 << 3,
  
  // 动态 props 和有 key 值绑定的元素
  FULL_PROPS = 1 << 4,
  
  // 有事件绑定的元素
  HYDRATE_EVENTS = 1 << 5,
  
  // children 顺序确定的 fragment
  STABLE_FRAGMENT = 1 << 6,
  
  // children 中有带有 key 的节点的 fragment
  KEYED_FRAGMENT = 1 << 7,
  
  // 没有 key 的 children 的 fragment
  UNKEYED_FRAGMENT = 1 << 8,
  
  // 带有 ref、指令的元素
  NEED_PATCH = 1 << 9,
  
  // 动态的插槽
  DYNAMIC_SLOTS = 1 << 10,
  
  // 静态节点
  HOISTED = -1,
  
  // 不是 render 函数生成的元素，如 renderSlot
  BAIL = -2,
}
```

这些二进制的值是通过左移操作符 `<<` 生成的，关于左移操作符，我们在[《响应式原理：副作用函数探秘》](https://juejin.cn/book/7146465352120008743/section/7147530994017370127)篇章中已经介绍过了，在这里也是一种二进制操作的体现：

```js
TEXT = 0000000001; 
CLASS = 0000000010; 
STYLE = 0000000100;
```

这里通过二进制来表示 `PatchFlags` 可以方便我们做很多属性的判断，比如 `TEXT | STYLE` 来得到 `0000000101`，表示 `patchFlag` 既有 `TEXT` 属性也有 `STYLE` 属性，当需要进行判断有没有 `STYLE` 属性时，只需要 `FLAG & STYLE > 0`就行。

#### 什么时候生成的？

在搞清楚 `patchFlags` 的一些定义和使用基础后，那它是什么时候被赋值到 `vnode` 节点上的呢？前言中的模板字符串在 `compiler` 阶段会被转成一个 `render` 函数的字符串代码：

```js
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "hello world", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _hoisted_1,
    _createElementVNode("p", null, _toDisplayString(msg), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}
```

这里可以看出，`render` 函数内是通过 `createElementVNode` 方法来创建 `vnode` 的，该函数的第四个参数就代表着 `patchFlag`。对于我们上面的示例，其中 `<p>hello world</p>` 是 `hoisted`，对应的 `patchFlag = -1`，`<p>{{ msg }}</p>` 是动态文字节点，对应的 `patchFlag = 1`。

#### 有什么用？

接下来看看其实际使用案例，还是拿之前的 `patchElement` 函数来说：

```js
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
  let { patchFlag, dynamicChildren, dirs } = n2
  // 如果 patchFlag 不存在，那么就设置成 FULL_PROPS，意味着要全量 props 比对
  patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
  const oldProps = n1.props || EMPTY_OBJ
  const newProps = n2.props || EMPTY_OBJ

  const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
  if (dynamicChildren) {
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds
    )
  } else if (!optimized) {
    // full diff
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds,
      false
    )
  }

  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.FULL_PROPS) {
      // 如果元素的 props 中含有动态的 key，则需要全量比较
      patchProps(
        el,
        n2,
        oldProps,
        newProps,
        parentComponent,
        parentSuspense,
        isSVG
      )
    } else {
      // class
      if (patchFlag & PatchFlags.CLASS) {
        if (oldProps.class !== newProps.class) {
          hostPatchProp(el, 'class', null, newProps.class, isSVG)
        }
      }
      // style
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
      }

      // props
      if (patchFlag & PatchFlags.PROPS) {
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          // #1471 force patch value
          if (next !== prev || key === 'value') {
            hostPatchProp(
              el,
              key,
              prev,
              next,
              isSVG,
              n1.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
      }
    }

    // text
    if (patchFlag & PatchFlags.TEXT) {
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children as string)
      }
    }
  } else if (!optimized && dynamicChildren == null) {
    patchProps(
      el,
      n2,
      oldProps,
      newProps,
      parentComponent,
      parentSuspense,
      isSVG
    )
  }
  
}
```

这里涉及到两个比较重点的事儿，一个是和 `dynamicChildren` 相关，另一个是和动态 `props` 相关。我们先看和动态 `props` 相关的内容。

之前的章节我们跳过了对 `PatchFlags` 内容的理解，到了这里，我们通过代码可以知道 `Vue` 在更新子节点时，首先也是利用 `patchFlag` 的能力，对子节点进行分类做出不同的处理，比如针对以下例子：

```html
<template>
  <div :class="classNames" id='test'>
    hello world
  </div> 
</template>
```

得到的编译结果：

```js
import { normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", {
    class: _normalizeClass(classNames),
    id: "test"
  }, " hello world ", 2 /* CLASS */))
}
```

此时 `patchFlag & PatchFlags.CLASS > 0` 则在 `diff` 过程中，需要进行 `class` 属性的 `diff`， 从而减少了对 `id` 属性的不必要 `diff`，提升了 `props diff` 过程中的性能。

### dynamicChildren

另外，我们注意到，在编译后的 `render` 函数中会有一个 `_openBlock()` 函数的执行，我们来一起看一下其实现：

```
export const blockStack = []
export let currentBlock = null

export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}
```

`openBlock` 实现比较通俗易懂，就是向 `blockStack` 中 `push` `currentBlock`。其中 `currentBlock` 是一个数组，用于存储动态节点。`blockStack` 则是存储 `currentBlock` 的一个 `Block tree`。

然后我们接着看 `createElementBlock` 的实现：

```js
export function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) {
  return setupBlock(
    createBaseVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      shapeFlag,
      true /* isBlock */
    )
  )
}

function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT, isBlockNode = false, needFullChildrenNormalization = false) {
  // ...
  // 添加动态 vnode 节点到 currentBlock 中
  if (
    isBlockTreeEnabled > 0 &&
    !isBlockNode &&
    currentBlock &&
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }
  
  return vnode
}


function setupBlock(vnode) {
  // 在 vnode 上保留当前 Block 收集的动态子节点
  vnode.dynamicChildren =
    isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR) : null
  // 当前 Block 恢复到父 Block
  closeBlock()
  // 节点本身作为父 Block 收集的子节点
  if (isBlockTreeEnabled > 0 && currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}
```

`createElementBlock` 内部首先通过 `createBaseVNode` 创建 `vnode` 节点，在创建的过程中，会根据 `patchFlag` 的值进行判断是否是动态节点，如果发现 `vnode` 是一个动态节点，那么会被添加到 `currentBlock` 当中，然后在执行 `setupBlock` 函数的时候，将 `currentBlock` 赋值给 `vnode.dynamicChildren` 属性。

我们前面看 `patchElement` 的时候，有注意到函数体内部会进行是否有 `dynamicChildren` 属性进行不同的逻辑执行，前面的章节，我们只介绍了 `patchChildren` 完整的子节点 `diff` 算法，当 `dynamicChildren` 存在时，这里只会进行 `patchBlockChildren` 的动态节点 `diff`：

```js
const patchBlockChildren = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG) => {
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i]
    const newVNode = newChildren[i]
    // 确定待更新节点的容器
    const container =
      // 对于 Fragment，我们需要提供正确的父容器
      oldVNode.type === Fragment ||
      // 在不同节点的情况下，将有一个替换节点，我们也需要正确的父容器
      !isSameVNodeType(oldVNode, newVNode) ||
      // 组件的情况，我们也需要提供一个父容器
      oldVNode.shapeFlag & 6 /* COMPONENT */
        ? hostParentNode(oldVNode.el)
        :
        // 在其他情况下，父容器实际上并没有被使用，所以这里只传递 Block 元素即可
        fallbackContainer
    patch(oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true)
  }
}
```

`patchBlockChildren` 的实现很简单，遍历新的动态子节点数组，拿到对应的新旧动态子节点，并执行 `patch` 更新子节点即可。

这样一来，更新的复杂度就变成和动态节点的数量正相关，而不与模板大小正相关。这也是 `Vue 3` 做的一个重要的编译时优化的一部分。

### 总结

有了上面的一些介绍，我们还是回到前言的例子中：

```
<template>
  <p>hello world</p>
  <p>{{ msg }}</p>
</template>
```

转成 `vnode` 后的结果大致为：

```js
const vnode = {
  type: Symbol(Fragment),
  children: [
    { type: 'p', children: 'hello world' },
    { type: 'p', children: ctx.msg, patchFlag: 1 /* 动态的 text */ },
  ],
  dynamicChildren: [
    { type: 'p', children: ctx.msg, patchFlag: 1 /* 动态的 text */ },
  ]
}
```

此时组件内存在了一个静态的节点 `<p>hello world</p>`，在传统的 `diff` 算法里，还是需要对该静态节点进行不必要的 `diff`。所以 `Vue3` 先通过 `patchFlag` 来标记动态节点 `<p>{{ msg }}</p>`， 然后配合 `dynamicChildren` 将动态节点进行收集，从而完成在 `diff` 阶段只做**靶向更新**的目的。
