vue3简单剖析

Composition API RFC

不得不说,vue的语法,越来越react了,感觉vue3发布后,会有更多人专项react了

vue2实现计算属性和点击累加的代码就不看了,大家都会,看下vue3的

我们执行npm run dev 调试打个包

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
<!DOCTYPE html>
<html lang="en">
<body>
<div id='app'></div>
</body>
<script src="./dist/vue.global.js"></script>
<script>
const { createApp, reactive, computed, effect } = Vue;

const RootComponent = {
template: `
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
`,
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
effect(() => {
console.log('数字被修改了 ',state.count)
})
function increment() {
state.count++
}

return {
state,
increment
}
}
}

createApp().mount(RootComponent, '#app')
</script>
</html>

这个reactive和react-hooks越来越像了, 大家可以去Composition API RFC这里看细节

功能

编译器(Compiler)的优化主要在体现在以下几个方面:

  • 使用模块化架构
  • 优化 “Block tree”
  • 更激进的 static tree hoisting 功能
  • 支持 Source map
  • 内置标识符前缀(又名 “stripWith”)
  • 内置整齐打印(pretty-printing)功能
  • 移除 source map 和标识符前缀功能后,使用 Brotli 压缩的浏览器版本精简了大约 10KB

运行时(Runtime)的更新主要体现在以下几个方面:

  • 速度显著提升
  • 同时支持 Composition API 和 Options API,以及 typings
  • 基于 Proxy 实现的数据变更检测
  • 支持 Fragments
  • 支持 Portals
  • 支持 Suspense w/ async setup()

最后,还有一些 2.x 的功能尚未移植过来,如下:

  • 服务器端渲染
  • keep-alive
  • transition
  • Compiler DOM-specific transforms
  • v-on DOM 修饰符 v-model v-text v-pre v-onc v-html v-show

typescript

全部由typescript构建,我们学的TS热度++ 三大库的最终选择,ts乃今年必学技能

proxy取代deineProperty

除了性能更高以为,还有以下缺陷,也是为啥会有$set$delete的原因
1、属性的新加或者删除也无法监听;
2、数组元素的增加和删除也无法监听

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
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target)
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}

function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target already has corresponding Proxy
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// target is already a Proxy
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}

稍微精简下

1
2
3
4
5
6
7
function reactive(target) {
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
return observed
}

基本上除了set map weakset 和weakmap,都是baseHandlers,下面重点关注一下,Proxy的语法 大家需要复习下es6

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 const readonlyHandlers: ProxyHandler<any> = {
get: createGetter(true),

set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Set operation on key "${key as any}" failed: target is readonly.`,
target
)
}
return true
} else {
return set(target, key, value, receiver)
}
},

deleteProperty(target: any, key: string | symbol): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Delete operation on key "${key as any}" failed: target is readonly.`,
target
)
}
return true
} else {
return deleteProperty(target, key)
}
},

has,
ownKeys
}

关于proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key) {
console.log('获取值:', key)
return target[key]
},
set(target, key, value) {
console.log('修改值:', key, value)
target[key] = value
return true
}
})

p.push(4)


获取值: push
获取值: length
修改值: 3 4
修改值: length 4

比defineproperty优秀的 就是数组和对象都可以直接触发getter和setter, 但是数组会触发两次,因为获取push和修改length的时候也会触发

我们还可以用Reflect

1
2
3
4
5
6
7
8
9
10
11
12
13
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key) {
console.log('获取值:', key)
return Reflect.get(target,key)
},
set(target, key, value) {
console.log('修改值:', key, value)
return Reflect.set(target,key, value)
}
})

p.push(4)

多次触发和深层嵌套问题,一会我们看vue3是怎么解决的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let data = {name:{ title:'kkb'}}
let p = new Proxy(data, {
get(target, key) {
console.log('获取值:', key)
return Reflect.get(target,key)
},
set(target, key, value) {
console.log('修改值:', key, value)
return Reflect.set(target,key, value)
}
})

p.name.title = 'xx'

获取值: name
"xx"

vue3深度检测

baseHander

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
track(target, OperationTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}

返回值如果是object,就再走一次reactive,实现深度

vue3处理重复trigger

很简单,用的hasOwProperty, set肯定会出发多次,但是通知只出去一次, 比如数组修改length的时候,hasOwProperty是true, 那就不触发

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
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
value = toRaw(value)
const hadKey = hasOwn(target, key)
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}

手写vue3的reactive

刚才说的细节,我们手写一下

effect

computed

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
// 用这个方法来模式视图更新
function updateView() {
console.log('触发视图更新啦')
}
function isObject(t) {
return typeof t === 'object' && t !== null
}

// 把原目标对象 转变 为响应式的对象
const options = {
set(target, key, value, reciver) {
// console.log(key,target.hasOwnProperty(key))
if(!target.hasOwnProperty(key)){
updateView()
}
return Reflect.set(target, key, value, reciver)
},
get(target, key, reciver) {
const res = Reflect.get(target, key, reciver)
if(isObject(target[key])){
return reactive(res)
}
return res
},
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key)
}
}
// 用来做缓存
const toProxy = new WeakMap()

function reactive(target) {
if(!isObject(target)){
return target
}
// 如果已经代理过了这个对象,则直接返回代理后的结果即可
if(toProxy.get(target)){
return toProxy.get(target)
}
let proxyed = new Proxy(target, options)
toProxy.set(target, proxyed)
return proxyed
}

// 测试数据
let obj = {
name: 'Ace7523',
array: ['a', 'b', 'c']
}

// 把原数据转变响应式的数据
let reactivedObj = reactive(obj)

// 改变数据,期望会触发updateView() 方法 从而更新视图
// reactivedObj.name = 'change'

// reactivedObj.array.unshift(4)

完整代码

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
130
131
132
133
134
135
136
137



let toProxy = new WeakMap()
let toRaw = new WeakMap()
let effectStack = []
let tagetMap = new WeakMap()

const baseHander = {
get(target, key) {
const res = Reflect.get(target, key)
// 收集依赖
track(target, key)
// 递归寻找
return typeof res == 'object' ? reactive(res) : res
},
set(target, key, val) {
const info = { oldValue: target[key], newValue: val }
const res = Reflect.set(target, key, val)
// 触发更新
trigger(target, key, info)
return res
}
}
function reactive(target) {
// 查询缓存
let observed = toProxy.get(target)
if (observed) {
return observed
}
if (toRaw.get(target)) {
return target
}
observed = new Proxy(target, baseHander)
// 设置缓存
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}



// {
// target:{
// age: [] (set),
// name:[effect]
// }
// }

function trigger(target, key, info) {
// 触发更新
const depsMap = tagetMap.get(target)

if (depsMap === undefined) {
return
}
const effects = new Set()
const computedRunners = new Set()
if (key) {
let deps = depsMap.get(key)
deps.forEach(effect => {
if (effect.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
// const run = effect=> effect()
effects.forEach(effect => effect())
computedRunners.forEach(effect => effect())

}
function track(target, key) {
let effect = effectStack[effectStack.length - 1]
if (effect) {
let depsMap = tagetMap.get(target)
if (depsMap === undefined) {
depsMap = new Map()
tagetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (dep === undefined) {
dep = new Set()
depsMap.set(key, dep)
}
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
}
}
}

// 存储effect

function effect(fn, options = {}) {
let e = createReactiveEffect(fn, options)
// 没有考虑compueted
if (!options.lazy) {
e()
}
return e
}

function createReactiveEffect(fn, options) {
const effect = function effect(...args) {
return run(effect, fn, args)
}
effect.deps = []
effect.computed = options.computed
effect.lazy = options.lazy
return effect

}

function run(effect, fn, args) {
if (effectStack.indexOf(effect) === -1) {

try {
effectStack.push(effect)
return fn(...args)
}
finally {
effectStack.pop()
}
}
}

function computed(fn) {
const runner = effect(fn, { computed: true, lazy: true })
return {
effect: runner,
get value() {
return runner()
}
}
}

image

测试代码

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>

<body>
<div id='app'></div>
<button id="btn">点我</button>
<script src="./vue3-test.js"></script>
<script>
const root = document.querySelector('#app')
const btn = document.querySelector('#btn')

const obj = reactive({
name: 'Vue3测试',
age: 6
});

let double = computed(() => {
return obj.age * 2
});

effect(() => {
root.innerHTML = `<h1>${obj.name}今年${obj.age}岁了,乘以2是${double.value}</h1>`
})

btn.addEventListener('click', () => {
obj.age += 1
}, false)
</script>
</body>

</html>

其他细节 track收集依赖,trigger触发更新

vue3其他模块细节

代码仓库中有个 packages 目录,里面主要是 Vue 3.0 的相关源码功能实现,具体内容如下所示。

compiler-core

平台无关的编译器,它既包含可扩展的基础功能,也包含所有平台无关的插件。暴露了 AST 和 baseCompile 相关的 API,它能把一个字符串变成一棵 AST

compiler-dom

针对浏览器的编译器。

runtime-core

与平台无关的运行时环境。支持实现的功能有虚拟 DOM 渲染器、Vue 组件和 Vue 的各种API, 可以用来自定义 renderer ,vue2中也有 ,入口代码看起来

runtime-dom

针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等, 暴露了重要的render和createApp方法

1
2
3
4
5
6
const { render, createApp } = createRenderer<Node, Element>({
patchProp,
...nodeOps
})

export { render, createApp }

runtime-test

一个专门为了测试而写的轻量级 runtime。比如对外暴露了renderToString方法,在此感慨和react越来越像了

server-renderer

用于 SSR,尚未实现。

shared

没有暴露任何 API,主要包含了一些平台无关的内部帮助方法。

vue

用于构建「完整」版本,引用了上面提到的 runtime 和 compiler目录。入口文件代码如下

1
2
3
4
5
6
7
'use strict'

if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/vue.cjs.prod.js')
} else {
module.exports = require('./dist/vue.cjs.js')
}

所以想阅读源码,还是要看构建流程,这个和vue2也是一致的。