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 函数来挂载组件。

它看起来是这样:

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

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

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

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

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

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

编译后生成

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-coreapiCreateApp.ts文件下的createRenderer 函数中,createRenderer 函数的作用是创建renderer。createRenderer 函数的内部声明了很多函数,但是我们目前只关注它的返回值。

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文件中。

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

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

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

  • 将根组件对象转换成vnode,然后再将vnode传入render函数执行。

  • 如果在开发环境下,则声明context.reload方法,克隆vnode并重新调用render函数。

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

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。

以下面的代码为例:

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

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函数声明代码如下(省略了部分代码):

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

dynamicChildrenchildren 属性的作用都是用于存储元素子节点,但是区别在于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的两种情况,然后再去处理情况情况。

代码如下:

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属性时,这个时候处理比较简单直接,直接用一个指针从两数组的第一个元素同时向后遍历即可,如:

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

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

  • 如果旧节点列表比新节点列表长,那么多出的节点就是不需要的节点,需要删除。

  • 如果旧节点列表比新节点列表短,那么少了的节点就是需要添加的节点,需要创建添加。

具体代码如下:

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列表从头对齐进行比较:

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

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

第二步:与第一步相同,只是换成了从后面对齐遍历:

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

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

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

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

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

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

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

 // 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就是需要删除的节点。

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是否被处理过。

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在列表中的位置,例如:

(b-d、c-e两两类型相同且都没key值)
 a  [ b  c ] 
     /  /
 [ d   e ] a

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

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函数。而在

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

这里的hostSetTexthostInsert 等函数其实就是对真实DOM的操作。例如hostSetText

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

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

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

最后更新于