3-5、Vue3-核心源码讲解

结构更新

Vue3 的源码采用 TS + monorepo
为什么越来越多的项目选择 Monorepo? - 掘金

Vue3 的重大更新(breaking changes)

[科普文] Vue3 到底更新了什么?-腾讯云开发者社区-腾讯云

⭐️组合式 API

将 Vue2 的选项式更新为组合式

选项式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
export default {
data() {
return {
count: 1
}
},
mounted() {
this.count = 0
},
methods: {
addCount() {
this.count++
}
}
}
</script>

缺点

  1. 遵循语法写在特定区域:data、methods、computed、watch 等都是有固定语法的
  2. 当项目的负责度增加后,这些逻辑就会散落在代码的各处,不利于后期维护

69ee27d59f01075ea46a8639bdb254b0.gif

组合式

1
2
3
4
5
6
7
8
9
10
<script setup>
import { ref, onMounted } from "vue"
const count = ref(0)
const addCount = () => {
count.value++
}
onMounted(() => {
count.value = 0
})
</script>

d5ff314c398ab84cf0ff3c9894a09647.gif

优点

  1. 不需要遵循在特点区域写,可以按照逻辑一行行书写,就跟传统的 JS 代码写法一致,可以将相同的逻辑放在一起

⭐️响应式原理

Vue2:全部基于Object.defineProperty()get set实现。通过对data里面的数据递归处理,才能为每个属性增加getter setter,这样会有更高的性能开销,并且对于运行时动态新增/删除的属性无法自动处理为响应式
Vue3:基础类型基于对象的get|set实现,复杂类型则基于Proxy实现。
Proxy是 ES6 新增的 API,可以直接拦截对象上的所有操作,所以解决了vue2 中的运行时动态新增/删除的属性无法自动处理为响应式问题,并且减少了不必要的性能开销

其他新增功能

  1. Fragment允许组件返回多个根元素,减少层级

  2. slot插槽的增强与语法简化

  3. Suspense 组件异步内容加载组件,可以展示备用 UI

  4. Teleport 组件允许将元素渲染到 DOM 的任意位置

  5. 编译优化:优化了 VDOM 的对比算法

  6. TS 的支持

  7. tree-shaking 的支持

  8. 生命周期优化

  9. 等等…

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.createApp({
template: `
<div>
<h1>你好呀</h1>
<p>{{ msg }}</p>
<p v-if="array.length">{{ array.length }}</p>
</div>
`,
setup() {
const msg = Vue.ref('hello, my children')
const array = Vue.reactive([1, 2])

setTimeout(() => {
// debugger
msg.value = 'hello, my children~~~~~~'
}, 2000)

return { msg, array }
}
}).mount('#app')

初始化入口为:createApp 函数

初始化流程图

响应式原理

Ref 原理

完整源码:vue3-ref.ts
核心源码解析:

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
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}

// ref方法:里面调用 createRef 方法
export function ref(value?: unknown) {
return createRef(value, false)
}

// createRef方法:
function createRef(rawValue: unknown, shallow: boolean) {
// 当为 Ref 类型时表明是响应式了,所以直接返回
if (isRef(rawValue)) {
return rawValue
}

// 否则,调用 new RefImpl,并返回已处理为响应式的实例
return new RefImpl(rawValue, shallow)
}

// RefImpl:最核心的代码
class RefImpl<T> {
private _value: T
private _rawValue: T

public dep?: Dep = undefined
public readonly __v_isRef = true

constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
// ref 调用时,__v_isShallow 为 false,所以直接返回 value(ref 的传参)
// this._rawValue = value
this._rawValue = __v_isShallow ? value : toRaw(value)

// ref 调用时,__v_isShallow 为 false,所以直接返回 value(ref 的传参)
// this._value = value
this._value = __v_isShallow ? value : toReactive(value)
}

get value() {
// 在读取 实例.value 属性时触发:
// 1. 收集依赖
trackRefValue(this)
// 2. 返回值
return this._value
}

set value(newVal) {
// 在设置 实例.value 属性时触发:
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
// 1. 设置 _rawValue 的值
this._rawValue = newVal
// 2. 设置 _value 的值
this._value = useDirectValue ? newVal : toReactive(newVal)
// 3. 触发依赖
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}

总结:通过核心代码的解析,可以发现调用ref(0)后,最终返回的是个对象,传入的值是放在.value上的,并且通过get|set 函数实现响应式
所以这也是为什么const count = ref(0)后,使用/设置时要用count.value = 1
但在<template>里面可以省略.value,因为Vue框架在<template>解析编译时,自动加上了value

Reactive 原理

完整源码:vue3-reactive.ts
核心源码解析:

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
const user = reactive({ name: 'xx' })

// reactive方法:调用 createReactiveObject 方法
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}

// createReactiveObject:最核心的代码
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
// 边缘检测 --- start
// 传参不为对象时,警告,原值返回
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target) // COMMON:Object|Array;COLLECTION:Map|WeekMap|Set|WeekSet
if (targetType === TargetType.INVALID) {
return target
}
// 边缘检测 --- end

// 调用 new Proxy,进行数据代理
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, // handler 函数
)
proxyMap.set(target, proxy)
return proxy
}

总结:通过核心代码的解析,可以发现核心在于new Proxy,针对不同的复杂类型,使用不同的handler函数,针对性的处理get|set方法

依赖收集、触发流程与原理

当明白了数据能被改为响应式后,则需要研究下数据变化后为什么对应的页面/函数会执行呢?
这就涉及到依赖的收集与触发

关键词

  • 副作用函数
    • 会产生副作用的函数:使用/更改了函数外的变量的函数
1
2
3
4
5
const userInfo = { name: 'lisi' }

function getUserInfo() { // 副作用函数
return userInfo.name // 引用了外部变量
}
  • 响应式数据
    • 数据发生变化时,能触发其他使用该数据的同步变化,这种数据就被称为响应式数据
1
2
3
4
5
6
7
8
conts obj = { text: 'hello!' }

function effect() {
document.body.innerHTML = obj.text
}

obj.text = '你好'
// 当重新赋值'你好'后,如果该 effect 能自动重新执行,则 obj 就是响应式数据

实现思路(简易代码)

1
2
3
4
5
6
7
conts obj = { text: 'hello!' }

function effect() {
document.body.innerHTML = obj.text
}

obj.text = '你好'

通过上述例子代码观察可知:

  • 当副作用函数执行时,可以发现会触发obj.text读取操作
  • 当修改obj.text时,会触发obj.text设置操作

那我们是不是可以在读取设置时进行拦截呢?ES6 的 Proxy可以做代理
读取时,把对应的副作用函数收集存起来
设置时,把收集的副作用函数拿出来执行

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
const bucket = new Set(); // 存储副作用函数的“桶”

// 原始数据
const obj = { text: "hello!" };

// 对原始数据的代理
const data = new Proxy(obj, {
// 拦截读取操作
get(target, key) {
// 将副作用函数放入“桶”
bucket.add(effect);
console.log("[ bucket ] >", bucket);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newValue) {
// 设置属性值
target[key] = newValue;
// 将副作用函数拿出“桶”,并执行
bucket.forEach((fn) => fn());
},
});

function effect() {
document.body.innerHTML = data.text;
}

effect(); // 执行副作用函数,触发读取操作

setTimeout(() => {
data.text = "你好"; // 触发设置操作
}, 3000);

上面的代码就是一个简易的可运行的响应式原理(还存在很多设计问题)

完善的响应式

问题 1

副作用函数的命名被我们固定为effect了,真实情况可能是其他名字或匿名
解决:设计一个专门注册副作用函数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let activeEffect = undefined // 全局变量:存储被注册的副作用函数

// 注册副作用函数的函数
function effect(fn) {
// 存储传入的副作用函数
activeEffect = fn

// 执行该副作用函数
activeEffect()
}

// 注册副作用函数
effect(
// 一个匿名的副作用函数
() => {
document.body.innerHTML = data.text;
}
)

问题 2

当给响应式数据设置一个新值时,也会触发副作用函数的执行
解决:将副作用的存储与响应式数据的属性关联起来,存储就不能再使用Set

1
2
3
4
5
6
7
8
9
10
11
// 观察副作用函数
effect(
// 一个匿名的副作用函数
() => {
document.body.innerHTML = data.text;
}
)
可以得到一个关系:
data
-- text
-- effect

解决问题 1、问题 2 后的完善代码如下:

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
let activeEffect = undefined; // 全局变量:存储被注册的副作用函数

// 注册副作用函数的函数
function effect(fn) {
// 存储传入的副作用函数
activeEffect = fn;

// 执行该副作用函数
activeEffect();
}

const bucket = new WeakMap(); // 存储副作用函数的
// bucket 的数据结构为:{
// [target]: {
// [key]: [effect1, effect2, ...]
// // ...
// }
// }

// 对 Proxy 的封装
const reactive = (_obj) => {
// 对原始数据的代理
return new Proxy(_obj, {
// 拦截读取操作
get(target, key) {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}

let deps = depsMap.get(key);

if (!deps) deps = new Set();

deps.add(activeEffect);
depsMap.set(key, deps);
}

// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newValue) {
// 设置属性值
target[key] = newValue;

let depsMap = bucket.get(target);
if (!depsMap) return;

let deps = depsMap.get(key);
if (!deps) return;

deps.forEach((fn) => fn());
},
});
};

const data = reactive({ text: "hello!", name: "张三" });

function myEffect1() {
console.log("[ myEffect1() ] >");
document.body.innerHTML = data.text;
}

function myEffect2() {
console.log("[ myEffect2() ] >");
document.body.innerHTML = data.text + data.name;
}

// 注册副作用函数
effect(myEffect1);
effect(myEffect2);

setTimeout(() => {
console.log("[ setTimeout 3000 ] >");
data.pp = "你好!"; // 触发设置操作
}, 3000);

setTimeout(() => {
console.log("[ setTimeout 5000 ] >");
data.text = "你好!"; // 触发设置操作
}, 5000);

其中的bucket的数据结构如下:

在将上述完善后的代码的reactive函数再次完善下,可以得到越来越接近于 Vue3 源码的代码:

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

const bucket = new Map(); // 存储副作用函数的“桶”

// 依赖收集(追踪)
const track = (target, key) => {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}

let deps = depsMap.get(key);

if (!deps) deps = new Set();

deps.add(activeEffect);
depsMap.set(key, deps);
}
};

// 依赖触发
const trigger = (target, key) => {
let depsMap = bucket.get(target);
if (!depsMap) return;

let deps = depsMap.get(key);
if (!deps) return;

deps.forEach((fn) => fn());
};

const reactive = (_obj) => {
// 对原始数据的代理
return new Proxy(_obj, {
// 拦截读取操作
get(target, key) {
// 依赖收集(追踪)
track(target, key);

// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newValue) {
// 设置属性值
target[key] = newValue;

// 依赖触发
trigger(target, key);
},
});
};

// ...

问题 3

当使用过的属性不再使用时,已绑定的依赖项还会触发

1
2
3
4
5
6
7
8
9
10
11
effect(() => {
document.body.innerHTML = obj.success ? obj.msg : 'error'
})
// 当 success 为 true 时,则使用 msg,那对应的依赖项为:
obj(target)
-- success(key)
-- effect
-- msg(key)
-- effect
// 但当 success 为 false 时,则固定显示文本 'error',但 msg 已绑的依赖项未解除
// 则如果执行 obj.msg = 'xx',则还是会触发 effect 的执行,这完全是多余的

解决:给副作用函数增加一个属性,用于存储相关联的依赖项,在读取副作用时先断开联系,等真正执行副作用时会重新建立联系

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
let activeEffect = undefined

// 新增的:clearup 函数,用于断开联系
function clearup(effectFn) {
effectFn.deps.forEach((deps) => {
deps.delete(effectFn);
});
effectFn.deps = [];
}

// 注册副作用函数的函数
function effect(fn) {
const effectFn = () => {

clearup(effectFn); // 新增的:clearup 函数,用于断开联系

// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn;

// 执行该副作用函数
fn();
};

effectFn.deps = []; // 新增的:deps 数组,用于存储相关联的依赖项

effectFn();
}

// 改造 track,用于收集副作用函数的关联依赖项
// 依赖收集(追踪)
const track = (target, key) => {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}

let deps = depsMap.get(key);

if (!deps) deps = new Set();

deps.add(activeEffect);
depsMap.set(key, deps);

activeEffect.deps.push(deps) // 新增的:用于收集该副作用函数的关联依赖项
}
};

// 依赖触发
const trigger = (target, key) => {
let depsMap = bucket.get(target);
if (!depsMap) return;

let deps = depsMap.get(key);
if (!deps) return;

const newDeps = new Set(deps); // 新增的:用于避免出现死循环
newDeps.forEach((fn) => fn());
};

Vue3 源码内的依赖收集与触发

reactive为例,讲述下依赖收集、触发的完整流程
reactive 的核心代码:

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
function reactive() {
// 调用 new Proxy,进行数据代理
const proxy = new Proxy(
target,
mutableHandlers, // handler 函数
)
}

export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(),
set: createSetter(),
deleteProperty,
has,
ownKeys
}

const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}

const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}

function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// ...

const res = Reflect.get(target, key, receiver)

// ...

// ⭐️ 依赖收集
track(target, TrackOpTypes.GET, key)

// ...

return res
}
}

function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// ...

const result = Reflect.set(target, key, value, receiver)

// ...

// ⭐️ 依赖触发
trigger(target, TriggerOpTypes.SET, key, value, oldValue)

return result
}
}

依赖收集

通过getter实现依赖的收集

1
2
// ⭐️ 依赖收集
track(target, TrackOpTypes.GET, key)
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
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
// targetMap = {
// [target]: {
// [key]: []
// }
// }

let activeEffect = null

function track(target, type, key) {
let depsMap = targetMap.get(target)

if(!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}

let dep = depsMap.get(key)

if(!dep) {
dep = new Set()
depsMap.set(key, dep)
}

trackEffects(dep)
}

function trackEffects(dep) {
dep.add(activeEffect)
activeEffect!.deps.push(dep)
}

依赖触发

通过setter实现依赖的收集

1
2
// ⭐️ 依赖触发
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function trigger(target, type, key, value, oldValue) {
let depsMap = targetMap.get(target)

if(!depsMap) return

let deps = depsMap.get(key)

triggerEffects(deps)
}

function triggerEffects(deps) {
for (const dep of deps) {
dep()
}
}

渲染流程

模板 -编译-> 渲染函数 -> 虚拟 DOM -> 渲染器 -> 真实 DOM

流程

大致跟 Vue2 一样的:编译 -> 运行时

  1. 编译
    1. <template>转为模板 AST 树(用来描述模板的)
    2. 模板 AST 树转换为JS AST 树(用来描述渲染函数的)
      1. 期间会打上patchFlag(值为 number),用于精确化标记每个节点,只要打上了patchFlag则一定是动态的节点;没有打上的就是静态节点
      2. 并且还会额外使用dynamicChildren数组来储存打标的节点,直接用该数据进行 diff
    3. 基于JS AST 树生成render字符串
    4. 最后基于render字符串生成render函数
  2. 渲染时
    1. 运行实例的render函数,生成vnode
      1. vnode一种用来描述真实 DOM 的 JS 对象
    2. 基于vnode进行渲染到页面
      1. vnode是通过renderer渲染器转化为真实 DOM
      2. 期间会经历 diff 算法,实现最优的方式转化为真实 DOM
      3. renderer渲染器就是一堆DOM 的操作createElement/addEventListener/...

源码

编译

编译的主入口:Compile.ts,触发条件:.mount('#app')函数的调用,并完成首次页面的渲染

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

export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// ...

// ⭐️ <template> 转为模板 AST 树(用来描述模板的)
const ast = isString(template) ? baseParse(template, options) : template

// ...

// ⭐️ 将模板 AST 树转换为 JS AST 树(用来描述渲染函数的)
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)

// ⭐️ 基于 JS AST 树生成 render 字符串
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}

// ...
// ⭐️ 基于 render 字符串生成 render 函数
const render = (
__GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
) as RenderFunction

渲染

渲染时的主入口:无固定,触发条件:某个响应的数据的改变

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
// 若为 ref() 的值的改变,触发页面的重新渲染

class RefImpl<T> {
private _value: T
private _rawValue: T

public dep?: Dep = undefined
public readonly __v_isRef = true

constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}

get value() {
trackRefValue(this)
return this._value
}

set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// ⭐️ 依赖变更通知函数
triggerRefValue(this, newVal)
}
}
}

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}

export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}

function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}

// ...

effect.run()

// ...


patch(...)

完整的流程图

其他知识

虚拟 DOM

一种用来描述真实 DOM 的 JS 对象
Vue 的组件本质也是可以用虚拟 DOM 来描述的

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
// 普通元素
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}

// Vue 组件
// Vue 组件的定义也不是固定的
// MyComponent 可以是函数
const MyComponent = function() {
return { // 返回一个虚拟 DOM
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
// MyComponent 也可以是对象
const MyComponent = {
render() () { // 定义一个 render 方法
return { // 返回一个虚拟 DOM
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
}

const vnode = {
tag: MyComponent
}

渲染函数、渲染器

渲染函数:用于生成虚拟 DOM 的函数,因为手动写虚拟 DOM 的结构太麻烦了,所以封装成一个函数
每个组件有自己的渲染函数,在渲染器里面会用到它

1
2
3
4
5
6
7
8
const h = (tag, props) {
return {
tag,
props,
}
}

h('h1', { onClick: handler }) // 返回一个虚拟 DOM

渲染器:用于将虚拟 DOM 生成为真实 DOM 的函数。仅一个,要渲染的时候调用它

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

// vnode:虚拟 DOM 对象。
// container:一个真实 DOM 元素,作为挂载点,渲染器会把虚拟 DOM 渲染到该挂载点下
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名称创建 DOM 元素
const el = document.createElement(vnode.tag);
// 遍历 vnode.props,将属性、事件添加到 DOM 元素
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以 on 开头,说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
vnode.props[key] // 事件处理函数
);
}
}

// 处理 children
if (typeof vnode.children === "string") {
// 如果 children 是字符串,说明它是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children));
} else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach((child) => renderer(child, el));
}
// 将元素添加到挂载点下
container.appendChild(el);
}

renderer(vnode, document.body); // body 作为挂载点

编译器

作用:将模板(<template>)编译为渲染函数

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
// xxx.vue
<template>
<div @click="handlerClick">
click me
</div>
</template>
<script>
export default {
data() { /*...*/ },
methods: {
handlerClick() { /*...*/ }
}
}
</script>

// 经过编译器处理后,为:

// 编译器会把模板内容(<template>内的)编译成渲染函数并添加到 <script> 标签块的组件对象 render 上
<script>
export default {
data() { /*...*/ },
methods: {
handlerClick() { /*...*/ }
},
render() {
return h('div', { onClick: handlerClick }, 'click me')
}
}
</script>

一个完整的编译流程

编译优化

为了让渲染器能够快速的找到要更新的点,所以在编译期间做了一些优化:

  • PatchFlag 与 Block
    • 编译时,可进行打标:动态、静态
    • 然后再收集这些动态节点,被称为 Block
    • 后续就可以从 Block 里找节点更新
  • 静态提升
    • 将静态的节点创建放到渲染函数之外,这样只需要调用一次静态节点的创建
  • 预字符串化
    • 基于[静态提升],将大量静态节点的创建规律化,最终变成一个静态节点的创建
  • 缓存内联函数(@click=”a+b”)

浅响应、深响应

reactive默认是深响应的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const obj = reactive({ foo: { bar: 1 } })

effect(() => {
console.log(obj.foo.bar)
})
// 修改 obj.foo.bar 的值,能触发响应
obj.foo.bar = 2

// 实现原理:
// 在 new Proxy 的 get 里面处理
// shallow: true(浅)/false(深)
function reactive(_obj, shallow) {
new Proxy(_obj, {
get(target, key, receiver) {
track(target, key)

const res = Reflect.get(target, key, receiver)
if(shallow) return res // 浅响应
if(typeof res === 'object') return reactive(res) // 深响应
}
})
}

浅只读、深只读

只读的实现也是在 new Proxy 的 get、set 里面处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 实现原理:
// 在 new Proxy 的 get 里面处理
// shallow: true(浅)/false(深)
function reactive(_obj, shallow, isReadonly) {
new Proxy(_obj, {
get(target, key, receiver) {
if(!isReadonly) track(target, key)

const res = Reflect.get(target, key, receiver)
if(shallow) return res
if(typeof res === 'object') {
return reactive(res, shallow, isReadonly)
}

// ...
},
set(target, key) {
if(isReadonly) return true

// ...
}
})
}

代理数组、Set、 Map

代理数组解决以下响应式的问题

  • arr[大于长度] = xx 或 arr.length = x
    • 关键点:在Proxy 的 get函数里面判断数组的长度
  • for…in
    • 关键点:使用Proxy 的 ownKeys函数,判断是否为数组还是对象
  • for…of
    • 关键点:该循环的实现是与数组的长度、索引有关的,读取了数组的 Symbol.iterator 属性
  • 一些查找方法:includes、indexOf、lastIndexOf
    • 关键点:重写了这些方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function(...args) {
// this 是代理对象,先在代理对象中查找,将结果存储到 res 中
let res = originMethod.apply(this, args)
if (res === false || res === -1) {
// res 为 false 说明没找到,通过 this.raw 拿到原始数组,再去其中
// 查找,并更新 res 值
res = originMethod.apply(this.raw, args)
}
// 返回最终结果
return res
}
})

代理 Set 解决以下响应式的问题

因为Set的操作方法跟普通对象操作方法不一致,所以会代理处理

服务端渲染

CSR

client-side rendering,客户端渲染,在客户端完成[数据获取+HTML]的拼装,最终在客户端渲染

优点:进行页面跳转后,不会刷新,是通过前端路由的方式动态地渲染页面,用户交互体验友好
缺点:会产生白屏问题,对 SEO(搜索引擎优化)也不友好

SSR

server-side rendering,服务端渲染,在服务端完成[数据获取+HTML]的拼装,最终在客户端渲染

优点:不会产生白屏问题,对 SEO(搜索引擎优化)友好
缺点:进行页面跳转,会重复上述 5 个步骤,用户体验非常差;缺少响应式

CSR vs SSR

同构渲染

分为首次与非首次渲染。
“同构”指:同一套代码即可在服务端运行,也可以在客户端运行。
同构渲染中的首次渲染与 SSR 的工作流程是一致的。当首次访问或者刷新页面时,整个页面的内容是在服务端完成渲染的,浏览器最终得到的是渲染好的 HTML 页面。
但是该页面是静态的,这意味着用户还不能与页面进行任何交互,因为整个应用程序的脚本还没有加载和执行。另外,该静态的 HTML 页面中也会包含<link>、<script>等标签。
同构渲染中的非首次渲染与 CSR 的工作流程是一致的。当浏览器已经接收到初次渲染的静态 HTML 页面,接下来浏览器会解析并渲染该页面。在解析过程中,浏览器会发现 HTML 代码中存在<link><script>标签,于是会从 CDN 或服务器获取相应的资源(这一步与 CSR 一致)。当 JavaScript 资源加载完毕后,会进行激活操作。激活完成后,后续操作都会按照 CSR 应用程序的流程来执行。

一句话总结:代码会在服务端和客户端分别执行一次。在服务端会被渲染为静态的 HTML 字符串,然后发送给浏览器,浏览器再把这段纯静态的 HTML 渲染出来,并补齐响应式、事件绑定等(这也称为“激活”)

Vue 中的同构原理

服务端原理:基于虚拟 DOM 将其转为 HTML 字符串,使用的库为vue-server-renderer
因为服务端不存在真实 DOM,所以只能转为 HTLM 字符串,客户端获取后可直接进行渲染
所以本质就是一个“虚拟 DOM 转 HTML 字符串”的函数,主要功能:字符串的拼接

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
// 普通元素
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}

// 将虚拟 DOM 转为 HTML 字符串的伪函数
const renderElementVNode = (vnode) => {
const { tag, props, children } = vnode
let html = `<${tag}`
if(props) {
// ...
}
html += '>'

if(children) {
if(typeof children === 'string') {
// ...
} else {
// ...
}
}

html += `<${tag}>`
}

renderElementVNode(vnode) // "<div onclick="alert('hello')">click me</div>"

客户端原理:将虚拟 DOM 与已渲染的真实 DOM 进行关联,补齐响应式/事项等(这也称为“激活”)
所以本质也是通过一个函数来建立关联

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
// 服务端到客户端的模拟流程

// html 代表由服务端返回的 HTML 字符串
const html = renderComponentVNode(compVNode)

// 获取挂载点
const container = document.querySelector('#app')

// 设置挂载点的 innerHTML = 由服务端返回的内容
container.innerHTML = html

// 接着调用 hydrate 函数完成激活
renderer.hydrate(VNode, container)

// VNode:代表当前
renderer.hydrate = (VNode, container) => {
// 从容器元素的第一个子节点开始
hydrateNode(container.firstChild, vnode)
}

const hydrateNode = (node, vnode) => {
// node:真实 DOM
// vnode:虚拟 DOM

const { type } = vnode
// 1. 让 vnode.el 引用真实 DOM
vnode.el = node

// 其他细节...

// 5. 重要:hydrateNode 函数需要返回当前节点的下一个兄弟节点,以便继续进行后续的激活操作
return node.nextSibling
}

同构导致的编码问题

部分 API/库 使用的时候需要判断环境(服务端/客户端),否则会报错;或者使用双端支持的 API/库 (axios)

补充知识

如何获取复杂数据的具体类型?

比如:

  • { a: 1 },期望返回类型为object
  • [{ a: 1 }],期望返回类型为array
  • const a = function () {},期望返回类型为function
1
2
3
4
5
const objectType = (obj: object): string => {
const fullTypeString = Object.prootype.toString.call(obj) // '[object Array]'
const typeString = fullTypeString.slice(8, -1) // Array
return typeString.toLocaleLowerCase() // array
}

Map、WeakMap、Set、WeakSet

Map:类似于object的,采用键值对存储数据,键可以是任意类型的(基础/复杂类型都可以),可以使用forEach遍历,并且按照set顺序返回

WeakMap:虚弱版的Map,键必须为复杂类型,弱引用当复杂类型设为null后,WeakMap 里面的值也会自动垃圾回收,变为undefined,不支持forEach遍历

Set:类似于array的,里面的值不允许重复,值是任意类型的(基础/复杂类型都可以),无法通过索引取值,只能forof循环取值

WeakSet:虚弱版的Set,值必须为复杂类型,弱引用当复杂类型设为null后,WeakSet 里面的值也会自动垃圾回收,变为undefined,不支持forof遍历

Proxy

1
2
3
4
5
6
7
8
9
10
11
new Proxy(target, handle);

// target: 目标对象
// property: 属性名
// value: 新值
// receiver: 最初接收赋值的对象,通常是 proxy 本身

const handle = {
get: function (target, property, receiver) {},
set(target, property, value, receiver) {}
}

面试题

手写一份 Vue3 的响应式

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

let activeEffect = undefined

const effect = fn => {
const effectFn = () => {
activeEffect = effectFn
fn()
}

effectFn()
}

const targetMap = new WeakMap()

// 依赖收集
const track = (target, key, receiver) => {
let depsMap = targetMap.get(target)

if(!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}

let depMap = depsMap.get(key)

if(!deps) deps = new Set()

deps.add(activeEffect)

depsMap.set(key, deps)
}

// 依赖触发
const trigger = (target, key, receiver) => {
let depsMap = targetMap.get(target)

if(!depsMap) return

let depMap = depsMap.get(key)

if(!depMap) return

depMap.forEach(fn => fn())
}

const reactive = _obj => {
return new Proxy(_obj, {
get(target, key, receiver) {
track(target, key, receiver)
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue,receiver)
trigger(target, key, receiver)
}
})
}

相关资料

Vue.js 设计与实现.pdf


3-5、Vue3-核心源码讲解
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/3-5、Vue3-核心源码讲解/
作者
黄智强
发布于
2024年1月13日
许可协议