几种实现双向绑定的做法
1、发布者-订阅者模式(backbone.js)
2、脏值检查(angular.js)
3、数据劫持(vue.js)
vue.js则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动是发布消息给订阅者,触发相应的监听回调。
思路整理
要实现mvvm的双向绑定,就必须实现一下几点:
- 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅
者; - 2、实现一个指令解析器Compile,对每个元素的指令进行扫描和解析,根据指令模版替换数据,以及绑定相应的更新函数。
- 3、实现一个Watcher,作为Observer和Compile的桥梁,能够订阅并收到每个属性的变动通知,执行指令绑定相应的回调函数,从而更新视图。
代码实现
1、实现数据监听器Observer
function observe (obj, vm){
if (!data || typeof data !== 'object') {
return;
}
//取出所有属性遍历
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key , obj[key]);
});
}
function defineReactive(obj, key, val){
Object.defineProperty(obj, key, {
get: function(){
return val;
},
set: function(newVal){
if(newVal === val) return;
val = newVal;
console.log(val);
}
});
}
2、实现指令解析器Compile。这里需要用到文档片段DocumentFragment,它可以包含多个子节点,当我们将它插入到DOM中时,只有他的子节点会插入到目标节点中。用DocumentFragement处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂在目标的所有子节点劫持(通过append方法,原DOM中的节点会被自动删除,所以是真的劫持啊~)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。
/*编译模板*/
function nodeToFragment(node, vm){
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
compile(child);
flag.appendChild(child);
}
return flag;
}
function compile (node, vm){
var reg = /\{\{(.*)\}\}/;
if(node.nodeType === 1){ //节点类型为元素
var attr = node.attributes;
for(var i = 0, alen = attr.length; i < alen; i++) {
if(attr[i].nodeName == 'v-model' ){
var name = attr[i].nodeValue; //获取v-model绑定的属性名
// 对监听该node的input事件,当有输入时,把新值赋给vm的data
node.addEventListener('input', function (e) {
//给对应的data属性赋值,进而触发该属性的set方法
vm[name] = e.target.value;
})
node.value = vm[name];
node.removeAttribute('v-model');
}
}
}
if(node.nodeType === 3){ //节点类型为text
if(reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
node.nodeValue = vm[name];
}
}
}
由此实现了:文本框以及文本节点与vue实例中data属性的数据绑定,当输入框内容变化时,data属性中的数据同步变化。
接下来,需要实现data属性中的数据变化时,文本节点的内容同步变化。
3、这里插播一下订阅发布模式(subscribe&publish)
订阅发布模式定义了一种一对多的关系,让多个观察者同事监听某一个主题对象,这个主题对象的状态发生改变时,就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 =》 订阅者执行响应操作
为了要实现“data属性中的数据变化时,文本节点的内容同步变化”,当set方法触发后做的第二件事就是就是作为发布者发出通知,文本节点作为订阅者,在收到通知后执行响应的更新操作。
所以基本思路是:
1、在监听数据的过程中,为data中的每一个属性生成一个主题对象dep。
2、在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会自己添加到响应属性的dep中。
目前已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法
接下来实现:发出通知 dep.notify() => 触发订阅者的 update 方法 => 更新视图。
/*发布订阅者*/
function Watcher(vm, node, name){
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function(){
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
get: function(){
this.value = this.vm[this.name]; //触发响应属性的get;
}
}
function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub){
sub.update();
})
}
}
为了给每一个属性生成一个主题对象,所以原来的observe新增new Dep()
,为了在属性改变时发出通知,还需要在set方法里执行dep.notify()方法;
在编译HTML时,为每一个数据绑定的节点生成一个订阅者。
image.png
完整代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>双向绑定</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script type="text/javascript">
/*监听数据*/
function observe (obj, vm){
if (!obj || typeof obj !== 'object') {
return;
}
//取出所有属性遍历
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key , obj[key]);
});
}
function defineReactive(obj, key, val){
//为每一个属性生成一个主题对象。
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function(){
// 添加订阅者 watcher 到主题对象 Dep
if(Dep.target) dep.addSub(Dep.target);
return val;
},
set: function(newVal){
if(newVal === val) return;
val = newVal;
dep.notify(); //数据改变时发出通知
console.log(val);
}
});
}
/*编译模板*/
function nodeToFragment(node, vm){
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
compile(child, vm);
flag.appendChild(child);
}
return flag;
}
function compile (node, vm){
var reg = /\{\{(.*)\}\}/;
if(node.nodeType === 1){ //节点类型为元素
var attr = node.attributes;
for(var i = 0, alen = attr.length; i < alen; i++) {
if(attr[i].nodeName == 'v-model' ){
var name = attr[i].nodeValue; //获取v-model绑定的属性名
// 对监听该node的input事件,当有输入时,把新值赋给vm的data
node.addEventListener('input', function (e) {
//给对应的data属性赋值,进而触发该属性的set方法
vm[name] = e.target.value;
})
// node.value = vm[name];
node.removeAttribute('v-model');
}
}
new Watcher(vm, node, name, 'input');
}
if(node.nodeType === 3){ //节点类型为text
if(reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
// node.nodeValue = vm[name];
new Watcher(vm, node, name, 'text');
}
}
}
Watcher.prototype = {
update: function(){
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
get: function(){
this.value = this.vm[this.name]; //触发响应属性的get;
}
}
/*发布订阅者*/
function Watcher(vm, node, name, nodeType){
Dep.target = this;
this.vm = vm;
this.node = node;
this.nodeType = nodeType;
this.name = name;
this.update();
Dep.target = null;
}
function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub){
sub.update();
})
}
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
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 返回到 app 中
document.getElementById(id).appendChild(dom);
}
</script>
</body>
</html>
参考:
https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension
https://github.com/DMQ/mvvm
网友评论