Vue3.x源码阅读笔记(二)-组件挂载
首先明确一点,vnode是在组件instance中存在的。
组件挂载方法主要在mountComponent中。
这里的创建组件实例主要是通过函数去创建的,在Vue2当中,是通过实例化类的方式去创建的
1 | |
主要是有三个步骤:
- 创建组件实例
- 设置组件实例
- 设置并且运行带副作用的渲染函数
创建组件实例-createComponentInstance
vue2通过new Vue初始化一个组件的实例,Vue3通过创建对象的形式,两者并无本质区别。接受一个vnode、parent。
可以看到,通过对象创建了组件实例,上面有非常多的属性。
这样就完成了组件的上下文、根组件指针以及派发事件方法的设置。
1 | |
设置组件实例-setupComponent
上面通过创建对象的形式,给当前的组件设置了初始化的实例,接下来就是设置组件实例,因为上面虽然初始化了instance,但是很多属性都是空的,所以需要给这些空的组件实例属性去初始化。
这里对我们上面 createComponentInstance返回的instance做处理
- 从
vnode中取出props、children,初始化Props和Slots - 判断是不是一个有状态组件,如果是的话设置有状态组件的实例
什么是有状态组件?简单的来讲就是有自己维护内部数据的组件称之为有状态组件,否则就是无状态组件。
1 | |
有状态组件实例-setupStatefulComponent
当我们从组件instance.vnode.shapeFlag中判断这个组件是一个有状态组件,那么就会执行setupStatefulComponent()进一步去设置有状态组件实例。
如果是有状态组件,做了四件事情
- 创建渲染代理的属性访问缓存
- 创建渲染上下文代理
- 判断处理 setup 函数
- 完成组件实例设置,兼容2.x版本
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// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 获取到当前组件的对象
/**
* data: ƒ data()
setup: setup() { const count = ref(1); const divRef = ref() onMounted(() => {…}
template: "\n <p>\n </p><div ref=\"divRef\">\n 454654{{}}\n </div>\n <p></p>\n <p></p>\n "
__emits: null
__props: []
*/
const Component = instance.type as ComponentOptions
// 1.创建渲染代理的属性访问缓存
instance.accessCache = Object.create(null)
// 2.创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 3.判断处理 setup 函数
const { setup } = Component
if (setup) {
// 如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 执行 setup 函数,获取结果
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
// 处理 setup 执行结果
handleSetupResult(instance, setupResult)
}
else {
// 4.完成组件实例设置,兼容2.x
finishComponentSetup(instance)
}
}1. 创建渲染代理的属性访问缓存
首先是创建渲染上下文代理,在创建之前,可以看到还创建了一层缓存,这是 因为组件在渲染时候会经常触发get函数,其中会经常用到hasOwn判断当前的key在不在某个类型的数据中,会非常浪费性能。所以主要作用是用于缓存渲染器代理属性,减少读取次数。1
2
3
4
5
6
7
8
9
// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 1.创建渲染代理的属性访问缓存
instance.accessCache = Object.create(null)
}2. 创建渲染上下文代理
这层代理其实类似Vue2中的代理,比如说,data上的数据被代理在this._data上面,访问data中的this.msg,其实是在访问this._data.msg。props中的数据 存储在this._props等等。
在Vue3中,我们将setupState、ctx、data、props属性访问代理到instance.ctx中。
当我们访问instance.ctx时候,就会访问到PublicInstanceProxyHandlers这个函数当中。
1 | |
PublicInstanceProxyHandlers这个函数,当我们在访问、设置、查询instance.ctx,也就是setupState、ctx、data、props渲染上下文上的每个属性,这个时候会触发。
- 访问会触发
get() - 设置会触发
set() - 查询会触发
has()
在访问时候会触发其中的get函数
1 | |
可以看到,主要首先判断当前的key是不是以$开头,如果是以$开头,那么证明访问的是公共的一个API。公共API如下
1 | |
不是$开头的就代表是data,props,ctx中的一种,ctx 包括了计算属性、组件方法和用户自定义的一些数据。
然后尝试先从accessCache代理缓存中获取,如果key存在,那么依次从setup、data、context、props中获取。这里的位置顺序很重要,如果在**setup**和**data**中同时定义了一个相同的值,那么,最终就只会取**setup**中的值,比如:
1 | |
上面代码块中的setup中的 msg和data的中msg相同,但是这里按照权重只会获取setup中的值。
缓存中没有获取到就会通过hasOwn一层一层判断,找到并且存入accessCache缓存中再返回。
最后如果是$开头,就代表是一个公共属性,类似于$watch、$parent、$nextTick、$ref、$attr等这些公共属性,然后判断是不是 vue-loader 编译注入的 css 模块内部的 key,然后判断是不是用户自定义以$开头的属性,判断是不是全局属性等等,最终找不到会报错为Property xxx was accessed during render but is not defined on instance.
当我们设置修改instance中的ctx时候,会触发set函数。和get函数一样,设置也会按照权重依次设置,有相同的值如果setup中的 msg和data的中msg相同,也只会设置setup中的。
这里依次判断是不是setup、data、props等,如果是props在dev下不能直接修改,并且不能给$开头的内部属性赋值,最后如果是用户自定义数据,会被保留到ctx上下文当中。
1 | |
has较少使用,也比较简单,就是依次判断是否存在于 accessCache、data、setupState、props 、用户数据、公开属性以及全局属性中
1 | |
3. 判断处理 setup 函数
上面完成了设置渲染代理缓存和创建渲染上下文代理,接下来就是回到刚刚到setupStatefulComponent()中、继续往下执行setup()。
在执行setup()函数中,执行了几个关键的步骤
创建
setupContext函数上下文执行
setup()函数处理
setup()结果完成组件实例设置
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// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 1.创建渲染代理的属性访问缓存
instance.accessCache = Object.create(null)
// 2.创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
//
const { setup } = Component
if (setup) {
/**
* const fn = (a,b) => {}
* fn.length ? fn.length = 1
*/
// 如果 setup 函数带参数,则创建一个 setupContext
const setupContext = (instance.setupContext =
// 有参数的话创建setup上下文,上下文就是 emit,attr,slots,创建一个setupContext
setup.length > 1 ? createSetupContext(instance) : null)
// 执行 setup 函数获取结果
// 执行setup函数,,做异常处理,传入props,emit,获取结果,就是我们写的return
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
resetTracking()
// 设置完setup,销毁组件实例
currentInstance = null
// 如果返回了一个promise
if (isPromise(setupResult)) {
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult: unknown) => {
// .then之后再执行
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
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
// 处理setup结果
handleSetupResult(instance, setupResult, isSSR)
}
} else {
// 设置render函数,vue版本判断,将template转换为render并且挂载在instance中,类似vue2做的事情
// 完成组件初始化
finishComponentSetup(instance, isSSR)
}
}3.1 创建
setupContext函数上下文这里判断了当前是否存在
setup.length并且大于1,存在的话,创建SetupContext。setup.length是什么?其实就是函数参数的个数,函数中有一个参数length就是1,有2两个参数length就是2。这里就是说,当我们写setup(props,{attrs, slots, emit})函数时候,如果存在写了第二个参数,就会为第二个参数创建上下文。也就是setupContext。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 1.创建渲染代理的属性访问缓存
instance.accessCache = Object.create(null)
// 2.创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
const { setup } = Component
// 3. 判断处理setup函数
if (setup) {
// 3.1 创建setupContext函数上下文
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
}
}createSetupContext()实现也很简单,就是返回了一个包含attrs, slots, emit的对象,就是setupContext。可以让我们在setup内部获取组件的属性、插槽以及派发事件的方法emit。1
2
3
4
5
6
7
8
9// packages/runtime-core/src/component.ts
function createSetupContext (instance) {
// 这里返回的attrs,slots,emit.也就是setupContext上下文
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit
}
}3.2 执行
setup()函数再给当前的
setup第二个参数创建完成上下文,接下来就是执行setup()函数了。可以看到,函数通过callWithErrorHandling进行执行处理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// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 1.创建渲染代理的属性访问缓存
instance.accessCache = Object.create(null)
// 2.创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
const { setup } = Component
// 3. 判断处理setup函数
if (setup) {
// 3.1 创建setupContext函数上下文
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 3.2 执行setup()函数
// 执行setup函数,,做异常处理,传入props,emit,获取结果,就是我们写的return
// __DEV__是指,在本地开发时候,props是只读的
// 通过callWithErrorHandling执行的好处是保证有容错处理不会导致页面渲染失败
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION, // 0
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
}
}callWithErrorHandling是用来执行函数,保证不会因为异常导致代码执行不下去。在这里的一个参数就是props,第二个是setupContext。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 用来执行函数,如果有异常则抛出异常
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
// 存在参数则将参数传入fn中,否则就只执行fn()
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type) // 监听异常函数处理
}
return res
}3.3 处理
setup()结果上面几步创建了
setupContext上下文,执行了setup函数,现在会拿到我们在组件中写的setup函数返回的结果,交给handleSetupResult1
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// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 1.创建渲染代理的属性访问缓存
instance.accessCache = Object.create(null)
// 2.创建渲染上下文代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
const { setup } = Component
// 3. 判断处理setup函数
if (setup) {
// 3.1 创建setupContext函数上下文
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 3.2 执行setup()函数
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION, // 0
[instance.props, setupContext]
)
// 3.3 处理setup结果
handleSetupResult(instance, setupResult, isSSR)
}
}在这个
handleSetupResult函数中
首先判断setup返回的结果是不是一个函数,如果是函数,就认为是一个render渲染函数,比如可以这么写setup(){ return ()=>(h("div")) }。
再然后判断是不是一个对象,对象的话,把这个对象变成响应式,赋值给instance.setupState, 这样前面的代理就可以从instance.setupState获取到数据了。
最后,如果是setup没有返回任何结果,那么就会提示一个警告⚠️,告诉你应该返回东西。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// packages/runtime-core/src/component.ts
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) { // setup返回了一个render渲染函数
// 如果返回的是 function 的话,那么绑定到 render 上
// 认为是 render 逻辑
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)){
// 返回的是一个对象的话
// 先存到 setupState 上
instance.setupState = proxyRefs(setupResult)
} else if (__DEV__ && setupResult !== undefined){
warn(
`setup() should return an object. Received: ${
setupResult === null ? 'null' : typeof setupResult
}`
)
}
// 完成组件实例设置
finishComponentSetup(instance, isSSR)
}3.4. 完成组件实例设置,兼容2.x版本
最终在完成了创建
setupContext上下文,执行setup函数,处理setup结果这几部之后,最后就是通过调用finishComponentSetup()完成组件实例设置。
这个函数主要做了两件事情:编译模板
兼容2.x的
Options API1
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// packages/runtime-core/src/component.ts
function finishComponentSetup (instance) {
// 获取到组件对象
const Component = instance.type
// 对模板或者渲染函数的标准化
if (!instance.render) {
// compile是编译函数
if (compile && !Component.render) {
// 运行时编译
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement || NO
})
Component.render._rc = true
}
if ((process.env.NODE_ENV !== 'production') && !Component.render) {
if (!compile && Component.template) {
// 只编写了 template 但使用了 runtime-only 的版本
warn(`Component provided template option but ` +
`runtime compilation is not supported in this build of Vue.` +
(` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
) /* should not happen */)
}
else {
// 既没有写 render 函数,也没有写 template 模板
warn(`Component is missing template or render function.`)
}
}
// 组件对象的 render 函数赋值给 instance
instance.render = (Component.render || NOOP)
if (instance.render._rc) {
// 对于使用 with 块的运行时编译的渲染函数,使用新的渲染上下文的代理
instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
}
}
// 兼容 Vue.js 2.x Options API
{
currentInstance = instance
applyOptions(instance, Component)
currentInstance = null
}
}首先是模板编译过程。
compile是什么呢?其实就是模板编译的函数。这里需要说明的是,Vue的两个版本:runtime版本,开发方式是不借助 webpack 编译,直接引入 Vue.js,运行时编译runtime-only版本,使用 SFC(Single File Components).vue单文件的开发方式来开发组件,通过webpack的vue-loader来处理,把template部分转换成render函数添加到组件对象的属性中。
这两个版本,最大的区别就是是否有compile这个函数,如果有,那么就是runtime版本。在Vue3中,是通过registerRuntimeCompiler函数去注册的。
1 | |
这里的流程主要是如果 compile 有值,表示是一个**runtime**版本,并且组件没有 render 函数,那么就需要把 template 编译成 render 函数,并且将编译好的**render**设置到**instance.render**上。否则会报一个警告,告诉用户不可以在**runtime-only**版本中写**template**。
剩下就是兼容2.x的options API。这里跳过。
无状态组件
无状态组件直接返回undefined
1 | |
设置并且运行副作用渲染函数-setupRenderEffect
经过前面的步骤创建组件实例和设置组件实例之后,剩下就是运行副作用渲染函数setupRenderEffect。可以理解为,当数据发生变化时候,这个函数就会重新被执行。
当我们组件实例中的isMounted没有挂载时候,先执行了beforeMount的钩子,然后主要做了两件事情。
渲染组件生成 subTree
把 subTree 挂载到 container 中。
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// packages/runtime-core/src/renderer.ts
// 运行副作用渲染函数
const setupRenderEffect: SetupRenderEffectFn = (
// 组件实例
instance,
// 需要挂载的vnode
initialVNode,
// 挂载容器
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
// beforeMount hook
// 执行beformMount钩子
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
/**
* 我们在父组件可以通过@hook:生命周期监听子组件变化
*/
instance.emit('hook:beforeMount')
}
// 渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 把子树 vnode 挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
// 保留渲染生成的子树根 DOM 节点
initialVNode.el = subTree.el
instance.isMounted = true
// mounted hook
// 执行mounted钩子
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
if ((vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
parentSuspense
)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
parentSuspense
)
}
// 设置为已经挂载
instance.isMounted = true
} else {
// 更新组件
}
}
}render执行()-渲染组件生成 subTree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const setupRenderEffect: SetupRenderEffectFn = (
// 组件实例
instance,
// 需要挂载的vnode
initialVNode,
// 挂载容器
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
}组件
vnode和子树vnode,这两个的概念是什么呢?当我们在一个App.vue里面写入了另外一个Hello.vue组件时候
这个里面的hello组件就是initialVNode,就是组件vnode,但是组件vnode不能挂载,我们页面中挂载的是DOM的vnode。
这里就是将组件vnode转换为真实DOM的vnode过程1
2
3
4
5
6
7
8
9<!-- App.vue >
<template>
<div class="app">
<p>This is an app.</p>
<!-- 子组件,App组件生成VNode,这里的子组件就是initialVNode,也叫组件VNode >
<!-- 是一个抽象节点,并非真实的DOM元素 >
<hello></hello>
</div>
</template>子组件
Hello.vue-subTree
在Hello.vue里面就是subTree1
2
3
4
5
6
7<!-- 这里是子组件生成的subTree,真实的DOM VNode >
<!-- renderComponentRoot生成的 >
<template>
<div class="hello">
<p>Hello, Vue 3.0!</p>
</div>
</template>每一个组件都会有
render函数,无论是template还是别的,最终都会被转化为render函数,renderComponentRoot就是执行render函数去生成组件DOM的vnode。
比如App.vue和Hello.vue,会执行两次这里的函数。第一次instance就是App.vue,将App.vue组件的vnode转换为真实DOM的vnode(subTree)。
这里就是需要注意下组件**vnode**(initialVNode)和DOM的Vnode(subTree),是通过renderComponentRoot转换的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const setupRenderEffect: SetupRenderEffectFn = (
// 组件实例
instance,
// 需要挂载的vnode
initialVNode,
// 挂载容器
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
instance.update = effect(function componentEffect() {
// 渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
})
}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// packages/runtime-core/src/componentRenderUtils.ts
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
// 有状态组件。执行render.call生成vnode
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
result = normalizeVNode(
// 执行组件的render函数
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
} else {
// functionalv 函数式组件
const render = Component as FunctionalComponent
result = normalizeVNode( // render函数中写了几个参数,一个的话传入props,否则传入props,{attr,slots, emit}
render.length > 1
? render(
props,
__DEV__
? {
get attrs() {
markAttrsAccessed()
return attrs
},
slots,
emit
}
: { attrs, slots, emit }
)
: render(props, null as any /* we know it doesn't need it */)
)
}
......
}可以看一下
App.vue在调用renderComponentRoot之前和之后的变化。调用之前,
instance有一个vnode,那是组件的vnode(initialVNode),subTree为空调用之后,可以看到已经执行了
render函数,生成了真实DOM对应的vnode(subTree)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// App.vue的instance对象,renderComponentRoot之前-initialVNode
{
appContext:{...},
ctx:{setup中的数据变量:xx,$开头的公共属性...}
// 组件vnode
render: ƒ render(_ctx, _cache),
vnode:{
type:{
render: ƒ render(_ctx, _cache)
setup: ƒ setup()
template: "<div>{{b}}</div>"
__emits: null
__props: []
[[Prototype]]: Object
__v_isVNode: true
__v_skip: true
},
},
// 真实的dom vnode
subTree:null
}
// App.vue的instance对象,renderComponentRoot之后-subTree
{
appContext:{...},
ctx:{setup中的数据变量:xx,$开头的公共属性...}
// 组件vnode
render: ƒ render(_ctx, _cache),
vnode:{
type:{
render: ƒ render(_ctx, _cache)
setup: ƒ setup()
template: "<div>{{b}}</div>"
__emits: null
__props: []
[[Prototype]]: Object
__v_isVNode: true
__v_skip: true
},
}
// 真实的dom vnode
subTree:{
anchor: null
appContext: null
children: "b"
component: null
dirs: null
dynamicChildren: []
dynamicProps: null
el: null
key: null
patchFlag: 1
props: null
ref: null
scopeId: null
shapeFlag: 9
slotScopeIds: null
ssContent: null
ssFallback: null
staticCount: 0
suspense: null
target: null
targetAnchor: null
transition: null
type: "div"
__v_isVNode: true
__v_skip: true
}
}把 subTree 挂载到 container 中
执行
renderComponentRoot()方法中的render()函数生成subTree,接下来就是将subTree挂载到页面的过程,通过patch来实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const setupRenderEffect: SetupRenderEffectFn = (
// 组件实例
instance,
// 需要挂载的vnode
initialVNode,
// 挂载容器
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
instance.update = effect(function componentEffect() {
// 渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 把子树 vnode 挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
})
}回到之前第一节里面的
patch过程,第一节在执行switch流程中,会执行processComponent,而现在这里,我们的子树vnode是一个普通元素vnode,所以会执行processElement()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
63const patch = (
// n1是旧的vnode,n1旧节点不存在表示挂载,初次渲染为空
n1,
// n2是新的vnode,后续会根据这个vnode执行不同的处理逻辑
n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 存在新旧vnode,并且新旧vnode不相同,说明是一次更新过程,不考虑子节点复用,先销毁旧节点
// isSameVNodeType 对比vnode的type和key
// 如果说,一个元素由UL变成了div,那么就会直接删除原来的节点,直接去做挂载
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// patchFlag的概念,用来标记一个节点应该如何进行diff更新策略
// 没有patchFlag,执行全量diff
/**
* 如果新的vnode patchFlag是-2,表示退出优化模式。进行全量diff
*/
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, shapeFlag } = n2
/** type:第一次是用户写的内容,是一个Object,走default的流程
* data: data()
mounted: mounted()
setup: setup(const count = ref(1)...)
template: "\n <p>\n </p>"
*/
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
// 处理组件,刚开始渲染的App.vue也是一个组件对象,经过转换再次执行会变成普通dom
// 一般情况下,第一次渲染的就是一个组件对象,然后执行里面的组件挂载流程
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT组件
// ...
}
else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE组件
// ...
}
}
}processElement
processElement是用来处理DOM的VNode的。将VNode转换为真实的DOM。接受参数为新、老VNode,如果老的VNode不存在,证明是挂载节点,走mountElement,否则新老节点都存在,走patchElement更新流程1
2
3
4
5
6
7
8
9
10
11
12
13// packages/runtime-core/src/renderer.ts
// n1是旧的vnode,n2是新的vnode,
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
// n1不存在,挂载子节点
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
// 否则更新子节点
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}挂载子节点-mountElement
如果我们页面初始化,第一次挂载,就会先执行
mountElement1
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// packages/runtime-core/src/renderer.ts
const mountElement = (
// 挂载的vnode
vnode: VNode,
// 容器
container,anchor,parentComponent,parentSuspense,isSVG: boolean,slotScopeIds,optimized
) => {
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
// 创建DOM元素
el = vnode.el = hostCreateElement(vnode.type as string,isSVG,props && props.is,props)
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)
}
// 如果元素有props,则初始化处理props上面的class,style,event等属性
// packages\runtime-dom\src\patchProp.ts
if (props) {
for (const key in props) {
/**
* 有些props是Vue自身用到的
* 需要过滤掉vue自身用的key
// 比如生命周期相关的 key: beforeMount、mounted
*/
if (!isReservedProp(key)) {
// 更新Props
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
if (dirs) { // dirs是指令,如果存在指令,执行指令的beforeMount钩子,Vue3中,指令的生命周期修改的和Vue2生命周期类似
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// 把创建的 DOM 元素节点挂载到 container 上
hostInsert(el, container, anchor)
}这里主要做了几件事情:
创建
DOM元素处理
children数组处理
props挂载
DOM1. 创建
DOM元素通过调用
hostCreateElement创建DOM元素。1
2
3
4
5
6
7
8
9
10
11// packages/runtime-core/src/renderer.ts
const mountElement = (
// 挂载的vnode
vnode: VNode,
// 容器
container,anchor,parentComponent,parentSuspense,isSVG: boolean,slotScopeIds,optimized
) => {
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
// 1. 创建DOM元素
el = vnode.el = hostCreateElement(vnode.type as string,isSVG,props && props.is,props)
}可以看到,这里是通过
document.createElement创建的,然后返回创建好的元素。1
2
3
4
5
6
7
8
9
10
11
12// packages/runtime-dom/src/nodeOps.ts
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},2. 处理
Children数组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// packages/runtime-core/src/renderer.ts
const mountElement = (
// 挂载的vnode
vnode: VNode,
// 容器
container,anchor,parentComponent,parentSuspense,isSVG: boolean,slotScopeIds,optimized
) => {
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
// 1. 创建DOM元素
el = vnode.el = hostCreateElement(vnode.type as string,isSVG,props && props.is,props)
// 2. 处理Children数组
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 8
// 节点是文本子节点 packages\runtime-dom\src\nodeOps.ts
/** 这里只需要设置一下即可
* render(){
return h("div",{},"test")
}
*/
/**
* setElementText: (el, text) => {
el.textContent = text
},
*/
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 16
// 子节点是数组
/**
*render(){
return h("div",{},[h("p"),h(Hello)])
}
*/
// 遍历并且递归调用patch,深度遍历,最后子节点会被先挂载,其次是父节点
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized || !!vnode.dynamicChildren
)
}
}如果当前节点只是一个
TEXT节点,直接设置就可以了。
否则如果子节点是数组,则执行mountChildren方法: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// 子节点是数组的话挂载逻辑,遍历递归完成
const mountChildren = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
// 遍历子节点,调用patch
for (let i = start; i < children.length; i++) {
// 预处理 child
// 编译优化相关
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
// 递归patch,挂载children,深度优先遍历
/**
* 为什么是调用patch函数,而不是mountElement?
* 因为子节点可能是其他类型vnode,嵌套组件
* <App>
* <Hello>
* <OtherComponents/>
* <Hello/>
* <App/>
*/
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}这里是通过遍历循环
children执行patch的。所以子节点挂载顺序是深度优先遍历,通过这种方式递归构建一颗DOM树,通过将container参数传入,也建立了节点间的父子关系。3. 处理
props等属性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
26if (props) {
for (const key in props) {
/**
* 有些props是Vue自身用到的
* 需要过滤掉vue自身用的key
// 比如生命周期相关的 key: beforeMount、mounted
*/
if (!isReservedProp(key)) {
// 更新Props
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}在处理当前
DOM VNode时候,如果有props属性,那么就会去处理props,style,class等属性。这里暂时跳过具体处理内容,对应源码在packages/runtime-dom/src/patchProp.ts中4. 挂载
DOM1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// packages/runtime-core/src/renderer.ts
const mountElement = (
// 挂载的vnode
vnode: VNode,
// 容器
container,anchor,parentComponent,parentSuspense,isSVG: boolean,slotScopeIds,optimized
) => {
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
// 1. 创建DOM元素
el = vnode.el = hostCreateElement(vnode.type as string,isSVG,props && props.is,props)
// 2. 处理Children数组
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 8
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(...)
}
// 3.处理props
if (props){...}
// 4.挂载DOM
hostInsert(el, container, anchor)
}最终会调用
hostInsert(el, container, anchor)方法,通过parent.insertBefore把创建的DOM元素节点挂载到container上。整理挂载顺序,先子后父。1
2
3
4// packages/runtime-dom/src/nodeOps.ts
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},更新子节点-patchElement
更新的部分,放在后面“组件的更新”里面。
总结
创建好vnode,会交给render函数执行,render如果没有vnode执行卸载流程,否则执行patch流程,patch会在switch判断当前是哪个类型的需要patch,如果在patch时候发现是一个组件,那么就会走processComponents流程,处理组件流程。
必须要有一个明确的认识。这里是在处理组件,设置我们组件实例,比如说我们在xxx.vue中打印this就是当前组件的实例
思考
setup函数执行时机
模板编译时机
组件兼容2.x和options api是在哪里做的
我们使用 callWithErrorHandling 把 setup 包装了一层,它有哪些好处?
mounted钩子函数是在哪里执行的
render函数执行的时机
构建dom是深度优先还是广度优先?为什么?
本文标题:Vue3.x源码阅读笔记(二)-组件挂载
文章作者:Niuhk
发布时间:2022-04-19
最后更新:2022-07-23
原始链接:https://www.niuhk.cn/2022/04/19/Vue3.x源码阅读笔记(二)-组件挂载/
版权声明:转载请注明出处!
分享