简介
Observer为模型数据中的每个属性添加访问器属性,同时将页面中的用到的模型数据加入到监视队列watch中,只要修改模型数据的值,就会自动调用set方法,set方法会实际修改模型数据的属性值并通知监视队列中接受此数据影响的元素修改元素内容。Compile编译DOM元素, 把元素的属性与模型数据关联起来,并创建元素的watcher和更新DOM元素的属性。
index.html
<!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>Vue Demo</title>
</head>
<body>
<div id="app">
<input type="text" v-model="message" />
<h5>{{ message }}</h5>
</div>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
let vm = new MVVM({
el: "#app",
data: {
message: "hello world"
}
});
</script>
</body>
</html>
mvvm.js
class MVVM {
constructor(options) {
const { el, data } = options;
this.$el = el;
this.$data = data;
if (this.$el) {
new Observer(this.$data);
this.proxyData(this.$data);
new Compile(this.$el, this);
}
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newValue) {
data[key] = newValue;
}
});
});
}
}
observer.js
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (!data || typeof data !== "object") {
return;
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
this.observe(data[key]);
});
}
defineReactive(object, key, value) {
const that = this;
const dep = new Dep();
console.log("dep", key);
Object.defineProperty(object, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue !== value) {
that.observe(newValue);
value = newValue;
dep.notify();
}
}
});
}
}
class Dep {
constructor() {
this.subs = [];
}
addSub(watcher) {
console.log("watcher", watcher, this.subs.length);
this.subs.push(watcher);
}
notify() {
this.subs.forEach(watcher => {
watcher.update();
});
}
}
compile.js
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
let fragment = this.nodeToFragment(this.el);
this.compile(fragment);
this.el.appendChild(fragment);
}
}
isElementNode(node) {
return node.nodeType === 1;
}
isDirective(name) {
return name.includes("v-");
}
nodeToFragment(node) {
const fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = node.firstChild)) {
fragment.appendChild(firstChild);
}
return fragment;
}
compile(fragment) {
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
this.compileElement(node);
this.compile(node);
} else {
this.compileText(node);
}
});
}
compileElement(node) {
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
const { name, value } = attr;
if (this.isDirective(name)) {
const [, type] = name.split("-");
CompileUtil[type](node, this.vm, value);
}
});
}
compileText(node) {
let text = node.textContent;
const reg = /\{\{([^\}]+)\}\}/;
if (reg.test(text)) {
CompileUtil.text(node, this.vm, text);
}
}
}
CompileUtil = {
getData(vm, expr) {
expr = expr.split(".");
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
setData(vm, expr, newVal) {
expr = expr.split(".");
expr.reduce((prev, next, index) => {
if (index === expr.length - 1) {
return (prev[next] = newVal);
}
return prev[next];
}, vm.$data);
},
text(node, vm, text) {
let updateFn = this.updater["textUpdater"];
const expr = text.replace(/\{\{\s*([^\}\s]+)\s*\}\}/g, (...args) => {
new Watcher(vm, args[1], newValue => {
console.log("text", newValue);
updateFn && updateFn(node, newValue);
});
return args[1];
});
updateFn && updateFn(node, this.getData(vm, expr));
},
model(node, vm, expr) {
let updateFn = this.updater["modelUpdater"];
new Watcher(vm, expr, newValue => {
updateFn && updateFn(node, newValue);
});
node.addEventListener("input", e => {
this.setData(vm, expr, e.target.value);
});
updateFn && updateFn(node, this.getData(vm, expr));
},
updater: {
textUpdater(node, value) {
node.textContent = value;
},
modelUpdater(node, value) {
node.value = value;
}
}
};
watcher.js
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
Dep.target = this;
this.value = this.get();
Dep.target = null;
}
get() {
return CompileUtil.getData(this.vm, this.expr);
}
update() {
const newValue = this.get();
const oldValue = this.value;
if (newValue !== oldValue) {
this.cb(newValue);
}
}
}