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

结合Vue3-demo项目分析源码(五),完成组件的渲染,结束mounted生命周期。此时可以在页面看到内容了。当添加事件修改数据时,Vue进入组件更新过程。

分析

MVVM模型

mvvm模型

  • Model(模型):表示应用程序核心

  • View(视图):显示数据(数据库记录)

  • ViewModel(视图模型):暴露公共属性和命令的视图的抽象

按登陆demo理解:

App.vue:

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
<template>
<div>
<!-- 登录成功后的提示 -->
<div v-if="isLogin">欢迎你:{{ user.name }}</div>
<!-- 登录操作 -->
<div v-else>
用户名:
<input name="name" v-model="user.name" />
<br />
密码:
<input name="pwd" v-model="user.pwd" type="password" />
<br />
<button @click="onLogin">去登录</button>
</div>
</div>
</template>
<script>
import { ref, reactive } from "vue";
import * as UserModel from "./UserModel";
export default {
name: "app",
props: ["msg"],
setup() {
let isLogin = ref(false); // 是否已经登录
let user = reactive({
name: "", // 用户名
pwd: "", // 密码
});
const onLogin = () => {
// 去登录
UserModel.login(user.name, user.pwd).then(
(res) => {
// 登录成功
if (res) {
isLogin.value = true;
}
},
(errorMsg) => {
// 登录失败
alert(errorMsg);
}
);
};
return {
isLogin,
user,
onLogin,
};
},
};
</script>

UserModel.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 登录业务逻辑
* @param {*} name 用户名
* @param {*} pwd 密码
*/
export function login(name, pwd) {
return new Promise((resolve, reject) => {
if ("123456" === pwd) {
resolve(true);
} else {
reject("用户名或密码错误");
}
});
}

成功:

2AiGTI.png

失败:

2AiYkt.png

由上,UserModelMVVMModel,用来处理用户的业务逻辑;App.vue组件为MVVMView,作为视图显示内容;而setupStateMVVMViewModel,用来驱动视图显示。

分析实现代码

ref()

由上,let isLogin = ref(false);isLogin是经过ref方法返回的对象。当点击登陆按钮时,isLogin.value属性被设置成true

查看文件vue-next/packages/reactivity/src/ref.ts第41行:

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
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
...
//创建并返回一个RefImpl对象
return new RefImpl(rawValue, shallow)
}
//RefImpl对象
class RefImpl<T> {
private _value: T

public readonly __v_isRef = true

constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}

get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}

set value(newVal) {
//对比新老数据,不一样时set
if (hasChanged(toRaw(newVal), this._rawValue)) {
//将新数据赋给原始值
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
//调用trigger方法告诉Vue有数据更新
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}

trigger()

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

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 trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
//根据target获取所有依赖当前target的组件
const depsMap = targetMap.get(target)
...
//调用组件update方法
const run = (effect: ReactiveEffect) => {
...
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
//循环遍历所有组件的update方法
effects.forEach(run)
}

由上,trigger方法获取了依赖isLogin的所有组件depsMap,并遍历所有组件执行这些组件的update方法。update方法即在结合Vue3-demo项目分析源码(五)中提到的setupRenderEffect中由effect方法定义的instance.update方法

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
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
//定义组件的update方法,通过响应式来调用渲染组件
instance.update = effect(function componentEffect() {
//组件第一次渲染
if (!instance.isMounted) {
...
//若不是第一次渲染
} else {
...
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
// beforeUpdate hook
//调用beforeUpdate生命周期
if (bu) {
invokeArrayFns(bu)
}
// 调用vnode的onVnodeBeforeUpdate钩子函数
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
...
// 获取一个新节点
const nextTree = renderComponentRoot(instance)
// 老节点
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
)
...
next.el = nextTree.el
// 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)
}

由于更新非首次渲染,故调用了beforeUpdate生命周期。接着使用onVnodeBeforeUpdate钩子函数,其作用与beforeUpdate生命周期类似。然后update调用renderComponentRoot方法,根据组件的不同类型调用render方法创建并返回一个vnode节点,即上述代码的nextTree变量。然后将新节点nextTree和老节点prevTree传入patch方法对比

由于更新后改变的元素是v-ifv-else绑定的两个div节点,属于ELEMENT类型,按结合Vue3-demo项目分析源码(二)中分析,ELEMENT类型调用方法processElement。由于更新非第一次渲染,故按结合Vue3-demo项目分析源码(五)中分析,非首次渲染调用patchElement方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) {
...
} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}

patchElement()

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

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
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
...
if (patchFlag > 0) {
// the presence of a patchFlag means this element's render code was
// generated by the compiler and can take the fast path.
// in this path old node and new node are guaranteed to have the same shape
// (i.e. at the exact same position in the source template)
//依次对比两个元素的属性值,判断有不一样的属性时替换新属性
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
//对比两个元素的class属性
// class
// this flag is matched when the element has dynamic class bindings.
if (patchFlag & PatchFlags.CLASS) {
//class有变化时替换新的class
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}

// style
// this flag is matched when the element has dynamic style bindings
//对比两个元素的style属性有没有不同
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}

// props
// This flag is matched when the element has dynamic prop/attr bindings
// other than class and style. The keys of dynamic prop/attrs are saved for
// faster iteration.
// Note dynamic keys like :[foo]="bar" will cause this optimization to
// bail out and go through a full diff because we need to unset the old key
//对比两个元素的属性值有没有不同
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
if (
next !== prev ||
(hostForcePatchProp && hostForcePatchProp(el, key))
) {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}

// text
// This flag is matched when the element has only dynamic text children.
//对比两个元素的文本内容有没有不同
if (patchFlag & PatchFlags.TEXT) {
//若有不同,则替换成新的文本
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
}
...
}

上述代码表示,当出现两个新老节点时,会判断两个元素的classstyle文本值TEXT等是否一致,若有不同,则替换为新的属性。其中,判断是否一致的逻辑即为差分算法

如上述对比两个元素的sytle属性,调用方法hotPatchProp方法

查看文件vue-next/packages/runtime-dom/src/patchProp.ts第16行

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
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
switch (key) {
...
case 'style':
patchStyle(el, prevValue, nextValue)
break
...
}
}
// 对比 style
export function patchStyle(el: Element, prev: Style, next: Style) {
const style = (el as HTMLElement).style
// 如果新节点没有 style 属性的话就直接移除 style 属性
if (!next) {
el.removeAttribute('style')
// 如果新节点的 style 属性是原始文本类型
} else if (isString(next)) {
// 判断两个 style 属性文本是否一致
if (prev !== next) {
style.cssText = next
}
} else {
// 如果新节点中 style 属性是一个对象形式
for (const key in next) {
// 设置 style 属性
setStyle(style, key, next[key])
}
if (prev && !isString(prev)) {
for (const key in prev) {
if (next[key] == null) {
setStyle(style, key, '')
}
}
}
}
}

差分算法

概念:

a数列:a[1],a[2],a[3],...,a[n]

创建b数列,令b[i] = a[i] - a[i-1],则b[1] = a[1]b[2] = a[2] - a[1]

a[i] = b[1] + b[2] + ... + b[i]

a[i] = a[1] + a[2] - a[1] + a[3] - a[2] + ... + b[i] - b[i-1]

因此,b数组为a数组的差分数组,a数组为b数组的前缀和数组

以上,Vue3源码分析结束。

附录-Vue3流程图

vue流程图

a[l,r]+c

b[l] + c = a[l] - a[l-1] + c

b[r] = a[r] - a[r-1]

b[r+1] - c = a[r+1] - a[r] -c

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