MiniVue
# MiniVue
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
- 渲染系统模块;
- 响应式系统模块;
- 应用程序入口模块;
# 渲染系统实现
渲染系统,该模块主要包含三个功能:
- 功能一:h函数,用于返回一个VNode对象;
- 功能二:mount函数,用于将VNode挂载到DOM上;
- 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode
const h = (tag, props, children) => {
// vnode -> javascript对象 -> {}
return {
tag,
props,
children,
}
}
const mount = (vnode, container) => {
// vnode -> element
// 1.创建出真实的原生, 并且在vnode上保留el
const el = (vnode.el = document.createElement(vnode.tag))
// 2.处理props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith('on')) {
// 对事件监听的判断
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3.处理children
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach((item) => {
mount(item, el)
})
}
}
// 4.将el挂载到container上
container.appendChild(el)
}
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement
n1ElParent.removeChild(n1.el)
mount(n2, n1ElParent)
} else {
// 1.取出element对象, 并且在n2中进行保存
const el = (n2.el = n1.el)
// 2.处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1.获取所有的newProps添加到el
for (const key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
if (key.startsWith('on')) {
// 对事件监听的判断
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 2.2.删除旧的props
for (const key in oldProps) {
if (key.startsWith('on')) {
// 对事件监听的判断,每次移除所有旧的事件,否则在添加props时会重复(事件函数newValue !== oldValue为 true)
const value = oldProps[key]
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 3.处理children
const oldChildren = n1.children || []
const newChidlren = n2.children || []
if (typeof newChidlren === 'string') {
// 情况一: newChildren本身是一个string
// 边界情况 (edge case)
if (typeof oldChildren === 'string') {
if (newChidlren !== oldChildren) {
el.textContent = newChidlren
}
} else {
el.innerHTML = newChidlren
}
} else {
// 情况二: newChildren本身是一个数组
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChidlren.forEach((item) => {
mount(item, el)
})
} else {
// oldChildren: [v1, v2, v3, v8, v9]
// newChildren: [v1, v5, v6]
// 1.前面有相同节点的原生进行patch操作
const commonLength = Math.min(oldChildren.length, newChidlren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChidlren[i])
}
// 2.newChildren.length > oldChildren.length
if (newChidlren.length > oldChildren.length) {
newChidlren.slice(oldChildren.length).forEach((item) => {
mount(item, el)
})
}
// 3.newChildren.length < oldChildren.length
if (newChidlren.length < oldChildren.length) {
oldChildren.slice(newChidlren.length).forEach((item) => {
el.removeChild(item.el)
})
}
}
}
}
}
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
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
// 1.通过h函数来创建一个vnode
const vnode = h('div', {class: "why", id: "aaa"}, [
h("h2", null, "当前计数: 100"),
h("button", {onClick: function() {}}, "+1")
]); // vdom
// 2.通过mount函数, 将vnode挂载到div#app上
mount(vnode, document.querySelector("#app"))
// 3.创建新的vnode
setTimeout(() => {
const vnode1 = h('div', {class: "coderwhy", id: "aaa"}, [
h("h2", null, "呵呵呵"),
h("button", {onClick: function() {}}, "-1")
]);
patch(vnode, vnode1);
}, 2000)
</script>
</body>
</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
31
32
33
34
35
36
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
# 响应式系统 Vue2 实现
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach((effect) => {
effect()
})
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap()
function getDep(target, key) {
// 1.根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2.取出具体的dep对象
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
// vue2对raw进行数据劫持
function reactive(raw) {
Object.keys(raw).forEach((key) => {
const dep = getDep(raw, key)
let value = raw[key]
Object.defineProperty(raw, key, {
get() {
dep.depend()
return value
},
set(newValue) {
if (value !== newValue) {
value = newValue
dep.notify()
}
},
})
})
return raw
}
// 测试代码
// 先defineProperty劫持对象属性,不会调用其get、set方法
const info = reactive({ counter: 100, name: 'why' })
const foo = reactive({ height: 1.88 })
// 调用 watchEffect,确定当前 activeEffect,自动执行该 effect,
// 此时.调用会执行get方法,dep.depend()会将该effect加入该key的Dep中
// watchEffect1
watchEffect(function () {
console.log('effect1:', info.counter * 2, info.name)
})
// watchEffect2
watchEffect(function () {
console.log('effect2:', info.counter * info.counter)
})
// watchEffect3
watchEffect(function () {
console.log('effect3:', info.counter + 10, info.name)
})
watchEffect(function () {
console.log('effect4:', foo.height)
})
// info.counter++;
// info.name = "why";
foo.height = 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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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
# 响应式系统 Vue3 实现
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach((effect) => {
effect()
})
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
// Map({key: value}): key是一个字符串
// WeakMap({key(对象): value}): key是一个对象, 弱引用
const targetMap = new WeakMap()
function getDep(target, key) {
// 1.根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2.取出具体的dep对象
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
// vue3对raw进行数据劫持
function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const dep = getDep(target, key)
dep.depend()
return target[key]
},
set(target, key, newValue) {
const dep = getDep(target, key)
target[key] = newValue
dep.notify()
},
})
}
// const proxy = reactive({name: "123"})
// proxy.name = "321";
// 测试代码
const info = reactive({ counter: 100, name: 'why' })
const foo = reactive({ height: 1.88 })
// watchEffect1
watchEffect(function () {
console.log('effect1:', info.counter * 2, info.name)
})
// watchEffect2
watchEffect(function () {
console.log('effect2:', info.counter * info.counter)
})
// watchEffect3
watchEffect(function () {
console.log('effect3:', info.counter + 10, info.name)
})
watchEffect(function () {
console.log('effect4:', foo.height)
})
// info.counter++;
// info.name = "why";
foo.height = 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
85
86
87
88
89
90
91
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
# 为什么Vue3选择Proxy呢
劫持目标
- Object.definedProperty 是劫持对象的属性时,如果新增元素,那么Vue2需要再次 调用definedProperty,
- Proxy 劫持的是整个对象,不需要做特殊处理;
修改对象的不同:
- 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
- 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
Proxy 能观察的类型比 defineProperty 更丰富
- has:in操作符的捕获器;
- deleteProperty:delete 操作符的捕捉器;
- 等等其他操作;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;
缺点:Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9
# 入口函数
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector)
let isMounted = false
let oldVNode = null
watchEffect(function () {
if (!isMounted) {
// 第一次进入需要 mount 挂载
oldVNode = rootComponent.render()
mount(oldVNode, container)
isMounted = true
} else {
// 非第一次进入需要 patch 更新
const newVNode = rootComponent.render()
patch(oldVNode, newVNode)
oldVNode = newVNode
}
})
},
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="../02_渲染器实现/renderer.js"></script>
<script src="../03_响应式系统/reactive.js"></script>
<script src="./index.js"></script>
<script>
// 1.创建根组件
const App = {
data: reactive({
counter: 0
}),
render() {
return h("div", null, [
h("h2", null, `当前计数: ${this.data.counter}`),
h("button", {
onClick: () => {
this.data.counter++
console.log(this.data.counter);
}
}, "+1")
])
}
}
// 2.挂载根组件
const app = createApp(App);
app.mount("#app");
</script>
</body>
</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
31
32
33
34
35
36
37
38
39
40
41
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
编辑 (opens new window)
上次更新: 2022/03/23, 17:55:39