实现一个mini-vue3
# vdom
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.red {
color: red;
}
.green {
color: green;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
/**
* Vue 的三个核心模块:
Reactivity Module 响应式模块
Compiler Module 编译器模块
Renderer Module 渲染模块
响应式模块允许我们创建 JavaScript 响应对象并可以观察其变化。当使用这些对象的代码运行时,它们会被跟踪,因此,它们可以在响应对象发生变化后运行。
编译器模块获取 HTML 模板并将它们编译成渲染函数。这可能在运行时在浏览器中发生,但在构建 Vue 项目时更常见。这样浏览器就可以只接收渲染函数。
渲染模块的代码包含在网页上渲染组件的三个不同阶段:
渲染阶段 : 将调用 render 函数,它返回一个虚拟 DOM 节点
挂载阶段 : 使用虚拟DOM节点并调用 DOM API 来创建网页
补丁阶段 : 渲染器将旧的虚拟节点和新的虚拟节点进行比较并只更新网页变化的部分
*/
/* createVNode 函数,我们用来创建一个虚拟dom对象,它只需要返回一个指定规划好的格式即可。
在vue2中,是一个多级的嵌套结构,在vue3中,是一个扁平化的props结构。
*/
function h(tag, props, children) {
return {
tag,
props,
children,
};
}
/* mount方法将虚拟dom渲染并挂载到指定的节点上
在实际的mount源码内,回去辨别虚拟vnode是否含有render\template模板,并最后转换成render函数
倘若没有指定挂载的节点,则会警告
mount方法内部会去辨别props内的值是一个v-model、onclick还是一个标准的attribute等,从而做更多的事情,这里暂时忽略
*/
function mount(vnode, container) {
const el = (vnode.el = document.createElement(vnode.tag));
// props
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
el.setAttribute(key, value);
}
}
// children 仅为字符串时(注意,有可能children传入的是一个'red'字串,也可能是一个123数字,
// 但在这里实际上vue隐式的将数字转换为了string类型,从而用户不用去关注这个转换)
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children;
} else {
vnode.children.forEach((child) => {
mount(child, el); // 递归调用mount方法将子组件渲染到界面
});
}
}
container.appendChild(el);
}
const vdom = h("div", { class: "red" }, [h("span", null, "hello")]);
mount(vdom, document.getElementById("app"));
// 简易patch -- 有许多边界情况 --
// 其实源码内有tagtype\key\以及额外的枚举来判断其是否是一个会动态改变的dom以及需要关注改变的方向
function patch(n1, n2) {
debugger;
const el = (n2.el = n1.el);
if (n1.tag === n2.tag) {
//props
const oldProps = n1.props || {};
const newProps = n2.props || {};
for (const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (newValue !== oldValue) {
el.setAttribute(key, newValue);
}
}
// 删除props
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// children
const oldChildren = n1.children;
const newChildren = n2.children;
if (typeof newChildren === "string") {
// newChild 为字符串
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
el.textContent = newChildren;
}
} else {
el.textContent = newChildren;
}
} else {
// newChild 为数组
if (typeof oldChildren === "string") {
// newChildren为数组 oldChild为字符串
el.innerHTML = "";
newChildren.forEach((child) => {
mount(child, el);
});
} else {
// newChildren oldChildren都为数组
// 公共部分长度
const commonLength = Math.min(
oldChildren.length,
newChildren.length
);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
if (newChildren.length > oldChildren.length) {
newChildren.slice(commonLength).forEach((child) => {
mount(child, el);
});
} else if (newChildren.length < oldChildren.length) {
oldChildren.slice(commonLength).forEach((child) => {
el.removeChild(child.el);
});
}
}
}
} else {
}
}
const vdom2 = h("div", { class: "green" }, [h("span", null, "changed")]);
patch(vdom, vdom2);
</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
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# reactive
<script>
// 在vue2中,我们可以这样做, 从而在使用reactive时,将数值进行了代理
let activeEffect;
// 我们将dep类归还为原始的发布订阅模式
class Dep {
subscriber = new Set();
depend() {
if (activeEffect) {
console.log(activeEffect);
this.subscriber.add(activeEffect);
}
}
notify() {
this.subscriber.forEach((effect) => {
effect();
});
}
}
// vue2
// function reactive(raw) {
// Object.keys(raw).forEach((key) => {
// // 每个属性创建了属于他们的dep类
// const dep = new Dep()
// let value = raw[key]
// Object.defineProperty(raw, key, {
// get() {
// dep.depend()
// return value
// },
// set(newValue) {
// value = newValue
// dep.notify()
// },
// })
// })
// return raw
// }
// vue3代理
// 这里我们得到了target和key,接下来需要想的其实是如何将其挂载到dep上,并且每次都能找到准确的地方
// 这里vue3做了一个全局的weakmap来存储这些变化
const targetMap = new WeakMap();
// weakMap:如果这个目标对象本身不再可以从任何代码访问,这个目标可以被垃圾回收机制回收,并且它再weakMap中的关联条目也可以被垃圾回收机制回收
function getDep(target, key) {
let depsMap = targetMap.get(target);
// 1. 使用公共的targetMap存放所有的响应式逻辑,使用map建立指定需要添加响应式的对象的数组(该数组的成员是一个个表示键值对的数组。)
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取这个对象内的指定key的需要触发更新的数组
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
const reactiveHandlers = {
get(target, key, receiver) {
// 将自己加进去
const dep = getDep(target, key);
dep.depend();
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const dep = getDep(target, key);
const result = Reflect.set(target, key, value, receiver);
dep.notify();
return result;
},
// 这里可能还会有监听其他的陷阱 使其响应式
// has\ownKeys -- if ('count' in state)
};
function reactive(raw) {
return new Proxy(raw, reactiveHandlers);
}
function watchEffect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
const state = reactive({
count: 0,
});
watchEffect(() => {
console.log(state.count);
});
</script>
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
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
# deps
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
</html>
<!-- <script>
/* 当我们在使用watchEffect的时候,dep会帮我们收集依赖,并且在调用时执行依赖(副作用) */
let activeEffect
class Dep {
subscriber = new Set()
depend() {
if (activeEffect) {
this.subscriber.add(activeEffect)
}
}
notify() {
this.subscriber.forEach((effect) => {
effect()
})
}
}
const dep = new Dep()
function watchEffect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
watchEffect(() => {
dep.depend()
console.log('effect')
})
dep.notify()
</script> -->
<script>
/* 事实上,我们可能需要在其中传递一些参数:new Dep(something),并拥有取值函数(getter)和存值函数(setter)。
* 当我们的取值发生改变时,它会主动帮助我们去执行副作用,而不需要我们手动去执行
*/
let activeEffect;
class Dep {
constructor(value) {
this.subscriber = new Set();
this._value = value;
}
// 当我们需要去使用它的时候,我们就能够知道有人将会使用它,将他放入订阅者数组中
get value() {
this.depend();
return this._value;
}
// 当值发生了改变,我们将通知所有使用到它的函数,告知它们这个值变化了。
set value(newValue) {
this._value = newValue;
this.notify();
}
depend() {
if (activeEffect) {
this.subscriber.add(activeEffect);
}
}
notify() {
this.subscriber.forEach((effect) => {
effect();
});
}
}
const dep = new Dep("hello");
function watchEffect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
watchEffect(() => {
console.log(dep.value);
});
// 可以看到。在这里我们只需要执行一步改变即可完成与上面相同的效果
dep.value = "wuhu!";
</script>
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
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
# mini-vue
<div id="app"></div>
<script>
// v-dom
function h(tag, props, children) {
return {
tag,
props,
children,
};
}
function mount(vnode, container) {
const el = (vnode.el = document.createElement(vnode.tag));
// props 解析props 为el设置attribute
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);
}
}
}
// children
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children;
} else {
vnode.children.forEach((child) => {
mount(child, el);
});
}
}
// 元素挂载在container上
container.appendChild(el);
}
function patch(n1, n2) {
const el = (n2.el = n1.el);
if (n1.tag === n2.tag) {
//props
const oldProps = n1.props || {};
const newProps = n2.props || {};
for (const key in newProps) {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (newValue !== oldValue) {
el.setAttribute(key, newValue);
}
}
// 删除props 删除旧vnode上面的attrbute
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// children
const oldChildren = n1.children;
const newChildren = n2.children;
if (typeof newChildren === "string") {
// newChild 为字符串
if (typeof oldChildren === "string") {
if (newChildren !== oldChildren) {
el.textContent = newChildren;
}
} else {
el.textContent = newChildren;
}
} else {
// newChild 为数组
if (typeof oldChildren === "string") {
// newChildren为数组 oldChild为字符串
el.innerHTML = "";
newChildren.forEach((child) => {
mount(child, el);
});
} else {
// newChildren oldChildren都为数组
// 公共部分长度
const commonLength = Math.min(oldChildren.length, newChildren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
if (newChildren.length > oldChildren.length) {
newChildren.slice(commonLength).forEach((child) => {
mount(child, el);
});
} else if (newChildren.length < oldChildren.length) {
oldChildren.slice(commonLength).forEach((child) => {
el.removeChild(child.el);
});
}
}
}
} else {
}
}
//reactivity
let activeEffect;
// 依赖收集器
class Dep {
subscribers = new Set();
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach((effect) => {
effect();
});
}
}
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
const targetMap = new WeakMap();
// 该函数的功能是返回target[key]的依赖收集器
function getTarget(target, key) {
// depsMap为Map类型 里面存储要响应的对象的Map
/**
* {
* [target]: Map()
*}
*/
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 为每一个key设置Dep监听类
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
const reactiveHandler = {
get(target, key, receiver) {
const dep = getTarget(target, key);
// 收集依赖
dep.depend();
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const dep = getTarget(target, key);
const result = Reflect.set(target, key, value, receiver);
dep.notify();
return result;
},
};
function reactive(raw) {
return new Proxy(raw, reactiveHandler);
}
const App = {
data: reactive({
count: 0,
}),
render() {
return h(
"div",
{
onClick: () => {
this.data.count++;
},
},
String(this.data.count)
);
},
};
function mountApp(component, container) {
let isMounted = false;
let prevVdom;
watchEffect(() => {
if (!isMounted) {
prevVdom = component.render();
mount(prevVdom, container);
isMounted = true;
} else {
const newVdom = component.render();
patch(prevVdom, newVdom);
prevVdom = newVdom;
}
});
}
mountApp(App, document.getElementById("app"));
</script>
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
编辑 (opens new window)
上次更新: 2023/05/03, 18:26:16