class MVVM {
constructor(options) {
// 挂载在实例
this.$el = options.el;
this.$data = options.data;
// 如果有模版就编译
if (this.$el) {
// 数据劫持
new Observer(this.$data);
// 代理this
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.observer(data);
}
observer(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
this.observer(data[key]);
})
}
defineReactive(obj, key, value) {
let that = this;
let dep = new Dep(); // 每个变化的数据 都会对应一个数组 这个数组是存放所有更新操作
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return value;
},
set(newValue) {
if (newValue != value) {
that.observer(newValue);
value = newValue;
dep.notify();
}
}
})
}
}
class Dep {
constructor() {
// 订阅数组
this.subs = [];
}
addSub(watcher) {
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.node2fragment(this.el);
this.compile(fragment);
this.el.appendChild(fragment);
}
}
/* 辅助方法 */
isElementNode(node) {
return node.nodeType === 1;
}
isDirective(name) {
return name.includes('v-');
}
/* 核心方法 */
node2fragment(el) {
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
compileElement(node) {
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
if (this.isDirective(attrName)) {
let expr = attr.value;
let [, type] = attrName.split('-');
compileUtil[type](node, this.vm, expr);
}
})
}
compileText(node) {
// {{}}
let expr = node.textContent;
let reg = /{{([^}]+)}}/g;
if (reg.test(expr)) {
compileUtil['text'](node, this.vm, expr);
}
}
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);
}
});
}
}
compileUtil = {
getVal(vm, expr) {
expr = expr.split('.'); // [a,b,c,d,e]
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data)
},
getTextVal(vm, expr) {
return expr.replace(/{{([^}]+)}}/g, (...args) => {
return this.getVal(vm, args[1])
})
},
setVal(vm, expr, value) {
expr = expr.split('.'); // [a,b,c,d,e]
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex === expr.length - 1) {
return prev[next] = value;
}
return prev[next];
}, vm.$data)
},
text(node, vm, expr) {
let updateFn = this.updater['textUpdater'];
// {{msg.a}} => hello MVVM
let value = this.getTextVal(vm, expr);
expr.replace(/{{([^}]+)}}/g, (...args) => {
new Watcher(vm, args[1], () => {
updateFn && updateFn(node, this.getTextVal(vm, expr));
})
})
updateFn && updateFn(node, value);
},
model(node, vm, expr) {
let updateFn = this.updater['modelUpdater'];
new Watcher(vm, expr, (newValue) => {
updateFn && updateFn(node, this.getVal(vm, expr));
})
node.addEventListener('input', (e) => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue);
})
updateFn && updateFn(node, this.getVal(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;
this.value = this.get();
}
getVal(vm, expr) {
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data)
}
get() {
Dep.target = this;
let value = this.getVal(this.vm,this.expr);
Dep.target = null;
return value;
}
update() {
let newValue = this.getVal(this.vm,this.expr);
let oldValue = this.value;
if(newValue !=oldValue) {
this.cb(newValue);
}
}
}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>new MVVM</title>
</head>
<body>
<div id="app">
<input type="text" v-model="msg">
{{msg}}
<div>
<ul><li></li></ul>
</div>
<!-- {{1231}} -->
</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: {
msg: 'hello MVVM'
}
})
</script>
</body>
</html>