Vue MVVM双向绑定实例详解(数据劫持+发布者-订阅者模式)
一、MVVM模式
MVVM是Model-View-ViewModel的缩写。在前端开发中,MVVM是一种设计模式,它将数据(Model)、业务逻辑(ViewModel)和页面(View)分离开来。其中,ViewModel充当了连接View和Model的纽带,通过ViewModel将数据绑定到View中实现了数据的双向绑定。
二、双向绑定的实现
在Vue中实现双向绑定的方式,主要是通过数据劫持和发布者-订阅者模式的结合来实现的。
1. 数据劫持
在Vue中,通过Object.defineProperty()方法将数据属性转化为getter和setter,来实现数据劫持,当数据属性发生变化时,通过setter方法通知模板进行更新。
下面是一个数据劫持的示例。
var obj = {};
var value = '';
Object.defineProperty(obj, 'msg', {
get() {
console.log('get value');
return value;
},
set(newValue) {
console.log('set value');
value = newValue;
}
})
obj.msg = 'hello';
console.log(obj.msg);
运行结果:
set value
get value
hello
输出结果可以看到,当对obj.msg属性进行赋值时,会触发setter方法,然后再通过getter方法获取值,实现了对数据的劫持。
2. 发布者-订阅者模式
在Vue中,通过对属性的getter和setter进行封装,实现属性的订阅和发布,来实现对数据的双向绑定。
下面是一个发布者-订阅者模式的示例。
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
})
}
}
class Watcher {
constructor() {
Dep.target = this;
}
update() {
console.log('update view');
}
}
Dep.target = null;
var dep = new Dep();
var watcher = new Watcher();
dep.addSub(watcher);
dep.notify();
运行结果:
update view
输出结果可以看到,在订阅者Watcher中,通过将Dep.target属性标记为当前Watcher,然后在属性的getter方法中,向依赖的订阅者列表中添加当前Watcher,然后在属性的setter方法中,通过Dep.notify()方法通知依赖该数据的所有订阅者进行更新。
三、数据双向绑定的实现
数据双向绑定实现方式有很多,下面以Vue的实现为例,进行说明。
1. 实现双向绑定的构造函数
首先,我们需要实现一个构造函数,用于观察并监听ViewModel中的数据,当数据发生改变时,触发回调函数。
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
},
defineReactive(data, key, val) {
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newValue) {
if (val === newValue) {
return;
}
val = newValue;
dep.notify();
}
})
}
}
解析:
Observer构造函数接收一个参数data,data表示要建立双向绑定的ViewModel对象。
walk()方法遍历data的所有属性,并调用defineReactive()方法。
defineReactive()方法对data对象的每个属性进行处理。首先创建一个Dep实例,用于存储该属性所依赖的订阅者。然后使用Object.defineProperty()方法将data[key]转换为getter/setter方法,当属性被读取时,如果有订阅者依赖该属性,就将该订阅者添加到Dep实例中,当属性被修改时,通知Dep实例告知所有订阅者进行更新。
2. 实现模板编译
为了一个页面的数据与ViewModel建立双向绑定,需要实现模板编译的功能,将模板中的表达式与ViewModel中的数据绑定起来,这里我们可以使用正则表达式进行解析。
function compile(el) {
var element = document.querySelector(el);
var childNodes = element.childNodes;
Array.prototype.slice.call(childNodes).forEach(node => {
if (node.nodeType === 3) {
let text = node.textContent;
let reg = /\{\{(.*)\}\}/;
if (reg.test(text)) {
let matchValue = RegExp.$1.trim();
new Watcher(vm, matchValue, function(newVal) {
node.textContent = text.replace(reg, newVal);
})
}
} else if (node.nodeType === 1) {
let nodeAttrs = node.attributes;
Array.prototype.slice.call(nodeAttrs).forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
if (attrName.indexOf('v-') === 0) {
let dir = attrName.substring(2);
if (dir === 'model') {
new Watcher(vm, attrValue, function(newVal) {
node.value = newVal;
})
node.addEventListener('input', e => {
let newValue = e.target.value;
if (attrValue.indexOf('.') > -1) {
let keys = attrValue.split('.');
let data = vm;
keys.forEach((key, index) => {
if (index === keys.length - 1) {
data[key] = newValue;
} else {
data = data[key];
}
})
} else {
vm[attrValue] = newValue;
}
})
}
}
})
}
if (node.childNodes && node.childNodes.length) {
compile(node);
}
})
}
解析:
compile()函数接收一个参数el,el表示需要编译的模板元素。首先获取该元素下的所有子节点,通过Array.prototype.slice.call()将NodeList转换为数组。
遍历每个子节点,如果是文本节点(nodeType=3),则使用正则表达式将模板中的表达式解析出来,并创建一个Watcher,并在回调函数中更新视图。如果是元素节点(nodeType=1),则遍历节点的所有属性,并判断属性名是否以'v-'开头,如果是,则进行特殊处理,将该属性值与ViewModel中的数据建立双向绑定,并添加事件监听器,当该元素的value值修改时,将数据同步更新到ViewModel中。
最后,如果该节点还包含子节点,则递归遍历子节点,进行编译。
3. 实现Watcher
Watcher是连接ViewModel和View的桥梁,当ViewModel中的数据发生变化时,Watcher会触发回调函数来更新视图,同时将新的值传递到回调函数中。
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get();
}
Watcher.prototype = {
update() {
let newVal = this.get();
let oldVal = this.value;
if (newVal !== oldVal) {
this.value = newVal;
this.cb.call(vm, newVal, oldVal);
}
},
get() {
Dep.target = this;
let value = this.getVMValue(this.vm, this.exp);
Dep.target = null;
return value;
},
getVMValue(vm, exp) {
let data = vm;
exp.split('.').forEach(key => {
data = data[key];
})
return data;
}
}
解析:
Watcher构造函数接收三个参数,vm表示ViewModel对象,exp表示ViewModel中的数据对象的属性,cb为回调函数,在数据发生变化时会触发。
在Watcher中,通过get()方法获取ViewModel中的数据对象的属性的值,并将Watcher对象存储到Dep实例中的subs数组中。
当数据发生变化时,触发Dep的set()方法,通知Dep实例中的所有订阅者进行更新,并调用Watcher的update()方法来更新视图,同时将新的值传递到回调函数cb中。
四、示例说明
下面给出两个示例,分别演示了基本的数据绑定和模板编译如何工作。
示例1:基本双向绑定
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>双向绑定实例1</title>
</head>
<body>
<div id="app">
<input type="text" v-model="msg">
<p>{{ msg }}</p>
</div>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script>
let vm = new Observer({
msg: 'hello world'
})
compile('#app', vm)
</script>
</body>
</html>
当用户在input框中输入文字时,可以看到下面的p标签实时更新为input框中的文字。
示例2:模板编译
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>双向绑定实例2</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="item in list">
<input type="checkbox" v-model="item.checked">{{ item.title }}
</li>
</ul>
<button v-on:click="addItem">新增</button>
</div>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script>
let vm = new Observer({
list: [
{title: '任务1', checked: true},
{title: '任务2', checked: false},
{title: '任务3', checked: false}
]
})
compile('#app', vm)
</script>
</body>
</html>
该示例中,展示了如何在模板中使用v-for、v-model和v-on等指令,并通过模板编译将其和ViewModel中的数据建立起有效的双向绑定关系。
用户可以尝试增加、选择或取消选择其中一个任务,并观察页面的变化。
五、总结
本文主要介绍了Vue中实现数据双向绑定的原理以及基本实现方法,通过对数据劫持和发布者-订阅者模式的结合,实现了数据的双向绑定。同时,我们还实现了模板编译的功能,将ViewModel中的数据与模板建立双向绑定,在数据发生变化时,实现了及时地更新视图。这就实现了Vue的MVVM模式,使得前端开发更加容易维护和扩展。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:vue MVVM双向绑定实例详解(数据劫持+发布者-订阅者模式) - Python技术站