> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/yuan-ma-jie-xi/vue-yuan-ma-jie-xi/vue3-yuan-ma-jie-xi-xi-lie-xuan-ran-xi-tong.md).

# Vue3源码解析系列-渲染系统

## 虚拟DOM

在正式分析vue渲染器之前需要先简单了解一下虚拟DOM。虚拟DOM是一种思想，它本质是利用js对象去描述真实dom节点，这与AST(抽象语法树)有些相似。一个虚拟节点(简称vnode)包含了描述真实dom节点的一切信息，包括元素标签名、属性等，通过vnode可以创建一个真实的dom节点，而多个vnode可以构建与真实dom树相对应的vnode树，也就是说可以通过vnode树来描述真实的dom树(视图)。

## 渲染器

前面我们已经讲过Vue的模板编译，Vue先将模板进行解析(parse)得到模板AST，然后进行转换(transform)得到JavaScript AST(codegenNode)，最后生成(generate)渲染函数。这些步骤是编译时完成的，最终的结果是一个渲染函数的静态代码，最终要转换为视图还需要Vue渲染器的帮助。

### createApp

一个Vue项目的入口文件是`main.js`文件，在这个文件中会调用`createApp`来创建一个app对象，并且这个时候还会调用`mount` 函数来挂载组件。

它看起来是这样：

```tsx
import APP from "./src/App.vue";
import { createApp } from "@vue/runtime-dom";

createApp(APP).mount("#app");
```

vue单页面组件经过编译后最终得到的是一个组件对象。例如

```tsx
<script setup>
import { ref } from 'vue'
import Child from "./Child.vue"

const msg = ref('Hello World!')
</script>

<template>
  <h1>{{ msg }}</h1>
  <Child />
</template>
```

编译后生成

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

import { ref } from 'vue'
import Child from "./Child.vue"

const __sfc__ = {
  setup(__props) {

const msg = ref('Hello World!')

return (_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("h1", null, _toDisplayString(msg.value), 1 /* TEXT */),
    _createVNode(Child)
  ], 64 /* STABLE_FRAGMENT */))
}
}

}
__sfc__.__file = "App.vue"
export default __sfc__
```

`createApp(APP)` 实际上是将根组件对象传递到`createAPP`函数中调用。

### Renderer

createApp函数的定义位于`runtime-core`的`apiCreateApp.ts`文件下的`createRenderer` 函数中，`createRenderer` 函数的作用是创建renderer。`createRenderer` 函数的内部声明了很多函数，但是我们目前只关注它的返回值。

```tsx
export function createRenderer(){
// ...
return {
	  render,
	  hydrate,
	  createApp: createAppAPI(render, hydrate)
	}
}
```

createRenderer函数返回了包含三个方法的对象：

1. render:：将vnode转换成视图的函数。
2. hydrate：用于SSR，略。
3. createApp：就是我们前面提到的那个函数。

这里可以清晰地看出`createApp`函数通过`createAppAPI`函数创建，我们重点来看看`createAppAPI`函数。

createAppAPI的核心逻辑代码在`runtime-core`下的`apiCreateApp.ts`文件中。

```tsx
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }

    const context = createAppContext()
    const installedPlugins = new Set()

    let isMounted = false

    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

	    // ...

      use(plugin: Plugin, ...options: any[]) {
					// ...
      },

      mixin(mixin: ComponentOptions) {
					// ...
      },

      component(name: string, component?: Component): any {
					// ...
      },

      directive(name: string, directive?: Directive) {
					// ...
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

          // HMR root reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, isSVG)
            }
          }

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            render(vnode, rootContainer, isSVG)
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }

          return vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          )
        }
      },

      unmount() {
        if (isMounted) {
          render(null, app._container)
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = null
            devtoolsUnmountApp(app)
          }
          delete app._container.__vue_app__
        } else if (__DEV__) {
          warn(`Cannot unmount an app that is not mounted.`)
        }
      },

      provide(key, value) {
					// ...
      }
    })

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }
}
```

这里有很多我们熟知的`use`、`component`等方法，但是现在我们主要关注`mount`方法，因为通过它组件才能被转换成我们看到的视图。

mount函数主要做了两件事(本文不考虑SSR)：

* 将根组件对象转换成`vnode`，然后再将`vnode`传入`render`函数执行。
* 如果在开发环境下，则声明`context.reload`方法，克隆`vnode`并重新调用`render`函数。

可以发现，vue将渲染函数最终转换成视图的秘诀就在于`render`函数中。并且`createAppAPI`函数所调用的`render`函数就是`createRenderer`函数中的`render`函数。让我们来看看`render`函数声明：

```tsx
const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
```

render函数接收三个参数，其中最重要的是前两个：

1. vnode：虚拟节点。
2. container：绑定视图的真实DOM节点。

参数中的vnode被作为一个新的vnode，当render执行完毕前会将其挂载在`container._vnode` ，目的是方便新旧vnode进行比较。

* 如果旧vnode存在但是新vnode不存在，说明要删除该节点。
* 如果新旧节点都不存在，那么什么也不需要做。
* 如果新节点存在但是旧节点不存在，那么说明需要添加节点。
* 如果新旧节点都存在，那么说明需要patch(打补丁)。

从代码中可以看出，新增节点和新旧vnode都存在的这两种情况下，vue都采用了patch操作。

那么说了这么多，patch到底是什么呢？

## Patch

### 什么是Patch

Patch是Vue优化性能的一种方法，它是在虚拟DOM的基础上实现的。Patch提升性能的思想是利用js的算力来降低DOM更新的性能代价。

试想下一个vue组件被渲染为真实的dom节点后，当组件更新时要如何更新视图(真实DOM)？最简单直接的方法是先将dom节点全删除，然后再重新渲染新的dom节点。但是这样的方法非常浪费性能，因为操作DOM的性能消耗是非常大的，尤其是当操作大量dom节点时。

vue采用了另一种思路，先用虚拟DOM来描述真实的DOM，旧vnode树与当前真实DOM对应，新vnode树表示更新后的真实DOM，然后当需要更新时比较新旧两个vnode树，用js计算出两者的不同之处，然后只针对不同之处进行相应的修改，相同的地方就不用去改，这样就达到了“尽量少地操作dom节点”的目的。这一操作就像打补丁一样，vue将其称为Patch。

以下面的代码为例：

```tsx
<a href="{a}" >Link</a>
const a = ref('https:xxx.com')
// 更改a
a.value = 'https:yyy.com'
```

标签a更新前后只有属性`href`改变，因此只要调用`element.setAttribute`更改`href`属性即可，不需要删除a节点再重新渲染。

### patch函数

patch函数代码如下，其中n1,n2分别是旧vnode和新vnode。

```tsx
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    if (n1 === n2) {
      return
    }

    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }
```

patch函数并不复杂，首先检查旧vnode是否存在且新旧vnode类型是否相同，如果旧vnode存在且新旧vnode类型不相同，则会先卸载旧vnode后，再去渲染新vnode。

vue会通过新vnode的类型来调用不同函数去进行patch(打补丁)，在vue template中其实常用的node类型也就只有文本节点、元素节点和注释节点，但是vue额外地定了Fragment、Static等类型，因此vue vnode总共有这几种类型：

* Component
* Fragment
* Static
* Text
* Element
* Comment
* TeleportImpl
* SuspenseImpl

TeleportImpl和SuspenseImpl类型主要是为内置组件准备的，Static类型是指静态节点，也就是不包含任何响应式状态的节点，这种节点不需要随着随着组件状态更新而更新，因此认为它们是“静态的”，Fragment则表示一个片段，例如包含多个根节点的模板。

### patchElement

patchText和patchComment相对而已比较简单，更改的地方比较少，patchElement相对而言复杂很多，因为Element可能会包含子节点，这就会涉及到子节点的比较。

patchElement函数声明代码如下(省略了部分代码)：

```tsx
const patchElement = (
    n1: VNode,
    n2: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
		// ...
		if (__DEV__ && isHmrUpdating) {
      // HMR updated, force full diff
      patchFlag = 0
      optimized = false
      dynamicChildren = null
    }

    if (dynamicChildren) {
      patchBlockChildren(
        n1.dynamicChildren!,
        dynamicChildren,
        el,
        parentComponent,
        parentSuspense,
        areChildrenSVG,
        slotScopeIds
      )
      if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
        traverseStaticChildren(n1, n2)
      }
    } else if (!optimized) {
      // full diff
      patchChildren(
        n1,
        n2,
        el,
        null,
        parentComponent,
        parentSuspense,
        areChildrenSVG,
        slotScopeIds,
        false
      )
    }
```

`dynamicChildren` 和`children` 属性的作用都是用于存储元素子节点，但是区别在于`dynamicChildren` 只会保存动态节点，这其实是一个性能优化的点，因为在更新时静态内容是不需要改变的，需要重新渲染的只有动态节点。一个包含`dynamicChildren` 属性的vnode被称为Block。

在patchElement过程时，如果存在`dynamicChildren` 属性会直接更新`dynamicChildren` 属性中的子节点，只有当不存在`dynamicChildren` 属性，且不需要进行优化时(例如热更新)才会进行全比较(full diff)。

我们重点看看`patchChildren`的情况，全比较时元素children时有这几种情况：

* children是Text
* children是数组
* children为空，即没有子节点

当children是数组时，也分这三种情况情况：

* v-for创建且子节点全部或部分有key属性
* v-for创建且子节点都没key属性
* 没用v-for创建的多个子节点

这两种情况可以在编译时发现，在生成渲染函数时通过设置patchFlag的值来标记是哪种情况。

vue首先会去判断使用v-for的两种情况，然后再去处理情况情况。

代码如下：

```tsx
const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized = false
  ) => {
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children

    const { patchFlag, shapeFlag } = n2
    // fast path
    if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
        // this could be either fully-keyed or mixed (some keyed some not)
        // presence of patchFlag means children are guaranteed to be arrays
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
        // unkeyed
        patchUnkeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        return
      }
    }

    // children has 3 possibilities: text, array or no children.
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // text children fast path
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
      }
      if (c2 !== c1) {
        hostSetElementText(container, c2 as string)
      }
    } else {
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // prev children was array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          // two arrays, cannot assume anything, do full diff
          patchKeyedChildren(
            c1 as VNode[],
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else {
          // no new children, just unmount old
          unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
        }
      } else {
        // prev children was text OR null
        // new children is array OR null
        if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(container, '')
        }
        // mount new if array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }
      }
    }
  }
```

#### patchUnkeyedChildren

当使用v-for创建子节点列表且都没有key属性时，这个时候处理比较简单直接，直接用一个指针从两数组的第一个元素同时向后遍历即可，如：

```tsx
 a  b  c 更新前vnode子元素列表
 a  b    更新后vnode子元素列表
👆(指针)
```

然后将指向的更新前后vnode传入patch进行调用，patch是一个递归函数。前后vnode children长度不一定是相同的，但是处理也很简单：

* 如果旧节点列表比新节点列表长，那么多出的节点就是不需要的节点，需要删除。
* 如果旧节点列表比新节点列表短，那么少了的节点就是需要添加的节点，需要创建添加。

具体代码如下：

```tsx
const patchUnkeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length
    const newLength = c2.length
    const commonLength = Math.min(oldLength, newLength)
    let i
    for (i = 0; i < commonLength; i++) {
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
    if (oldLength > newLength) {
      // remove old
      unmountChildren(
        c1,
        parentComponent,
        parentSuspense,
        true,
        false,
        commonLength
      )
    } else {
      // mount new
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized,
        commonLength
      )
    }
  }
```

#### patchKeyedChildren

patchKeyedChildren函数相较于patchUnkeyedChildren函数复杂很多，因为patchKeyedChildren包含两种情况：

1. 都有key属性
2. 部分由key属性

假如children都有key属性，那么只需要判断key值就可以找到对应的新旧vnode，但是由于存在部分子元素没有key属性的情况，所以处理逻辑更加复杂。

总共有五个步骤：

**第一步**：首先会将新旧vnode列表从头对齐进行比较：

```tsx
[ a  b ] f  c
[ a  b ] e  c
 👆
```

**如果新旧vnode类型和key 值都相同，则直接对两vnode进行patch操作。**

**第二步**：与第一步相同，只是换成了从后面对齐遍历：

```tsx
 a  b  f [ c ]
 a  b  e [ c ]
          👆
```

**第三步**：判断旧vnode列表是否已经都处理完了，且新vnode列表是否还有节点没有被处理完。如果旧vnode都处理完毕且新vnode还有未被处理的，说明多出来的是新增的vnode，需要创建添加到DOM中。

**第四步**：和上一步相同，不过是判断新vnode，如果新vnode都处理完毕且旧vnode还有未被处理的，说明多出来的是不再需要的节点，需要删除。

**第五步**：如果新旧两vnode列表都还有节点未被处理，则进行最后的处理。例如以下这种情况：

```tsx
 a b [c d e] f g
 a b [e d c h] f g
```

第五个步骤又可以分成三个小步骤，用5.1、5.2来表示

**步骤5.1**：首先构建新vnode的key映射表：

```tsx
 // map的key为新vnode的key值，value是在新vnode列表中的下标
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
  for (i = s2; i <= e2; i++) {
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    if (nextChild.key != null) {
      if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
        warn(
          `Duplicate keys found during update:`,
          JSON.stringify(nextChild.key),
          `Make sure keys are unique.`
        )
      }
      keyToNewIndexMap.set(nextChild.key, i)
    }
  }
```

**步骤5.2**：然后遍历旧vnode列表，如果旧vnode存在key值，则通过5.1步骤创建的`keyToNewIndexMap` 映射表找到对应新vnode，两者进行patch，否则就遍历新vnode，找到类型与旧vnode相同且也没有key值的vnode，两者进行patch。如果最终仍然找不到对应的新vnode，则说明该vnode是多余的节点，需要删除。

例外如果在遍历过程中发现新vnode列表都已经处理完毕了，那么就无需再遍历了，未遍历的旧vnode就是需要删除的节点。

```tsx
let j
let patched = 0
const toBePatched = e2 - s2 + 1
// 标记是否发生了节点移动的情况
let moved = false
// 指向所有处理过的新vnode中index最大的节点(即最靠后的新vnode)
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

for (i = s1; i <= e1; i++) {
  const prevChild = c1[i]
  if (patched >= toBePatched) {
    // all new children have been patched so this can only be a removal
    unmount(prevChild, parentComponent, parentSuspense, true)
    continue
  }
  let newIndex
  if (prevChild.key != null) {
    newIndex = keyToNewIndexMap.get(prevChild.key)
  } else {
    // key-less node, try to locate a key-less node of the same type
    for (j = s2; j <= e2; j++) {
      if (
        newIndexToOldIndexMap[j - s2] === 0 &&
        isSameVNodeType(prevChild, c2[j] as VNode)
      ) {
        newIndex = j
        break
      }
    }
  }
```

在这一过程中也会创建一个Map `newIndexToOldIndexMap`，它的作用是记录新vnode和对应旧vnode的位置，为下一个步骤的移动节点做准备，以及标记当前新vnode是否被处理过。

```tsx
if (newIndex === undefined) {
  unmount(prevChild, parentComponent, parentSuspense, true)
} else {
  // i+1是因为位置不能为0，为0就表示该vnode没有被处理过，见步骤5.3
  newIndexToOldIndexMap[newIndex - s2] = i + 1
  if (newIndex >= maxNewIndexSoFar) {
    maxNewIndexSoFar = newIndex
  } else {
    // 当出现有新vnode的下标小于maxNewIndexSoFar的情况，例如两key相同的新旧vnode，
    // 此时两者的在对应vnode列表中的下标应该是不同的，说明需要移动
    moved = true
  }
  patch(
    prevChild,
    c2[newIndex] as VNode,
    container,
    null,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized
  )
  patched++
}
}
```

**步骤5.3**：移动节点，上一个步骤中虽然已经对新旧vnode都进行了patch操作，但是并未调整vnode在列表中的位置，例如：

```tsx
（b-d、c-e两两类型相同且都没key值）
 a  [ b  c ] 
     /  /
 [ d   e ] a
```

这个时候虽然已经将vnode都patch了，但是a的位置还未调整，因此最后一步就是跳转位置，调整的方法依靠上一步创建的`newIndexToOldIndexMap`。

```tsx
const increasingNewIndexSequence = moved
  ? getSequence(newIndexToOldIndexMap)
  : EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
for (i = toBePatched - 1; i >= 0; i--) {
  const nextIndex = s2 + i
  const nextChild = c2[nextIndex] as VNode
  const anchor =
    nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
  // 前面都没有处理过的新节点
  if (newIndexToOldIndexMap[i] === 0) {
    // mount new
    // ...
    )
  } else if (moved) {
    // move if:
    // There is no stable subsequence (e.g. a reverse)
    // OR current node is not among the stable sequence
    if (j < 0 || i !== increasingNewIndexSequence[j]) {
      move(nextChild, container, anchor, MoveType.REORDER)
    } else {
      j--
    }
  }
}
```

### 操作DOM节点

前面已经实现了新旧vnode的diff，通过diff找到新旧vnode的不同之处，接下来就是针对不同之处进行实际的DOM修改。

事实上在patch过程中已经在操作DOM节点了，因为旧vnode与真实DOM对应，因此diff过程中对旧vnode的操作其实就是对真实dom节点的操作。

我们以Text节点为例，假设新旧vnode都是Text类型，那么patch过程中就会调用processText函数。而在

```tsx
const patch: PatchFn = () => {
  // ...
  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:
      processText(n1, n2, container, anchor)
      break
	// ...
}

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  if (n1 == null) {
    hostInsert(
      (n2.el = hostCreateText(n2.children as string)),
      container,
      anchor
    )
  } else {
    const el = (n2.el = n1.el!)
    if (n2.children !== n1.children) {
      hostSetText(el, n2.children as string)
    }
  }
}
```

这里的`hostSetText` 、`hostInsert` 等函数其实就是对真实DOM的操作。例如`hostSetText` ：

```tsx
const { setText: hostSetText, }  = options
setText: (node, text) => {
  node.nodeValue = text
},
```

这样的函数还有很多，它们都包含对真实DOM的操作。

```tsx
const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options
```

因此在patch过程中，真实dom逐渐地被修改，直到与新vnode一致。

## 小结

首先我们了解了什么是虚拟DOM，虚拟DOM其实就是通过js对象来描述真实dom节点的一种思想。接下来我们分析了vue的渲染器(renderer)，以及它是如何将编译器生成的渲染函数转变为视图的。

渲染函数最终转换成视图（即DOM）是通过render函数实现的，渲染器中最重要的操作是patch，通过diff找到新vnode和旧vnode的不同之处，然后针对不同之处修改真实DOM节点，最终使视图与新vnode一致。

vue渲染器将渲染函数转换成视图的起点在于`createApp(app).mount(’#app’)`语句，它会先创建一个app对象，然后在mount操作调用render函数去递归执行patch，在开发环境下，还会在reload时重新调用render，以此来实现页面热更新。
