双向数据绑定的实现原理
目前最火的框架vue最大的优点就是双向数据绑定,其中Object.defineProperty() 这个API就是Vue实现双向数据绑定的关键。
VUE实现:v-model
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>双向数据绑定</title>
</head>
<body>
<div id="root">
<p></p>
<input v-model="message">
</div>
<script src="../js/vue.js"></script>
<script>
//v-model,它使得在表单输入和应用状态中做双向数据绑定
var app6 = new Vue({
el: '#root',
data: {
message: '双向数据绑定'
}
});
</script>
</body>
</html>
原生js中的Object.defineProperty()实现
- 通过id简单粗暴的方法
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>双向数据绑定</title>
</head>
<body>
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
<script>
var obj = {},
val='';
Object.defineProperty(obj, 'textVal', {
get: function() {
console.log('get val:' + val);
return val;
},
set: function(newVal) {
val = newVal;
console.log('set val:' + val);
document.getElementById('a').value = val;
document.getElementById('b').innerHTML = val;
}
});
document.addEventListener('keyup', function(e) {
obj.textVal = e.target.value || '';
});
</script>
</body>
</html>
- 复杂的方法
<!DOCTYPE html>
<head></head>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
</div>
<script>
function Compile(node, vm) {
if(node) {
this.$frag = this.nodeToFragment(node, vm);
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function(node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
self.compileElement(child, vm);
frag.append(child); // 将所有子节点添加到fragment中
}
return frag;
},
compileElement: function(node, vm) {
var reg = /\{\{(.*)\}\}/;
//节点类型为元素
if(node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for(var i = 0; i < attr.length; i++ ) {
if(attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取v-model绑定的属性名
node.addEventListener('input', function(e) {
// 给相应的data属性赋值,进而触发该属性的set方法
vm[name]= e.target.value;
});
// node.value = vm[name]; // 将data的值赋给该node
new Watcher(vm, node, name, 'value');
}
};
}
//节点类型为text
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
// node.nodeValue = vm[name]; // 将data的值赋给该node
new Watcher(vm, node, name, 'nodeValue');
}
}
},
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
function Watcher(vm, node, name, type) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.type = type;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function() {
this.get();
this.node[this.type] = this.value; // 订阅者执行相应操作
},
// 获取data的属性值
get: function() {
this.value = this.vm[this.name]; //触发相应属性的get
}
}
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
//添加订阅者watcher到主题对象Dep
if(Dep.target) {
// JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if(newVal === val) return;
val = newVal;
console.log(val);
// 作为发布者发出通知
dep.notify();
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
}
function Vue(options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom =new Compile(document.getElementById(id),this);
// 编译完成后,将dom返回到app中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>
详见【demo】
以上文章来自:黄卉 , https://huanghui8030.github.io/vue/data-vue.html