虚拟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-core
的apiCreateApp.ts
文件下的createRenderer
函数中,createRenderer
函数的作用是创建renderer。createRenderer
函数的内部声明了很多函数,但是我们目前只关注它的返回值。
复制 export function createRenderer(){
// ...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
createRenderer函数返回了包含三个方法的对象:
这里可以清晰地看出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
}
}
这里有很多我们熟知的use
、component
等方法,但是现在我们主要关注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函数接收三个参数,其中最重要的是前两个:
参数中的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总共有这几种类型:
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
)
}
dynamicChildren
和children
属性的作用都是用于存储元素子节点,但是区别在于dynamicChildren
只会保存动态节点,这其实是一个性能优化的点,因为在更新时静态内容是不需要改变的,需要重新渲染的只有动态节点。一个包含dynamicChildren
属性的vnode被称为Block。
在patchElement过程时,如果存在dynamicChildren
属性会直接更新dynamicChildren
属性中的子节点,只有当不存在dynamicChildren
属性,且不需要进行优化时(例如热更新)才会进行全比较(full diff)。
我们重点看看patchChildren
的情况,全比较时元素children时有这几种情况:
当children是数组时,也分这三种情况情况:
这两种情况可以在编译时发现,在生成渲染函数时通过设置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包含两种情况:
假如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)
}
}
}
这里的hostSetText
、hostInsert
等函数其实就是对真实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,以此来实现页面热更新。