# 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，以此来实现页面热更新。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET 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?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
