MVVM

简单的双向绑定,不是MVVM的

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
<!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>如何实现双向绑定 mvvm</title>
</head>
<body>
<p>Hello, <span id="name"></span></p>
<input type="text" id="input" />
<script>
const obj = {
input: ""
};

const inputDOM = document.getElementById("input");
const nameDOM = document.getElementById("name");

inputDOM.addEventListener("input", function(e) {
obj.input = e.target.value;
});

Object.defineProperty(obj, "input", {
set: function(newValue) {
nameDOM.innerHTML = newValue.trim().toUpperCase();
}
});
</script>
</body>
</html>

正宗的MVVM,通过双向绑定来解说

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
<!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>如何实现双向绑定 mvvm</title>
</head>
<body>
<div id="app">
<input type="text" v-model="school.name" />
<div>姓名:{{ school.name }}</div>
<div>年龄:{{ school.age }}</div>
介绍:{{ getNewName }}
<ul>
<li>1</li>
<li>1</li>
</ul>
<button v-on:click="change">更新操作</button>
消息:
<div v-html="message"></div>
</div>
<!-- <script src="./node_modules/vue/dist/vue.min.js"></script> -->
<script src="./MVVM.js"></script>
<script>
let vm = new Vue({
el: "#app",
data: {
school: {
name: "芳草",
age: 30
},
message: "<h1>欢迎大家</h1>"
},
computed: {
getNewName() {
return this.school.name + "架构";
}
},
methods: {
change() {
this.school.name = "天涯";
}
}
});
</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
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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
//MVVM.js
//https://github.com/DMQ/mvvm
/**
* 观察者(发布订阅) 观察者 被观察者
*
* @class Dep
*/
class Dep {
constructor() {
this.subs = []; //存放所有的watcher
}
//订阅
addSub(watcher) {
//添加watcher
this.subs.push(watcher);
}
//发布
notify() {
this.subs.forEach(watcher => {
watcher.update();
});
}
}

/**
* 观察者
*
* @class Watcher
*/
class Watcher {
/**
*Creates an instance of Watcher.
* @param {*} vm 数据
* @param {*} expr 表达式
* @param {*} cb callback
* @memberof Watcher
*/
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
//默认先存放一个老值
this.oldValue = this.get();
}
get() {
// vm.$data.school vm.$data.school.name
Dep.target = this; //先把自己放在this上
//取值 把这个观察者 和数据关联起来
let value = CompileUtil.getVal(this.vm, this.expr);
Dep.target = null; //不取消 任何值取值,都会添加watcher
return value;
}
update() {
//更新操作 数据变化后 会调用观察者的update方法
let newVal = CompileUtil.getVal(this.vm, this.expr);
if (newVal !== this.oldValue) {
this.cb(newVal);
}
}
}
// vm.$watch(vm, 'school.name', newVal => { })

//实现数据挟持过程
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
if (data && typeof data === "object") {
//如果是对象
for (let key in data) {
this.defineReactive(data, key, data[key]);
}
}
}
defineReactive(obj, key, value) {
this.observer(value); // school : [watcher,warcher] b:[wartcher]
let dep = new Dep(); //给每一个属性 都加上一个具有发布订阅的功能
Object.defineProperty(obj, key, {
get() {
//创建watcher时 回取到对应的内容,并且把wartcher放到了全局上
Dep.target && dep.addSub(Dep.target);
return value;
},
set: newVal => {
if (newVal !== value) {
// 新的值是object的话,进行监听
this.observer(newVal);
value = newVal;
// 通知订阅者
dep.notify();
}
}
});
}
}
//基类 调度 解析模板指令
class Compiler {
constructor(el, vm) {
// 判断el属性 是不是一个元素 如果不是元素 那就获取
this.el = this.isElementNode(el) ? el : document.querySelector(el);
//把当前节点中的元素 获取到 放到内存中

this.vm = vm;
let fragment = this.node2fragment(this.el);
//把节点中的内容进行替换

//编译模版 用数据编译
this.compile(fragment);
//把内容再塞到页面中
this.el.appendChild(fragment);
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
//编译元素
compileElement(node) {
let attributes = node.attributes; //类数组
[...attributes].forEach(attr => {
let { name, value: expr } = attr;
//判断是不是指令
if (this.isDirective(name)) {
let [, directive] = name.split("-"); //v-on:click

let [directiveName, eventName] = directive.split(":");

CompileUtil[directiveName](node, expr, this.vm, eventName);
}
});
}
//编译文本
compileText(node) {
//判断当前文本节点中内容是否包含{{xxx}}
let content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
// console.log(content); //找到所有文本
//文本节点
CompileUtil["text"](node, content, this.vm); //{{a}} {{b}}
}
}
//核心的编译方法
compile(node) {
//用来编译内存中的dom节点
let childNodes = node.childNodes;
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
this.compileElement(child);
//如果是元素的话 需要把自己传进去 再去遍历子节点
this.compile(child);
} else {
this.compileText(child);
}
});
}
//把节点移动到内存中
node2fragment(node) {
//创建一个文档节点
let fragment = document.createDocumentFragment();
let firstChild;
// 将原生节点拷贝到fragment
while ((firstChild = node.firstChild)) {
//appendChild具有移动性
fragment.appendChild(firstChild);
}
return fragment;
}
isElementNode(node) {
//是不是元素节点
return node.nodeType === 1;
}
}
// 指令处理集合
CompileUtil = {
//根据表达式来取到对应的数据
getVal(vm, expr) {
// let arr = expr.split(".");
// if (arr.length === 0) {
// return vm.$data[expr];
// }
return expr
.replace(/^\s+(.*?)\s+$/g, "$1")
.split(".")
.reduce((data, current) => {
return data[current];
}, vm.$data);
},
setValue(vm, expr, value) {
expr.replace(/^\s+(.*?)\s+$/g, "$1")
.split(".")
.reduce((data, current, index, arr) => {
if (index == arr.length - 1) {
data[current] = value;
}
return data[current];
}, vm.$data);
},
//解析v-model这个指令
model(node, expr, vm) {
//给输入框赋予value属性 node.value=xxx
let fn = this.updater["modelUpdater"];

new Watcher(vm, expr, newVal => {
//给输入框加一个观察者 数据更新后会出发此方法,会拿新值 给输入框赋值
fn(node, newVal);
});

node.addEventListener("input", e => {
let value = e.target.value;
this.setValue(vm, expr, value);
});
let value = this.getVal(vm, expr);
fn(node, value);
},
html(node, expr, vm) {
//v-html="message"
let fn = this.updater["htmlUpdater"];
new Watcher(vm, expr, newVal => {
//给输入框加一个观察者 数据更新后会出发此方法,会拿新值 给输入框赋值
fn(node, newVal);
});

let value = this.getVal(vm, expr);
fn(node, value);
},
getContentValue(vm, expr) {
//遍历表达式 将内容 重新替换成一个完整的内容 返还回去
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
});
},
on(node, expr, vm, eventName) {
//expr: v-on:click="change"
node.addEventListener(eventName, e => {
vm[expr].call(vm, e);
});
},
text(node, expr, vm) {
let fn = this.updater["textUpdater"];
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//给表达式每个人 {{}} 都加上观察者
new Watcher(vm, args[1], newVal => {
fn(node, this.getContentValue(vm, expr)); //返回了一个全的字符串
});
return this.getVal(vm, args[1]);
});
fn(node, content);
},
updater: {
//把数据插入到节点中
modelUpdater(node, value) {
// node.value = value;
node.value = typeof value == "undefined" ? "" : value;
},
htmlUpdater(node, value) {
//xss攻击
// node.innerHTML = value;
node.innerHTML = typeof value == "undefined" ? "" : value;
},
textUpdater(node, value) {
// node.textContent = value;
node.textContent = typeof value == "undefined" ? "" : value;
}
}
};
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods;
//这个根元素 存在 编译模版
if (this.$el) {
//把数据 全部转化成用Object.defineProperty来定义
new Observer(this.$data);

for (let key in computed) {
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this);
}
});
}

for (let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key];
}
});
}

//把数据获取操作 vm上的取值操作 都代理套vm.$data
this.proxyVm(this.$data);

new Compiler(this.$el, this);
}
}
proxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
}
});
}
}
}