0%

vue3初始化渲染流程分析

前言

Vue3 官网中有下面这样一张图,基本展现出了 Vue3 的渲染原理:

alt text

本文会从源码角度来简单的看一下 Vue3 的运行全流程,旨在加深对上图的理解。

初始化渲染

从下面这个很简单的使用示例开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createApp, ref } from 'vue'

createApp({
template: `
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
</div>
`,
setup() {
return {
count: ref(0),
}
},
}).mount('#app')

首先调用了 createApp 方法:

1
2
3
4
5
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// ...
return app
})

我们可以看到重点在于 ensureRenderer

1
2
3
4
5
6
7
8
9
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)
// 延时创建渲染器,当用户只依赖响应式包的时候,可以通过 tree-shaking 移除核心渲染逻辑相关的代码
// 因为 ensureRenderer 是在执行 createApp 的时候调用的,如果你不执行 createApp 而只使用 vue 的一些响应式 API,就不会创建这个渲染器,所以说延时渲染。
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}

rendererOptions是一个对象,渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法。

这么做主要是方便跨平台,比如在其他非浏览器环境,可以替换成对应的节点操作方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// rendererOptions
{
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},

remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
// ...
}
1
2
3
function createRenderer(options: RendererOptions) {
return baseCreateRenderer(options)
}

调用 baseCreateRenderer, 这个函数简直可以用庞大来形容,patch、mount、diff均在这个方法中实现,回头我们再来细看实现,现在我们只需要关心他最后返回的什么

1
2
3
4
5
6
7
8
9
10
11
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions,
) {
// ...此处省略2000行
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate),
}
}

我们发现createApp是通过createAppAPI方法调用返回的,下面我们看createAppAPI。

createAppAPI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}

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 WeakSet()

let isMounted = false

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

version,

get config() {
return context.config
},

set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`,
)
}
},
// 都是一些眼熟的方法
// 注册plugin
use(plugin: Plugin, ...options: any[]) {
//...
},
// 全局混入
mixin(mixin: ComponentOptions) {
//...
},
// 注册全局组件
component(name: string, component?: Component): any {
//...
},
// 注册全局指令
directive(name: string, directive?: Directive) {
//...
},
// 挂载根组件
mount(
rootContainer: HostElement, // 挂载容器
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
//...
},
// 卸载
unmount() {
//...
},
// 分享数据
provide(key, value) {
//...
}
//...
})
//...
return app
}
}

这个就是最终的 createApp方法,接受了 rootComponent 和 rootProps 两个参数,我们在应用层面执行 createApp(App) 方法时,会把 App 组件对象作为根组件传递给 rootComponent。这样,createApp 内部就创建了一个 app 对象。

所谓的应用实例app其实就是一个对象,我们传进去的组件选项作为根组件存储在_component属性上,另外还可以看到应用实例提供的一些方法,比如注册插件的use方法,挂载实例的mount方法等。

createAppContext 实现

context其实也是一个普通对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {},
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap(),
}
}

这部分是vue的初次渲染逻辑,首先官方解构了mount方法, 然后又重写了app.mount,并调用normalizeContainer校验挂载元素,临时保存了需要渲染的内容。并对vue2的写法做了兼容处理,挂载元素进行渲染。

在以上整个 app 对象创建过程中,Vue.js 利用闭包和函数柯里化的技巧,很好地实现了参数保留。比如,在执行 app.mount 的时候,并不需要传入渲染器 render,这是因为在执行 createAppAPI 的时候渲染器 render 参数已经被保留下来了。

至此,createApp的流程大概到此结束,下一部分来分析mount挂载部分。

mounted挂载

重写app.mount方法

createApp 函数中,首先取出 app 对象中的 mount 函数,然后通过 app.mount = () => {} 对 mount 函数进行重写:

  1. 首先调用 normalizeContainer 函数来获取container节点;
  2. 判断该节点是否存在,若不存在,则直接返回;
  3. 清空container的innerHTML;
  4. 调用mount函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)

const { mount } = app

/*
(1)这里重写的目的是考虑到跨平台(app.mount里面只包含和平台无关的代码)
(2)这些重写的代码是完善web平台下的渲染逻辑(比如其他平台也可以进行类似的重写)
*/

app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 根组件容器 选择器字符串兼容
const container = normalizeContainer(containerOrSelector)
if (!container) return
// rootComponent
const component = app._component
// 如组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
component.template = container.innerHTML
// 2.x compat check
/**
* __COMPAT__ 是启动的时候通过rollup去注入进去的
* 用来判断是否向下兼容
*/
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null,
)
break
}
}
}
}

// clear content before mounting
container.innerHTML = ''
// 调用初始app.mount,挂载元素进行渲染
const proxy = mount(container, false, resolveRootNamespace(container))
if (container instanceof Element) {
// vue2的话,会给#app设置一个v-cloak属性,在render的时候清空掉
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}

return app
})

首先是通过 normalizeContainer 标准化容器(这里可以传字符串选择器或者 DOM 对象,但如果是字符串选择器,就需要把它转成 DOM 对象,作为最终挂载的容器),然后做一个 if 判断,如果组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容;接着在挂载前清空容器内容,最终再调用 app.mount 的方法走标准的组件渲染流程。

在这里,重写的逻辑都是和 Web 平台相关的,所以要放在外部实现。此外,这么做的目的是既能让用户在使用 API 时可以更加灵活,也兼容了 Vue.js 2.x 的写法,比如 app.mount 的第一个参数就同时支持选择器字符串和 DOM 对象两种类型。

从 app.mount 开始,才算真正进入组件渲染流程。

真正的挂载mounted

核心流程:

  1. 根据传入的根组件App创建vnode
  2. 渲染vnode。

mount方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
) {
if (!isMounted) {
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`,
)
}
const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context

if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}

// HMR root reload
if (__DEV__) {
context.reload = () => {
// casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries
render(
cloneVNode(vnode),
rootContainer,
namespace as ElementNamespace,
)
}
}

if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, namespace)
}
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 getExposeProxy(vnode.component!) || 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)\``,
)
}
}

createVNode

createVNode方法会根据组件的类型生成一个标志,后续会通过这个标志做一些优化处理。我们传的是一个组件选项,也就是一个普通对象,shapeFlag的值为4。后续我们会重点关注我们的主线组件 vnode 和普通元素 vnode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const createVNode = _createVNode;
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
// ...
const shapeFlag = isString(type)
? 1 /* ShapeFlags.ELEMENT */
: isSuspense(type)
? 128 /* ShapeFlags.SUSPENSE */
: isTeleport(type)
? 64 /* ShapeFlags.TELEPORT */
: isObject(type)
? 4 /* ShapeFlags.STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* ShapeFlags.FUNCTIONAL_COMPONENT */
: 0;
// ...
return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true);
}

然后调用了createBaseVNode方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false,
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance,
} as VNode

// ...

return vnode
}

可以看到返回的虚拟DOM也是一个普通对象,我们传进去的组件选项会存储在type属性上。

render

虚拟DOM创建完后就会调用render方法将虚拟DOM渲染为实际的DOM节点,render方法就是在baseCreateRenderer中创建的,通过参数传给createAppAPI的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const render: RootRenderFunction = (vnode, container, namespace) => {
// 新vnode不存在
if (vnode == null) {
// 卸载
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建或者更新组件
patch(
container._vnode || null, // 如果是首次渲染,container._vnode不存在,那么直接将新VNode渲染为DOM元素即可
vnode,
container,
null,
null,
null,
namespace,
)
}
if (!isFlushing) {
isFlushing = true
flushPreFlushCbs()
flushPostFlushCbs()
isFlushing = false
}
// 缓存 vnode 节点,表示已经渲染
container._vnode = vnode
}

如果要渲染的新VNode不存在,那么从容器元素上获取之前VNode进行卸载,否则调用patch方法进行打补丁,如果是首次渲染,container._vnode不存在,那么直接将新VNode渲染为DOM元素即可,否则会对比新旧VNode,使用diff算法进行打补丁,Vue2中使用的是双端diff算法,Vue3中使用的是快速diff算法。

patch

看看render方法里调用的patch方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const patch: PatchFn = (
n1, // n1 表示旧的vnode,当n1为null时就表示是一次挂载(挂载or更新由n1决定)
n2, // n2 表示新的vnode,根据n2的type进行不同的处理
container, // 渲染后会将vnode渲染到container上
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
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: //... 处理文本节点
case Comment: //... 处理注释节点
case Static: //... 处理静态节点
case Fragment: //... 处理Fragment组件节点
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理普通 DOM 元素
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理 TELEPORT
// ...
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理 SUSPENSE
// ...
} 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 挂载 DOM,一个是根据新旧 vnode 更新 DOM。

patch的多个参数中,重点关注前三个:

  1. 第一个参数 n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次挂载的过程;

  2. 第二个参数 n2 表示新的 vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑;

  3. 第三个参数 container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面。

switch里面根据VNode的类型不同做的处理也不同,因为我们的例子传的是一个组件选项对象,所以会走processComponent处理分支:

processComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
n2.slotScopeIds = slotScopeIds
// n1等于null,表示挂载组件
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
namespace,
optimized,
)
} else {
// 调用mountComponent挂载组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
optimized,
)
}
} else { // n1不为null,表示更新组件
updateComponent(n1, n2, optimized)
}
}

根据是否存在旧的VNode判断是调用挂载方法还是更新方法,先看mountComponent方法:

mountComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component

// 1.调用ComponentInternalInstance创建组件的实例
const instance: ComponentInternalInstance =
compatMountInstance ||
// 调用createComponentInstance函数创建一个实例对象,其属性皆为没有值
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense,
))

// ...

// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
// 2.设置组件实例
// 初始化组件, 主要是对组件的props/slots进行初始化处理
// 执行setup 生成render函数(所以setup是在所有选项式API钩子之前调用 包括beforeCreate)
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}

// setup() is async. This component relies on async logic to be resolved
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
// ...
} else {
// 3.调用设置和运行有副作用的渲染函数

// 1 创建一个组件更新函数
// 1.1 render获得vnode
// 1.2 patch(oldVnode, newVnode)
// 2 创建更新机制 new ReactiveEffect(更新函数)
// 执行渲染副作用函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized,
)
}

if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
  1. 调用ComponentInternalInstance创建组件的实例
  2. 设置组件实例,初始化组件 处理setup的两个参数, 执行setup 生成render函数(所以setup是在所有选项式API钩子之前调用 包括beforeCreate)
  3. 调用设置和运行有副作用的渲染函数

下面我们依次来看这三个方法:

1. createComponentInstance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null,
) {
const type = vnode.type as ConcreteComponent
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext

const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext,
// 还有非常多属性
// ...
}
// ...

return instance
}
2. setupComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false,
) {
isSSR && setInSSRSetupState(isSSR)

// vnode的props和子元素
const { props, children } = instance.vnode
// 是否是有状态的组件
const isStateful = isStatefulComponent(instance)
// 初始化props
initProps(instance, props, isStateful, isSSR)
// 初始化slots
initSlots(instance, children)
// 执行setupStatefulComponent获取setupResult
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) // 执行setup
: undefined

isSSR && setInSSRSetupState(false)
return setupResult
}

可以看到setupComponent会初始化props和slots,然后执行setupStatefulComponent,这里主要是执行setup函数,并返回结果

setupStatefulComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean,
) {
// 组件描述对象
const Component = instance.type as ComponentOptions

// ...

// 0. create render proxy property access cache
// 创建render proxy属性访问缓存 作用是缓存访问过的属性
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
// 代理ctx,拦截ctx的属性访问 从而实现取值的优先级:setupState > data > props > ctx
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup()
const { setup } = Component
if (setup) {
// 创建setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)

// instance赋值给currentInstance
// 设置当前实例为instance 为了在setup中可以通过getCurrentInstance获取到当前实例
// 同时开启instance.scope.on()
const reset = setCurrentInstance(instance)
// 暂停tracking 暂停收集副作用函数
pauseTracking()
// 执行setup
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[
__DEV__ ? shallowReadonly(instance.props) : instance.props,
setupContext,
],
)
// 重新开启副作用收集
resetTracking()
// currentInstance置为空
// activeEffectScope赋值为instance.scope.parent
// 同时instance.scope.off()
reset()

if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
if (__DEV__ && !instance.suspense) {
const name = Component.name ?? 'Anonymous'
warn(
`Component <${name}>: setup function returned a promise, but no ` +
`<Suspense> boundary was found in the parent component tree. ` +
`A component with async setup() must be nested in a <Suspense> ` +
`in order to be rendered.`,
)
}
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`,
)
}
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}

在这个方法里会调用组件选项的setup方法,这个函数中返回的对象会暴露给模板和组件实例,看一下handleSetupResult方法:

handleSetupResult
1
2
3
4
5
6
7
8
function handleSetupResult(instance, setupResult, isSSR) {
if (isFunction(setupResult)) {
instance.render = setupResult;
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance, isSSR);
}

如果setup返回的是一个函数,那么这个函数会直接被作为渲染函数。否则如果返回的是一个对象,会使用proxyRefs将这个对象转为Proxy代理的响应式对象。

finishComponentSetup

最后又调用了finishComponentSetup方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function finishComponentSetup(instance, isSSR) {
const Component = instance.type;
if (!instance.render) {
if (!isSSR && compile && !Component.render) {
const template = Component.template ||
resolveMergedOptions(instance).template;
if (template) {
const { isCustomElement, compilerOptions } = instance.appContext.config;
const { delimiters, compilerOptions: componentCompilerOptions } = Component;
const finalCompilerOptions = extend(extend({
isCustomElement,
delimiters
}, compilerOptions), componentCompilerOptions);
Component.render = compile(template, finalCompilerOptions);
}
}
instance.render = (Component.render || NOOP);
}
}

这个函数主要是判断组件是否存在渲染函数render,如果不存在则判断是否存在template选项,我们传的组件选项显然是没有render属性,而是传的模板template,所以会使用compile方法来将模板编译成渲染函数。

3. setupRenderEffect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode, // 组件 vnode
container,
anchor,
parentSuspense,
namespace: ElementNamespace,
optimized,
) => {
// 组件更新方法
const componentUpdateFn = () => {}

// create reactive effect for rendering
// 创建一个effect, 将componentUpdateFn更新方法传入响应式更新方法
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
NOOP,
() => queueJob(update),
instance.scope, // track it in component's effect scope
))

// 组件更新函数
const update: SchedulerJob = (instance.update = () => {
if (effect.dirty) {
effect.run()
}
})
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)

if (__DEV__) {
effect.onTrack = instance.rtc
? e => invokeArrayFns(instance.rtc!, e)
: void 0
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
update.ownerInstance = instance
}
// 执行组件更新
update()
}

这一步就涉及到Vue3的响应式原理了,核心就是使用Proxy拦截数据,然后在属性读取时将属性和读取该属性的函数(称为副作用函数)关联起来,然后在更新该属性时取出该属性关联的副作用函数出来执行。

简化后的ReactiveEffect类就是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let activeEffect;
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn;
}
run() {
activeEffect = this;
try {
return this.fn();
} finally {
activeEffect = null
}
}
}

执行它的run方法时会把自身赋值给全局的activeEffect变量,然后执行副作用函数时如果读取了Proxy代理后的对象的某个属性时就会将对象、属性和这个ReactiveEffect示例关联存储起来,如果属性发生改变,会取出关联的ReactiveEffect实例,执行它的run方法,达到自动更新的目的。

componentUpdateFn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
const componentUpdateFn = () => {
// 判断组件是否已经挂载
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
// 生命周期和父instance
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
// ...
if (el && hydrateNode) {
// ssr 相关
} else {
if (__DEV__) {
startMeasure(instance, `render`)
}
// 执行render函数获得subTree(也是一个vnode) 将subTree挂载到instance上 以供更新使用
const subTree = (instance.subTree = renderComponentRoot(instance)) // 整个组件渲染生成 DOM 对应的 vnode 树
// ...
// patch subTree初次挂载
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
// el同步到initialVNode
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
// ...
// 组件已经挂载
instance.isMounted = true

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}

// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
// 组件更新
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined

if (next) {
next.el = vnode.el
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}

// 执行render函数获得nextTree
const nextTree = renderComponentRoot(instance)

// 获取老的subTree
const prevTree = instance.subTree
instance.subTree = nextTree

// patch新旧节点更新组件
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
// ...
}
}

这里的patch操作其实就是调用本章开头的那个patch,可以看到patch其实是一个递归操作,这里patch subtree如果根组件的根元素是组件则会继续执行processComponent,如果是一个element元素则会执行processElement,processElement中会处理children,又会调用patch,如此递归直到整个组件挂载完成。

组件无论是首次挂载,还是更新,做的事情核心是一样的,先调用renderComponentRoot方法生成组件模板的虚拟DOM,然后调用patch方法打补丁。

renderComponentRoot
1
2
3
4
5
6
7
function renderComponentRoot(instance) {
const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx, inheritAttrs } = instance;
// 执行render函数
// render函数内部会通过_createVNode或者_createElementVNode等函数进一步生成子vnode
let result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx))
return result
}

renderComponentRoot核心就是调用组件的渲染函数render方法生成组件模板的虚拟DOM,然后扔给patch方法更新就好了。

updateComponent

看完了mountComponent方法,再来看看updateComponent方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const updateComponent = (n1, n2, optimized) => {
const instance = (n2.component = n1.component);
// 根据新旧子组件 vnode 判断是否需要更新子组件
if (shouldUpdateComponent(n1, n2, optimized)) {
// 新的子组件 vnode 赋值给 instance.next
instance.next = n2;
// 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
invalidateJob(instance.update)
// 执行子组件的副作用渲染函数
instance.update();
}else {
// 不需要更新
n2.el = n1.el;
instance.vnode = n2;
}
}

先调用shouldUpdateComponent方法判断组件是否需要更新,主要是通过检测和对比组件 vnode 中的 props、chidren、dirs、transiton 等属性,来决定子组件是否需要更新。

如果需要更新,那么会执行instance.update方法,这个方法就是前面setupRenderEffect方法里保存的effect.run方法,所以最终执行的也是componentUpdateFn方法。

updateComponentPreRender
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const updateComponentPreRender = (
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean,
) => {
// 新组件 vnode 的 component 属性指向组件实例
nextVNode.component = instance
// 旧组件 vnode 的 props 属性
const prevProps = instance.vnode.props
// 组件实例的 vnode 属性指向新的组件 vnode
instance.vnode = nextVNode
// 清空 next 属性,为了下一次重新渲染准备
instance.next = null
// 更新 props
updateProps(instance, nextVNode.props, prevProps, optimized)
// 更新 插槽
updateSlots(instance, nextVNode.children, optimized)

pauseTracking()
// props update may have triggered pre-flush watchers.
// flush them before the render update.
flushPreFlushCbs(instance)
resetTracking()
}

processElement

1
2
3
4
5
6
7
8
9
10
11
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
//挂载元素节点
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
//更新元素节点
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}

该函数的逻辑很简单,如果 n1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。

我们接着来看挂载元素的 mountElement 函数的实现:

mountElement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
let el
const { type, props, shapeFlag } = vnode
// 创建 DOM 元素节点
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
if (props) {
// 处理 props,比如 class、style、event 等属性
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], isSVG)
}
}
}
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
// 处理子节点是纯文本的情况
hostSetElementText(el, vnode.children)
}
else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 处理子节点是数组的情况
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
}
// 把创建的 DOM 元素节点挂载到 container 上
hostInsert(el, container, anchor)
}

可以看到,挂载元素函数主要做四件事:创建 DOM 元素节点、处理 props、处理 children、挂载 DOM 元素到 container 上。

首先是创建 DOM 元素节点,通过 hostCreateElement 方法创建,这是一个平台相关的方法,我们来看一下它在 Web 环境下的定义:

1
2
3
4
function createElement(tag, isSVG, is) {
isSVG ? document.createElementNS(svgNS, tag)
: document.createElement(tag, is ? { is } : undefined)
}

它调用了底层的 DOM API document.createElement 创建元素,如果是其他平台比如 Weex,hostCreateElement 方法就不再是操作 DOM ,而是平台相关的 API 了,这些平台相关的方法是在创建渲染器阶段作为参数传入的。

创建完 DOM 节点后,接下来要做的是判断如果有 props 的话,给这个 DOM 节点添加相关的 class、style、event 等属性,并做相关的处理,这些逻辑都是在 hostPatchProp 函数内部做的,这里就不展开讲了。

接下来是对子节点的处理,我们知道 DOM 是一棵树,vnode 同样也是一棵树,并且它和 DOM 结构是一一映射的。

如果子节点是纯文本,则执行 hostSetElementText 方法,它在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本:

1
2
3
function setElementText(el, text) {
el.textContent = text
}

如果子节点是数组,则执行 mountChildren 方法:

mountChildren
1
2
3
4
5
6
7
8
9
10
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
for (let i = start; i < children.length; i++) {
// 预处理 child
const child = (children[i] = optimized
? cloneIfMounted(children[i])
: normalizeVNode(children[i]))
// 递归 patch 挂载 child
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}

子节点的挂载逻辑同样很简单,遍历 children 获取到每一个 child,然后递归执行 patch 方法挂载每一个 child 。

可以看到,mountChildren 函数的第二个参数是 container,而我们调用 mountChildren 方法传入的第二个参数是在 mountElement 时创建的 DOM 节点,这就很好地建立了父子关系。

另外,通过递归 patch 这种深度优先遍历树的方式,我们就可以构造完整的 DOM 树,完成组件的渲染。

处理完所有子节点后,最后通过 hostInsert 方法把创建的 DOM 元素节点挂载到 container 上,它在 Web 环境下这样定义:

1
2
3
4
5
6
7
8
function insert(child, parent, anchor) {
if (anchor) {
parent.insertBefore(child, anchor)
}
else {
parent.appendChild(child)
}
}

这里会做一个 if 判断,如果有参考元素 anchor,就执行 parent.insertBefore ,否则执行 parent.appendChild 来把 child 添加到 parent 下,完成节点的挂载。

因为 insert 的执行是在处理子节点后,所以挂载的顺序是先子节点,后父节点,最终挂载到最外层的容器上。

总结

image-1

到这里,从我们创建实例到页面渲染,再到更新的全流程就讲完了,总结一下,大致就是:

  1. 每个Vue组件都需要产出一份虚拟DOM,也就是组件的render函数的返回值,render函数你可以直接手写,也可以通过template传递模板字符串,由Vue内部来编译成渲染函数,平常我们开发时写的Vue单文件,最终也会编译成普通的Vue组件选项对象;

  2. render函数会作为副作用函数执行,也就是如果在模板中使用到了响应式数据(所谓响应式数据就是能拦截到它的各种读取、修改操作),那么响应式数据和属性会与render函数关联起来,那么当响应式数据被修改以后,就能找到依赖它的render函数,那么就可以通知依赖的组件进行更新;