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