阅读目录
前言:
双向数据绑定的含义:可以将对象的属性绑定到UI,具体的说,我们有一个对象,该对象有一个name属性,当我们给这个对象name属性赋新值的时候,新值在UI上也会得到更新。同样的道理,当我们有一个输入框或者textarea的时候,我们输入一个新值的时候,也会在该对象的name属性得到更新。
双向数据绑定的思想是:
1. 我们需要一个方法来识别那个UI元素被绑定了相对应的属性。
2. 我们需要监听属性和UI元素的变化。
3. 我们需要将所有的变化传播到绑定的对象和元素上。
实现数据绑定的做法有如下几种:
1. 发布者--订阅模式(backbone.js)
2. 脏值检查(angular.js)
3. 数据劫持 (vue.js)
一:发布订阅模式实现数据双向绑定
我们现在使用 发布者-订阅模式来实现一个简单的 双向绑定数据;
发布-订阅模式的原理:它是一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
先来理解下 现实生活中的发布-订阅模式
比如小红最近在淘宝网上看上一双鞋子,但是呢 联系到卖家后,才发现这双鞋卖光了,但是小红对这双鞋又非常喜欢,所以呢联系卖家,问卖家什么时候有货,卖家告诉她,要等一个星期后才有货,卖家告诉小红,要是你喜欢的话,你可以收藏我们的店铺,等有货的时候再通知你,所以小红收藏了此店铺,但与此同时,小明,小花等也喜欢这双鞋,也收藏了该店铺;等来货的时候就依次会通知他们;
在上面的故事中,可以看出是一个典型的发布订阅模式,卖家是属于发布者,小红,小明等属于订阅者,订阅该店铺,卖家作为发布者,当鞋子到了的时候,会依次通知小明,小红等。
发布订阅模式的优点:
1. 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
2. 发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变;
发布订阅模式的缺点:
1. 创建订阅者需要消耗一定的时间和内存。
2. 虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等。
实现发布-订阅模式的步骤:
1. 谁是发布者?(比如上面的卖家)
2. 给发布者添加一个缓存列表,用于存放回调函数来通知订阅者。
3. 发布消息,发布者遍历这个缓存列表,依次触发存放的订阅者回调函数。
下面我们先来实现一个简单的发布-订阅模式(用户订阅鞋子的简单demo如下:)
// 定义发布者 var pubSub = { // 缓存列表,存放订阅者的回调函数 callbacks: [], // 增加订阅者 on: function(msg, callback) { if (!this.callbacks[msg]) { // 如果没有订阅过此消息,给这个消息创建一个缓存列表 this.callbacks[msg] = []; } this.callbacks[msg].push(callback); }, // 发布消息 public: function() { // 取出该消息对应的回调函数集合 var key = Array.prototype.shift.call(arguments); var fns = this.callbacks[key]; // 如果没有订阅过该消息的话,直接返回 if(!fns || fns.length === 0) { return; } for(var i = 0, fn; fn = fns[i++]; ) { fn.apply(this, arguments); } } }; // 现在空智 订阅如下消息 pubSub.on('red', function(size) { console.log("你的尺码是:"+size); // 控制台会打印出: 你的尺码是:40 }); // 空智2 订阅如下消息 pubSub.on('blue', function(size) { console.log("你的尺码是:"+size); // 控制台会打印出: 你的尺码是:42 }); // 发布消息 pubSub.public('red', 40); pubSub.public('blue', 42);
通过上面了解了简单的发布-订阅模式,现在我们来看看使用发布-订阅模式来实现双向绑定。
思路如下:
在HTML代码中使用一个自定义属性进行绑定,所有进行绑定的javascript对象以及DOM元素都将订阅一个发布者对象。不管什么时候如果一个javascript对象或一个输入框被监听到发生变化时候,所有依赖发布者对象都将会得到通知。
代码如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport"> <meta content="yes" name="apple-mobile-web-app-capable"> <meta content="black" name="apple-mobile-web-app-status-bar-style"> <meta content="telephone=no" name="format-detection"> <meta content="email=no" name="format-detection"> <title>标题</title> <link rel="shortcut icon" href="/favicon.ico"> </head> <body> <h3>发布订阅模式实现数据双向绑定demo</h3> <input type="text" id="inputId" data-bind-objId="name" style="border: 1px solid #ccc; 200px; height: 24px; "/> <div id="modelView" style="border: 1px solid red; 200px; height: 24px; margin-top:20px; margin-bottom:20px;"></div> <button id="btn">model的变化导致view的变化</button> <script> function DataBinder(objId, changeId) { // 发布订阅原型 var pubSub = { allCallbacks: [], // 增加订阅者 on: function(eventName, callback) { // 如果没有订阅过该消息,给这个消息创建一个缓存列表 if(!this.allCallbacks[eventName]) { this.allCallbacks[eventName] = []; } this.allCallbacks[eventName].push(callback); }, // 发布消息 public: function() { var eventName = Array.prototype.shift.call(arguments); // 取出该消息对应的回调函数集合 var callbacks = this.allCallbacks[eventName]; if (!callbacks || callbacks.length === 0) { return false; } for (var i = 0; i < callbacks.length; i++) { var callback = callbacks[i]; callback.apply(this, arguments); } } }; var dataAttr = "data-bind-" + objId; var message = objId + ":change"; var changeHandler = function(e) { var target = e.target || e.srcElement; var attrName = target.getAttribute(dataAttr); if (attrName && attrName !== "") { // 发布消息 pubSub.public(message, attrName, target.value); } }; // 监听视图层的事件变化 if (document.addEventListener) { document.addEventListener('input', changeHandler, false); } else { document.attachEvent("oninput", changeHandler); } // 监听模型上的变化,并把变化传播到所有绑定的元素上 pubSub.on(message, function(attrName, newVal) { var elements = document.querySelectorAll("[" + dataAttr + "=" + attrName + "]"); var tagName; for (var i = 0, ilen = elements.length; i < ilen; i++) { tagName = elements[i].tagName.toLowerCase(); if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { elements[i].value = newVal; changeId.innerHTML = newVal; } else { elements[i].innerHTML = newVal; changeId.innerHTML = newVal; } } }); return pubSub; } // 定义一个User模型 function User(uid, changeId) { var binder = new DataBinder(uid, changeId); var user = { attrs: {}, set: function(key, value){ this.attrs[key] = value; // model变化通知更新view binder.public(uid + ":change", key, value); }, get: function(key) { return this.attrs[key]; } }; return user; } // 绑定model到view var modelView = document.getElementById("modelView"); var inputId = document.getElementById("inputId"); // 测试demo var user = new User("objId", modelView); user.set("name", 1); modelView.innerHTML = user.get("name"); // 测试模型的变化到 视图层的变化 var btn = document.getElementById("btn"); btn.onclick = function() { var value = inputId.value; user.set("name", parseInt(value) + 1); modelView.innerHTML = user.get("name"); }; </script> </body> </html>
二:使用Object.defineProperty 来实现简单的双向绑定。
想了解 Object.defineProperty 时的话 请看这篇文章
实现的效果简单如下:页面上有一个input输入框和div显示框,当在input输入框输入值的时候,div也会显示对应的值,当我打开控制台改变 obj.name="输入任意值"的时候,按回车键运行下,input输入框的值也会跟着变,可以简单的理解为 模型-> 视图的 改变,以及 视图 -> 模型的改变。如下代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport"> <meta content="yes" name="apple-mobile-web-app-capable"> <meta content="black" name="apple-mobile-web-app-status-bar-style"> <meta content="telephone=no" name="format-detection"> <meta content="email=no" name="format-detection"> <title>标题</title> <link rel="shortcut icon" href="/favicon.ico"> </head> <body> <h3>使用Object.defineProperty实现简单的双向数据绑定</h3> <input type="text" id="input" /> <div id="div"></div> <script> var obj = {}; var inputVal = document.getElementById("input"); var div = document.getElementById("div"); Object.defineProperty(obj, "name", { set: function(newVal) { inputVal.value = newVal; div.innerHTML = newVal; } }); inputVal.addEventListener('input', function(e){ obj.name = e.target.value; }); </script> </body> </html>