结合Vue3-demo项目分析源码(五)

结合Vue3-demo项目分析源码(四),完成组件的创建,包含创建组件实例createComponentInstance和设置组件实例setupComponent两个步骤。整个组件的创建过程涉及到两个生命周期beforeCreatedcreated,到created才算组件创建完成,此时进入组件的渲染过程。回到mountComponent()方法,查看方法setupRenderEffect()

1
2
3
4
5
6
7
8
9
10
11
12
13
const mountComponent: MountComponentFn = (
...
) => {
//创建组件实例
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(...))
...
//设置组件实例
setupComponent(instance)
...
//处理setup方法
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense,isSVG, optimized)
...
}

分析

setupRenderEffect()

查看文件vue-next/packages/runtime-core/src/renderer.ts第1323行

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
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
//定义组件的update方法,通过响应式来调用渲染组件
instance.update = effect(function componentEffect() {
//组件第一次渲染
if (!instance.isMounted) {
...
// beforeMount hook
//调用beforeMount生命周期
if (bm) {
invokeArrayFns(bm)
}
// 调用Vnode的onVnodeBeforeMount钩子函数
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}

// render
//渲染组件
...
const subTree = (instance.subTree = renderComponentRoot(instance))
if (__DEV__) {
endMeasure(instance, `render`)
}

if (el && hydrateNode) {
...
} else {
...
//使用patch方法对比新老节点
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
...
// 设置vnode的el属性
initialVNode.el = subTree.el
}
// mounted hook
//调用mounted生命周期
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// 调用Vnode节点的onVnodeMounted钩子函数
if ((vnodeHook = props && props.onVnodeMounted)) {
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, initialVNode)
}, parentSuspense)
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
//若是keep-alive类型的组件就调用activated生命周期
const { a } = instance
if (
a &&
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
) {
queuePostRenderEffect(a, parentSuspense)
}
instance.isMounted = true
//组件渲染完成

//若不是第一次渲染
} else {
...
// beforeUpdate hook
//调用beforeUpdate生命周期
if (bu) {
invokeArrayFns(bu)
}
// 调用vnode的onVnodeBeforeUpdate钩子函数
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}

// render
...
//使用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
)
...
// updated hook
//调用updated生命周期
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
// 调用vnode的onVnodeUpdated钩子函数
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, next!, vnode)
}, parentSuspense)
}
...
}
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}

使用effect方法给当前组件实例instance提供了update方法

effect()

查看文件vue-next/packages/reactivity/src/effect.ts第55行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
//创建一个响应式的effect方法,即fn的高阶函数
const effect = createReactiveEffect(fn, options)
//若不是懒加载模式
if (!options.lazy) {
//直接调用effect方法,最后调用传递的fn方法
effect()
}
return effect
}

调用effect方法返回一个高阶函数,高阶函数内部调用的是传入的函数,即setupRenderEffect中传入的componentEffect。最终把高阶函数传递给instance的update属性。若不指定懒加载lazy=true则,effect会立即调用传入的componentEffect方法。

beforeMount生命周期

当组件为第一次渲染时,首先调用生命周期beforeMount方法

生命周期beforeMountcreated之后,但两者差别不大,平时可以在这两个生命周期做一些渲染前的准备工作。如网络请求,页面初始数据的操作等

renderComponentRoot()

调用完生命周期beforeMount,进入到组件渲染,首先调用renderComponentRoot方法

查看文件vue-next/packages/runtime-core/src/componentRenderUtils.ts第45行

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
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
...
try {
let fallthroughAttrs
//若为有状态组件(非函数式、element)
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
//调用当前组件的render方法获取vnode节点
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
} else {
//函数式节点
// functional
const render = Component as FunctionalComponent
// in dev, mark attrs accessed if optional props (attrs === props)
if (__DEV__ && attrs === props) {
markAttrsAccessed()
}
//直接调用函数获取vnode节点,第一个参数为props,第二个参数为[attrs,slots,emit]
result = normalizeVNode(
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 */)
)
fallthroughAttrs = Component.props
? attrs
: getFunctionalFallthrough(attrs)
}

// attr merging
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result
let setRoot: ((root: VNode) => void) | undefined = undefined
if (__DEV__ && result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) {
;[root, setRoot] = getChildRoot(result)
}
//给当前节点设置继承的属性
if (Component.inheritAttrs !== false && fallthroughAttrs) {
const keys = Object.keys(fallthroughAttrs)
const { shapeFlag } = root
if (keys.length) {
...
root = cloneVNode(root, fallthroughAttrs)

}
...
}

// inherit directives
//给当前节点设置继承的指令
if (vnode.dirs) {
...
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
}
// inherit transition data
//给当前节点设置继承的过渡数据
if (vnode.transition) {
...
root.transition = vnode.transition
}
...
}
...
currentRenderingInstance = null
return result
}

该方法通过调用当前组件的render方法创建并返回了vnode节点,在调用render方法时分函数式组件普通组件两种情况调用

函数式组件

定义一个函数式组件

1
2
3
4
5
6
7
8
9
10
11
/**
* 定义一个函数式组件
* @param {*} props 组件的参数
* @param {*} renderContext 当前组件的上下文对象
*/
import { h } from 'vue';
import App from './App';
export default function AppFunComponent(props,{attrs,slots,emit}){
//返回App.vue组件的vnode节点
return h(App,props);
}

使用:

1
2
3
4
5
import { createApp } from "vue";
// import App from "./App"; // 引入 App.vue 组件
import App from "./AppFunComponent"; // 引入 AppFunComponent 函数式组件
const app = createApp(App, { msg: "hello Vue 3" }); // 创建 App 根组件
app.mount("#app"); // 渲染 App

函数式组件属于无状态组件,即没有datasetupState等响应式的对象,比较轻量级,渲染起来比普通组件快。故当组件不需要datasetupState也能运行时,可以创建函数式组件,会提高效率

inheritAttrs

给函数组件添加一个子节点

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 定义一个函数式组件
* @param {*} props 组件的参数
* @param {*} renderContext 当前组件的上下文对象
*/
import { h } from 'vue';
import App from './App';
export default function AppFunComponent(props,{attrs,slots,emit}){
//返回App.vue组件的vnode节点
// return h(App, props);
return h(App,{...props,msg1:"继承属性"});
}

当运行时,子节点属性msg1被绑定到根节点<div>

继承属性

若不想让Vue自动绑定这些继承的属性,可以将inheritAttrs设为false

1
2
3
4
5
export default {
inheritAttrs: false,
name: "app",
props: ["msg", "testWord","user"],
}

当组件中不需要自动绑定属性到根节点,则建议将组件的inheritAttr设为false,可以优化性能。因为Vue默认会绑定所有继承的属性到组件的根节点。

由上,调用renderComponentRoot即调用其render方法获取到一个ELEMENT类型的vnode节点给subTree对象,并使用patch方法进行新老节点的对比。

processElement()

结合Vue3-demo项目分析源码(二)文章中提过的patch方法的,当节点类型为ELEMENT类型时,调用方法processElement

1
2
3
4
5
6
7
8
9
10
11
12
13
//普通的dom节点-div、span等
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}

查看文件vue-next/packages/runtime-core/src/renderer.ts第655行

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
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
//组件第一次渲染时
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}

mountElement()

查看文件vue-next/packages/runtime-core/src/renderer.ts第681行

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 mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const {
type,
props,
shapeFlag,
transition,
scopeId,
patchFlag,
dirs
} = vnode
...
//当vnode的el不为空
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// If a vnode has non-null el, it means it's being reused.
// Only static vnodes can be reused, so its mounted DOM nodes should be
// exactly the same, and we can simply do a clone here.
// only do this in production since cloned trees cannot be HMR updated.
// 创建 el 属性
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 创建 el 属性
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is
)
...

// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
//如果当前元素的子元素为文本
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
//若子元素不是文本
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
//循环遍历子元素
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
optimized || !!vnode.dynamicChildren
)
}
...
}
...
//插入当前元素到container中
hostInsert(el, container, anchor)
...
}

按demo,当第一次渲染App.vue并返回一个<div>元素节点时。

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
//main.js
const app = createApp(App,{
msg:"hello,这是组件渲染demo",
onVnodeBeforeMount: ()=>{
console.log("这是绑定在vnode节点上的beforeMount钩子函数")
}
});
app.mount("#app")//渲染app,将其挂载到id=app的节点上
//App.vue
<template>
<!-- 组件渲染demo -->
<div>{{msg}}</div>
</template>
<script>
// import { getCurrentInstance, onMounted, ref,h } from 'vue';
// import Hello from "./Hello"
export default {
inheritAttrs: false,
name: "app",
props: ["msg", "testWord","user"],
beforeMount(){
console.log("我是beforeMount生命周期");
}
};
</script>

按源码,首先调用hostCreateElement方法创建div元素,由于msg为文本类型,故接着调用hostSetElementText方法将传递的msg字符串"hello,这是组件渲染demo"传递给divtextContent属性。最后调用方法hostInsert将该div元素插入到div#app根元素中。

按方法setupRenderEffect流程,调用完patch方法,会设置当前节点vnode的el属性,然后开始调用mounted生命周期。mounted执行完后整个组件的渲染过程结束。

附录-Vue3流程图

vue流程图

  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2020-2021 Aweso Lynn
  • PV: UV: