首先明确一点,vnode是在组件instance中存在的。
组件挂载方法主要在mountComponent中。
这里的创建组件实例主要是通过函数去创建的,在Vue2当中,是通过实例化类的方式去创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// packages/runtime-core/src/renderer.ts
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建组件实例,vue2中,所有的组件通过extend Vue构造函数new Vue来实现,Vue3通过创建对象方式
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 1. 创建组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 2. 设置组件实例
setupComponent(instance)
// 3. 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

主要是有三个步骤:

  • 创建组件实例
  • 设置组件实例
  • 设置并且运行带副作用的渲染函数

    创建组件实例-createComponentInstance

    vue2通过new Vue初始化一个组件的实例,Vue3通过创建对象的形式,两者并无本质区别。接受一个vnodeparent
    可以看到,通过对象创建了组件实例,上面有非常多的属性。
    这样就完成了组件的上下文、根组件指针以及派发事件方法的设置。
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// packages/runtime-core/src/component.ts
function createComponentInstance (/**需要创建的vnode*/vnode, parent, suspense) {
// 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
// 组件唯一 id
uid: uid++,
// 组件 vnode
vnode,
// 父组件实例
parent,
// app 上下文
appContext,
// vnode 节点类型,如果是组件那type就是组件
type: vnode.type,
// 根组件实例
root: null,
// 新的组件 vnode
next: null,
// 子节点 vnode
subTree: null,
// 带副作用更新函数
update: null,
// 渲染函数
render: null,
// 渲染上下文代理
proxy: null,
// 带有 with 区块的渲染上下文代理
withProxy: null,
// 响应式相关对象
effects: null,
// 依赖注入相关
provides: parent ? parent.provides : Object.create(appContext.provides),
// 渲染代理的属性访问缓存
accessCache: null,
// 渲染缓存
renderCache: [],
// 渲染上下文
ctx: EMPTY_OBJ,
// data 数据
data: EMPTY_OBJ,
// props 数据
props: EMPTY_OBJ,
// 普通属性
attrs: EMPTY_OBJ,
// 插槽相关
slots: EMPTY_OBJ,
// 组件或者 DOM 的 ref 引用
refs: EMPTY_OBJ,
// setup 函数返回的响应式结果
setupState: EMPTY_OBJ,
// setup 函数上下文数据
setupContext: null,
// 注册的组件
components: Object.create(appContext.components),
// 注册的指令
directives: Object.create(appContext.directives),
// suspense 相关
suspense,
// suspense 异步依赖
asyncDep: null,
// suspense 异步依赖是否都已处理
asyncResolved: false,
// 是否挂载
isMounted: false,
// 是否卸载
isUnmounted: false,
// 是否激活
isDeactivated: false,
// 生命周期,before create
bc: null,
// 生命周期,created
c: null,
// 生命周期,before mount
bm: null,
// 生命周期,mounted
m: null,
// 生命周期,before update
bu: null,
// 生命周期,updated
u: null,
// 生命周期,unmounted
um: null,
// 生命周期,before unmount
bum: null,
// 生命周期, deactivated
da: null,
// 生命周期 activated
a: null,
// 生命周期 render triggered
rtg: null,
// 生命周期 render tracked
rtc: null,
// 生命周期 error captured
ec: null,
// 派发事件方法
emit: null
}
// 初始化渲染上下文
instance.ctx = { _: instance }
// 初始化根组件指针
instance.root = parent ? parent.root : instance
// 初始化派发事件方法
instance.emit = emit.bind(null, instance)
return instance
}

设置组件实例-setupComponent

上面通过创建对象的形式,给当前的组件设置了初始化的实例,接下来就是设置组件实例,因为上面虽然初始化了instance,但是很多属性都是空的,所以需要给这些空的组件实例属性去初始化。

这里对我们上面 createComponentInstance返回的instance做处理

  • vnode中取出propschildren,初始化PropsSlots
  • 判断是不是一个有状态组件,如果是的话设置有状态组件的实例

什么是有状态组件?简单的来讲就是有自己维护内部数据的组件称之为有状态组件,否则就是无状态组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function setupComponent(instance) {// createComponentInstance中返回的instance对象
// 1. 处理 props
// 取出存在 vnode 里面的 props
const { props, children } = instance.vnode;
initProps(instance, props);
// 2. 处理 slots
initSlots(instance, children);

// 源码里面有两种类型的 component
// 一种是基于 options 创建的
// 还有一种是 function 的
// 这里处理的是 options 创建的
// 叫做 stateful 类型
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) // 有状态组件,只有有options选项,这些状态组件设置实例才有意义,设置有状态组件实例
: undefined
isInSSRComponentSetup = false
// 不是有状态组件直接返回undefined
return setupResult
}
// 是否是有状态组件的判断
export function isStatefulComponent(instance: ComponentInternalInstance) {
return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
}

有状态组件实例-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.msgprops中的数据 存储在this._props等等。
    Vue3中,我们将setupState、ctx、data、props属性访问代理到instance.ctx中。

当我们访问instance.ctx时候,就会访问到PublicInstanceProxyHandlers这个函数当中。

1
2
3
4
5
6
7
8
9
10
11

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

PublicInstanceProxyHandlers这个函数,当我们在访问、设置、查询instance.ctx,也就是setupState、ctx、data、props渲染上下文上的每个属性,这个时候会触发。

  • 访问会触发get()
  • 设置会触发set()
  • 查询会触发has()

在访问时候会触发其中的get函数

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// packages/runtime-core/src/componentPublicInstance.ts
const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const {
ctx,
setupState,
data,
props,
accessCache,
type,
appContext
} = instance

let normalizedProps
// 上面的 publicPropertiesMap 列举了部分以$开头的属性,$开头的属性跳过。剩下的不以$开头的属性这部分数据可能是setupState(setup函数返回的数据),data,props,ctx中的一种
if (key[0] !== '$') { //依次判断当前是哪个key,// 说明不是访问 public api,如果以$开头的是公共api
const n = accessCache![key]// 获取到当前代理缓存中的值
// 当前的值先从代理缓存中尝试获取,没有的话再去对应的里面去取
if (n !== undefined) {
// 依次判断下面的key的来源,比如setup,data中,ctx中,props中,这里的位置顺序很重要,如果在data和setup定义了两个相同的key,那么会优先获取setup中的
switch (n) {
case AccessTypes.SETUP: // 来自setup中
return setupState[key]
case AccessTypes.DATA: // 来自data中
return data[key]
case AccessTypes.CONTEXT: // 来自context中
return ctx[key]
case AccessTypes.PROPS: // 来自props
return props![key]
// default: just fallthrough
}
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
// 代理缓存没找到,从setupState中获取数据,并且再缓存起来
accessCache![key] = AccessTypes.SETUP // 0
return setupState[key]
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
// 代理缓存没找到,从data中获取数据,并且再缓存起来
accessCache![key] = AccessTypes.DATA // 1
return data[key]
} else if (
// only cache other properties when instance has declared (thus stable)
// props
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
// 代理缓存没找到,从props中获取数据,并且再缓存起来
accessCache![key] = AccessTypes.PROPS // 2
return props![key]
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// 代理缓存没找到,从ctx中获取数据,并且再缓存起来
accessCache![key] = AccessTypes.CONTEXT // 3
return ctx[key]
} else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
// setup,data,props,ctx都没有取到
accessCache![key] = AccessTypes.OTHER // 4
}
}

// 类似$watch,$nextTick,$attrs,$root函数
const publicGetter = publicPropertiesMap[key]
let cssModule, globalProperties
// public $xxx properties
// 公开的$xxx方法,publicPropertiesMap中的比如$watch,$nextTick,$attrs
if (publicGetter) { // 访问的公共属性
if (key === '$attrs') {
track(instance, TrackOpTypes.GET, key) //
__DEV__ && markAttrsAccessed()
}
return publicGetter(instance)
} else if (
// css module (injected by vue-loader)
// css 模块,通过 vue-loader 编译的时候注入
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])
) {
// css中的变量,这里应该是可以直接从css中获取值的变量?
return cssModule
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// user may set custom properties to `this` that start with `$`
// 用户自定义的属性,也用 `$` 开头
accessCache![key] = AccessTypes.CONTEXT // 3
return ctx[key]
} else if (
// global properties
// 全局属性
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))
) {
if (__COMPAT__) {
const desc = Object.getOwnPropertyDescriptor(globalProperties, key)!
if (desc.get) {
return desc.get.call(instance.proxy)
} else {
const val = globalProperties[key]
return isFunction(val) ? val.bind(instance.proxy) : val
}
} else {
return globalProperties[key]
}
} else if (
__DEV__ &&
currentRenderingInstance &&
(!isString(key) ||
// #1091 avoid internal isRef/isVNode checks on component instance leading
// to infinite warning loop
// 避免检查实例导致无限循环
key.indexOf('__v') !== 0)
) {
if (
data !== EMPTY_OBJ &&
(key[0] === '$' || key[0] === '_') &&
hasOwn(data, key)
) {
// 如果在 data 中定义的数据以 $或者_ 开头,会报警告,因为 $ 是保留字符,不会做代理
warn(
`Property ${JSON.stringify(
key
)} must be accessed via $data because it starts with a reserved ` +
`character ("$" or "_") and is not proxied on the render context.`
)
} else if (instance === currentRenderingInstance) {
// // 在模板中使用的变量如果没有定义,报警告
warn(
`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`
)
}
}

可以看到,主要首先判断当前的key是不是以$开头,如果是以$开头,那么证明访问的是公共的一个API。公共API如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// packages/runtime-core/src/componentPublicInstance.ts
const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {
$: i => i,
$el: i => i.vnode.el,
$data: i => i.data,
$props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
$attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
$slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
$refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
$parent: i => getPublicInstance(i.parent),
$root: i => getPublicInstance(i.root),
$emit: i => i.emit,
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
$forceUpdate: i => () => queueJob(i.update),
$nextTick: i => nextTick.bind(i.proxy!),
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap)

不是$开头的就代表是data,props,ctx中的一种,ctx 包括了计算属性、组件方法和用户自定义的一些数据。
然后尝试先从accessCache代理缓存中获取,如果key存在,那么依次从setup、data、context、props中获取。这里的位置顺序很重要,如果在**setup****data**中同时定义了一个相同的值,那么,最终就只会取**setup**中的值,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<p>{{msg}}</p>
</template>
<script>
import { ref } from 'vue'
export default {
data() {
return {
msg: 'msg from data'
}
},
setup() {
const msg = ref('msg from setup')
return {
msg
}
}
}
</script>

上面代码块中的setup中的 msgdata的中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中的 msgdata的中msg相同,也只会设置setup中的。
这里依次判断是不是setup、data、props等,如果是propsdev下不能直接修改,并且不能给$开头的内部属性赋值,最后如果是用户自定义数据,会被保留到ctx上下文当中。

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
// packages/runtime-core/src/componentPublicInstance.ts
const PublicInstanceProxyHandlers: ProxyHandler<any> = {
set(
{ _: instance }: ComponentRenderContext,
key: string,
value: any
): boolean {
const { data, setupState, ctx } = instance
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
// 给setupState赋值
setupState[key] = value
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
// 给data赋值
data[key] = value
} else if (hasOwn(instance.props, key)) {
// props赋值会报错,不能直接给props赋值
__DEV__ &&
warn(
`Attempting to mutate prop "${key}". Props are readonly.`,
instance
)
return false
}
if (key[0] === '$' && key.slice(1) in instance) {
// 不能给$开头的Vue内部属性赋值,比如$parent,$attrs,$nextTick
__DEV__ &&
warn(
`Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`,
instance
)
return false
} else {
// 全局属性
if (__DEV__ && key in instance.appContext.config.globalProperties) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
value
})
} else {
// 用户自定义数据保留在ctx中
// 比如说
/**
* created(){
* 这个userMsg没有在data或者setup中赋值,但是也会被保留下来
* this.userMsg = 'msg from user'
* }
*/
ctx[key] = value
}
}
return true
}
}

has较少使用,也比较简单,就是依次判断是否存在于 accessCache、data、setupState、props 、用户数据、公开属性以及全局属性中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// packages/runtime-core/src/componentPublicInstance.ts
const PublicInstanceProxyHandlers: ProxyHandler<any> = {
has(
{
_: { data, setupState, accessCache, ctx, appContext, propsOptions }
}: ComponentRenderContext,
key: string
) {
let normalizedProps
// 依次判断是否存在于 accessCache、data、setupState、props 、用户数据、公开属性以及全局属性中
return (
accessCache![key] !== undefined ||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
(setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key)
)
}
}

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函数返回的结果,交给handleSetupResult

    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
    // 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 API

    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/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单文件的开发方式来开发组件,通过webpackvue-loader来处理,把 template 部分转换成 render 函数添加到组件对象的属性中。

这两个版本,最大的区别就是是否有compile这个函数,如果有,那么就是runtime版本。在Vue3中,是通过registerRuntimeCompiler函数去注册的。

1
2
3
4
5
6
7
8
9
let compile: CompileFunction | undefined

/**
* For runtime-dom to register the compiler.
* Note the exported method uses any to avoid d.ts relying on the compiler types.
*/
export function registerRuntimeCompiler(_compile: any) {
compile = _compile
}

这里的流程主要是如果 compile 有值,表示是一个**runtime**版本,并且组件没有 render 函数,那么就需要把 template 编译成 render 函数,并且将编译好的**render**设置到**instance.render**上。否则会报一个警告,告诉用户不可以在**runtime-only**版本中写**template**
剩下就是兼容2.x的options API。这里跳过。

无状态组件

无状态组件直接返回undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function setupComponent(
// 通过createComponentInstance创建好的组件实例
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
// 从VNode获取子节点和props
const { props, children } = instance.vnode
// 通过位运算得到
// 无论是有状态组件还是无状态组件,都是可以有props和slots的
const isStateful = isStatefulComponent(instance) // 是否是一个有状态组件
// 初始化Props
initProps(instance, props, isStateful, isSSR)
// 初始化slots
initSlots(instance, children)

// 设置有状态组件的实例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) // 有状态组件,只有有options选项,这些状态组件设置实例才有意义,设置有状态组件实例
: undefined
isInSSRComponentSetup = false
// 无状态组件直接返回undefined
return setupResult
}

设置并且运行副作用渲染函数-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
    16
    const 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不能挂载,我们页面中挂载的是DOMvnode
    这里就是将组件vnode转换为真实DOMvnode过程

    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里面就是subTree

    1
    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函数去生成组件DOMvnode
    比如App.vueHello.vue,会执行两次这里的函数。第一次instance就是App.vue,将App.vue组件的vnode转换为真实DOMvnode(subTree)
    这里就是需要注意下组件**vnode**(initialVNode)DOM的Vnode(subTree),是通过renderComponentRoot转换的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const 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
    22
    const 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
    63
    const 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是用来处理DOMVNode的。将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

    如果我们页面初始化,第一次挂载,就会先执行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
    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

  • 挂载DOM

    1. 创建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
    26
    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 ((vnodeHook = props.onVnodeBeforeMount)) {
    invokeVNodeHook(vnodeHook, parentComponent, vnode)
    }
    }

    在处理当前DOM VNode时候,如果有props属性,那么就会去处理props,style,class等属性。这里暂时跳过具体处理内容,对应源码在packages/runtime-dom/src/patchProp.ts

    4. 挂载DOM
    1
    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是深度优先还是广度优先?为什么?