<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Reactive</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
<div id='app'>
name:<input type="text" v-model="name">
passWord:<input type="text" v-model="passWord">
{{name}} {{passWord}}
<div>
<div>{{name}}</div>
</div>
</div>
<script>
* 简单实现vue响应式及双向绑定,对对象型数据读取键值还需完善
*/
class Vue {
constructor(option){
this.data = option.data;
this.id = option.el;
Observer.observe(this.data);
let dom = Vue.nodeToFragment(document.getElementById(this.id), this);
document.getElementById(this.id).appendChild(dom);
}
static nodeToFragment(node, vm){
let fragment = document.createDocumentFragment();
let child;
while(child = node.firstChild) {
fragment.appendChild(child);
}
Vue.loopNode(fragment.childNodes, vm);
return fragment;
}
static loopNode(nodes, vm) {
Array.from(nodes).forEach(node => {
Vue.compile(node, vm);
if(node.childNodes.length > 0){
Vue.loopNode(node.childNodes, vm);
}
})
}
static compile(node, vm) {
if (node.nodeType === 1) {
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if(attr.nodeName == "v-model") {
let name = attr.nodeValue;
node.addEventListener('input', function(e) {
vm.data[name] = e.target.value;
})
node.value = vm.data[name];
new Watcher(vm, node, name);
}
});
}
let reg = /\{\{([^}]*)\}\}/g;
let textContent = node.textContent;
if(node.nodeType === 3 && reg.test(textContent)){
node.cache = textContent;
node.textContent = node.cache.replace(reg, function() {
let attr = arguments[1];
new Watcher(vm, node, attr);
return vm.data[attr];
})
}
}
}
class Observer {
static observe(data) {
if(typeof data != 'object' || ! data) {
return
}
Object.keys(data).forEach(key => {
Observer.defineReactive(data, key, data[key]);
})
}
static defineReactive(data, key, val) {
let dep = new Dep();
Observer.observe(val);
Object.defineProperty(data, key, {
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if(newVal !== val){
val = newVal;
dep.notify();
}else {
return;
}
}
})
}
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
})
}
}
class Watcher {
constructor(vm, node, name){
this.vm = vm;
this.node = node;
this.name = name;
this.value = this.get();
}
get() {
Dep.target = this;
let value = this.vm.data[this.name];
Dep.target = null;
return value;
}
update() {
if(this.node.nodeType === 1) {
this.node.nodeValue = this.vm.data[this.name];
} else {
this.node.textContent = this.node.cache.replace(/\{\{([^}]*)\}\}/g,
(...rest) => this.vm.data[rest[1]]
)
}
}
}
var vm = new Vue({
el: 'app',
data:{
name: "reactive",
passWord: "123456",
obj: {
obj1: 'obj1'
},
arr: ['arr1', 'arr2']
}
})
</script>
</body>
</html>