-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmvvm_helloworld.html
265 lines (229 loc) · 11.6 KB
/
mvvm_helloworld.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
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
<!--
极简版MVVM,hello world级别
实现了双向绑定
1-最简单的版本
属性不会递归绑定
仅支持一个v-model指令
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<title>极简版MVVM,hello world级别</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">{{text}}
<p>{{text}}</p>
</div>
<script>
'use strict';
/**
* 这里的背景:
* 一个数据绑定若干个展示DOM(观察者们)
* 一个数据绑定若干个控制DOM(带输入指令属性的DOM,一般通过input等改变),
* 每一个对应的控制DOM改变,都会带来数据的同步更新,
* 当然,一般每一个控制DOM自身同时也是一个观察者
*
* 先了解,双向绑定的意思一般是:
* js修改数据 -> 数据变化 -> 该数据绑定的所有展示DOM(观察者们)都需更新
* input(控制DOM)更新 -> 数据同步更新 -> 该数据绑定的所有展示DOM(观察者们)都需更新
*
* 然后接下来将双向绑定分解为两个单向绑定:
*
*
* 数据 -> 视图的单向绑定流程为:
* 1.Observe监听数据(每一个数据属性都有一个dep依赖管理,管理着自己的观察者Watcher),
* 数据的get中,如果检测到Dep.target存在,则将它添加到dep的观察者队列中
*
* 2.Compile编译DOM时,每一个需要绑定数据的DOM节点就是一个watcher,
* 初始化watcher时,Dep.target指向这个watcher,同时触发数据的get,
* 因此数据会将这个watcher添加到自己的dep的观察者队列中
* 至此,M->V的单向绑定已经OK。
*
* 3.此后,任何的数据更新,都会带动它对应的dep中所有的Watcher更新,对应的视图也就自然更新了。
*
*
* 视图 -> 数据的单向绑定流程为:
* 1.Compile编译DOM时,解析指令(v-model),
* 如果检测的带这个指令的DOM,则监听它对应的input change事件,
* 触发这个事件中意味着视图变了,因此直接更新对应的数据,
* 解析指令完毕后需移除这个DOM的(v-model)属性,
* 至此,V-M的单向绑定已经OK。
*
* 2.注意,数据同步更新时,由于Watcher的存在,会带动它对应的dep中所有的Watcher更新,
* 所以,其它绑定该数据的视图也会被带动更新
*
* 3.Compile时,会先将跟节点el转换成文档碎片fragment进行解析编译操作,
* 解析完成后,再将fragment添加回原来的真实dom节点中
*/
function Observer(data) {
this.walk(data);
}
Observer.prototype.walk = function(data) {
// 遍历,将对象所有的属性进行监听
// 最简单的版本中,属性不会递归绑定
Object.keys(data).forEach((name) => {
this.defineReactive(data, name, data[name]);
});
};
Observer.prototype.defineReactive = function(data, key, val) {
// 每一个绑定数据的依赖管理,管理着所有需要跟随这个数据更新的Watcher
const dep = new Dep();
Object.defineProperty(data, key, {
get: function getter() {
console.log('尝试读取:' + key + ', 值为:' + val);
if (Dep.target) {
// 如果有观察者,添加观察者
// 这一步只会在对应Watcher初始化时才会走到
// 其余时候这个key对应的target是null
dep.addWatcher(Dep.target);
}
return val;
},
set: function setter(newValue) {
if (newValue === val) {
return;
}
console.log(key + '属性改变成了:' + newValue);
val = newValue;
// 通知这个属性的所有观察者,这样就实现了m->v
dep.notify();
}
});
}
function Dep() {
this.watchers = [];
}
Dep.prototype.addWatcher = function addWatcher(watcher) {
// 极简版的 watcher只考虑初始化时添加
this.watchers.push(watcher);
};
Dep.prototype.notify = function notify() {
this.watchers.forEach((watcher) => {
// 每次数据改变时,它的依赖管理会通知它所有的watcher
// 然后watcher依次更新自己的视图
watcher.update();
});
};
// 用来判断当前是哪一个Watcher初始化
Dep.target = null;
function Watcher(vm, node, name) {
/**
* 每次初始化时的逻辑:
* 1.Dep.target设为自己
* 2.调用对应数据的get,这样数据就知道有Watcher需要添加了
* 3.对应数据的dep会将这个Watcher添加到它的观察者队列中
* 4.以后数据的任何变动(set),就会通知这个Watcher
* 5.Dep.target再设为null,所以确保了Watcher只会被添加一次
*/
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype.update = function update() {
this.get();
// 目前赋值只考虑了文本与输入框节点的值更新
// 其实完全可以抽取成一个回调,由外部自己处理变动后的事件
// 每次update时,这个Watcher都会将它对应的视图更至最新(用最新的数据赋值)
if (this.node.nodeType === 1 && this.node.tagName.toLowerCase() === 'input') {
this.node.value = this.value || '';
} else {
this.node.nodeValue = this.value || '';
}
};
Watcher.prototype.get = function get() {
// 调用对应数据的get,获取最新值
// 如果是初始化时,此时target刚好为这个watcher,则会被添加到数据的watcher里
this.value = this.vm.data[this.name];
};
function Compile(node, vm) {
this.vm = vm;
this.node = node;
// 现将所有的节点劫持到frag中,然后再进行初始化(譬如解析指令,绑定数据等)
// 然后解析完毕后再将frag重新添加会DOM中,这是一种优化手段(不过现代浏览器就算不这样做,也有自己的优化的)
// 注意,这里是真的“劫持”,会从DOM树中消失,劫持到frag中
const frag = this.nodeToFragment(this.node, this.vm);
// 劫持完毕后,再frag中编译所有元素,包括解析指令,绑定数据等
this.compile(frag, this.vm);
// 上面所有的节点都已经被劫持到frag中了,所以这里再重新添加回DOM
this.node.appendChild(frag);
}
Compile.prototype.nodeToFragment = function nodeToFragment(node, vm) {
const frag = document.createDocumentFragment();
let child;
while (child = node.firstChild) {
frag.appendChild(child);
}
return frag;
};
// 编译,目前只识别{{}}展示DOM和input输入DOM
Compile.prototype.compile = function compile(node, vm) {
// 遍历循环编译所有的节点,这样就不会错漏
const childNodes = node.childNodes;
[].slice.call(childNodes).forEach((node) => {
if (node.nodeType === 1) {
// 元素节点
this.compileElement(node, vm);
} else if (node.nodeType === 3) {
// 文本节点
this.compileText(node, vm);
}
if (node.childNodes && node.childNodes.length) {
// 递归编译
this.compile(node, vm);
}
});
};
Compile.prototype.compileText = function compileText(node, vm) {
const textReg = /[{]{2}(.*)[}]{2}/;
// 节点类型为text
// 需要识别textReg
if (textReg.test(node.nodeValue)) {
// 获取正则刚刚匹配的捕获组
const name = RegExp.$1;
// 添加一个Watcher,因为展示节点需要跟随数据的更新而更新
// 初始化Watcher时就会自动赋值的
name && new Watcher(vm, node, name);
}
};
Compile.prototype.compileElement = function compileElement(node, vm) {
// 节点类型为元素
// 需要解析属性,看看有没有指令
const attr = node.attributes;
const len = attr.length;
for (let i = 0; i < len; i++) {
if (attr[i].nodeName === 'v-model') {
// 获取v-model绑定的属性名
const name = attr[i].nodeValue;
node.addEventListener('input', (e) => {
// 监听到视图更新,然后同步更新数据,这样也会带动展示视图的更新
vm.data[name] = e.target.value;
});
// 移除已经解析完毕的指令
node.removeAttribute('v-model');
// 让这个更新DOM跟随数据更新的
// 初始化Watcher时就会自动赋值的
name && new Watcher(vm, node, name);
}
}
};
function MVVM(options) {
this.el = document.getElementById(options.el);
this.data = options.data;
new Observer(this.data);
new Compile(this.el, this);
}
const vm = new MVVM({
el: 'app',
data: {
text: 'hello!'
}
});
</script>
</body>
</html>