vue双向数据绑定是通过数据劫持结合订阅者发布者模式(观察者)来实现的
关键在于data如何去更新view,因为view更新model可以同过监听input事件来进行更新
data更新view主要是通过Object.definePropety劫持各个属性的setter和getter方法,在数据变动时发布消息通知所有的订阅者。
简单的双向数据绑定原理
<!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>Document</title>
</head>
<body>
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
</body>
<script>
var obj = {}; //定义一个空对象
var val = 'val'; //赋予初始值
Object.defineProperty(obj, 'val', {//定义要修改对象的属性
get: function () {
return val;
},
set: function (newVal) {
val = newVal;//定义val等于修改后的内容
document.getElementById('a').value = val;//让文本框的内容等于val
document.getElementById('b').innerHTML = val;//让span的内容等于val
}
});
document.addEventListener('keyup', function (e) {//当在文本框输入内容时让对象里你定义的val等于文本框的值
obj.val = e.target.value;
})
console.log(obj.val)
setTimeout(function(){
obj.val='startvalue'
console.log(obj.val)
},5000)
</script>
</html>
进阶版
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>双向绑定</title>
</head>
<body>
<!-- 实现vue -->
<div id="app">
<input type="text" v-model="text">
{{ text }}
<br>
<input type="text" v-model="text2">
{{ text2 }}
<p>{{text2}}</p>
</div>
<script type="text/javascript">
function defineReactive(obj, key, val){
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function(){
if(Dep.target){
dep.addSub(Dep.target);
}
return val
},
set: function(newVal){
if(newVal === val){
return
}
val = newVal;
console.log('新值:' + val);
// 一旦更新立马通知
dep.notify();
}
})
}
/*观察者函数*/
function observe(obj,vm){
for(let key of Object.keys(obj)){
defineReactive(vm, key, obj[key]);
}
}
function nodeToFragment(node,vm){
var fragment = document.createDocumentFragment();
var child;
while(child = node.firstChild){
compile(child, vm);
fragment.appendChild(child);
}
return fragment
}
/*编译函数*/
function compile(node, vm){
var reg = /\{\{(.*)\}\}/; // 来匹配 {{ xxx }} 中的xxx
// 如果是元素节点
if(node.nodeType === 1){
var attr = node.attributes;
// 解析元素节点的所有属性
for(let i=0;i<attr.length;i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue; // 看看是与哪一个数据相关
node.addEventListener('input', function(e){
vm[name] = e.target.value; // 将实例的text 修改为最新值
});
node.value = vm[name]; // 将data的值赋给该node
node.removeAttribute('v-model');
}
};
}
// 如果是文本节点
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); // 不直接通过赋值的操作,而是通过绑定一个订阅者
}
}
}
/*Watcher构造函数*/
function Watcher(vm, node, name){
Dep.target = this; // Dep.target 是一个全局变量
this.vm = vm;
this.node= node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update(){
this.get();
this.node.nodeValue = this.value; // 注意,这是更改节点内容的关键
},
get(){
this.value = this.vm[this.name]; // 触发相应的get
}
}
/*dep构造函数*/
function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub(sub){
this.subs.push(sub);
},
notify(){
this.subs.forEach(function(sub){
sub.update();
})
}
}
/*Vue构造函数*/
function Vue(options){
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
// 处理完所有dom节点后,重新将内容添加回去
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world',
text2: 'hello world',
}
});
</script>
</body>
</html>