JS双向数据绑定

本文深入探讨了双向数据绑定的概念及其实现方式,包括发布-订阅模式、脏值检查和数据劫持等方法,通过实例代码展示了如何利用这些技术在前端框架中实现数据与视图的同步更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

双向数据绑定简述

双向数据绑定,可以将JS对象的属性绑定到DOM节点上,实现JS对象跟DOM节点的同名属性的关联,改变一方时,另一方也会得到更新。

双向数据绑定的思想大致如下: 一、将DOM节点的属性跟JS对象的属性建立关联 二、监听JS属性跟DOM元素的变化 三、同时修改JS对象跟DOM元素

常见的实现数据绑定的做法有如下几种: 一、发布-订阅模式(backbone.js) 二、脏值检查(angular.js) 三、数据劫持(vue.js)

发布订阅模式实现

发布订阅模式详见这篇文章,原理是一种一对多的关系,让多个观察者对象同时监听发布者对象,当发布者发生改变时,所有观察者也会得到通知。

实现原理

通过发布订阅模式实现数据双向绑定的原理如下: 一、当model发送改变时,触发model change事件,然后通过相应的事件处理函数更新。 二、当界面更新时,触发UI change事件,然后通过相应的事件处理函数更新model,以及绑定在model上的其他界面控件。

依据这个思路,可以定义ui-update-event和model-update-event两个事件。下面将分别介绍。

具体实现

直接上代码~~~

<!DOCTYPE html>
<html>
	<head>
      <meta charset="utf-8"/>
      <title>发布订阅模式实现数据双向绑定</title>
      <style>
        #inputId {
          border:1px solid #ccc;
          width:200px;
          height:24px;
        }
        #modelView {
          border:1px solid black;
          width:200px;
          height:24px;
          margin-top:20px;
          margin-bottom:20px;
        }
      </style>
  </head>     
  <body>
   		<input type="text" id="inputId" d-binding="user.name"/>
    	<div id="modelView" d-binding="user.name"></div>
    	<button id="btn">model的变化导致view的变化</button>
    	<script>
    		// 发布订阅原型
			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 DataBinder = (function () {
				function changeHandler(e) {
					var target = e.target || e.srcElement;
					var attrName = target.getAttribute("d-binding");
					if (attrName && attrName !== "") {
						// 发布消息
						pubSub.public("ui-update-event", attrName, target.value);
					}
				};
				// 监听视图层的事件变化
			  if (document.addEventListener) {
					document.addEventListener('keyup', changeHandler, false);
					document.addEventListener('change', changeHandler, false);
				} else {
					document.attachEvent("onkeyup", changeHandler);
				  document.attachEvent("onchange", changeHandler);
				}
				// 监听模型上的变化,并把变化传播到所有绑定的元素上
				pubSub.on("model-update-event", function(attrName, newVal) {
					var elements = document.querySelectorAll('[d-binding="' + 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;
						} else {
							elements[i].innerHTML = newVal;
						}
					}
				});
				return {
				  modelName : "",
				  initModel : function (modelName) {
					var self = this;
					self.modelName = modelName;
					pubSub.on("ui-update-event", function(attrName, propValue){
					  var propPathArr = attrName.split(".");
					  self.updateModelData(propPathArr[1], propValue);
					});
					return Object.create(this);
				  },
				  loadModelData : function (modelData) {
					for (prop in modelData) {
					  this.updateModelData(prop, modelData[prop]);
					}
				  },
				  updateModelData : function (propName, propValue) {
					eval(this.modelName)[propName] = propValue;
					pubSub.public("model-update-event", this.modelName + '.' + propName, propValue);
				  }
				}
			})();

			var user = DataBinder.initModel("user");
			user.loadModelData({
			  'name' : 1
			});
			// 测试模型的变化到 视图层的变化 
			var btn = document.getElementById("btn");
			var inputId = document.getElementById("inputId");

			btn.onclick = function() {
			  var value = inputId.value;
			  user.updateModelData("name", parseInt(value) + 1);
			};

    	</script>
  </body>
</html>
复制代码
ui-update-event事件

对于所有支持双向绑定的页面控件,当值发生改变时,就会触发ui-update-event事件更新model,以及绑定在model上的其他控件。 触发ui-update-event时,先执行

pubSub.on("ui-update-event", function(attrName, propValue){
	var propPathArr = attrName.split(".");
	self.updateModelData(propPathArr[1], propValue);
});
复制代码

通过updateModelData方法去执行model-update-event,从而更新model。

updateModelData : function (propName, propValue) {
	eval(this.modelName)[propName] = propValue;
	pubSub.public("model-update-event", this.modelName + '.' + propName, propValue);
复制代码
model-update-event

对于model这一层,当model发生改变时,会触发model-update-event的监听事件

pubSub.on("model-update-event", function(attrName, newVal) {
	var elements = document.querySelectorAll('[d-binding="' + 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;
		} else {
			elements[i].innerHTML = newVal;
		}
	}
});
复制代码

从而修改了DOM元素的值。

属性劫持

Object.defineProperty()方法直接在对象上定义一个新属性,或修改对象上的现有属性,并返回该对象。 关于Object.defineProperty()的介绍如下:

Object.defineProperty(obj, prop, descriptor)

参数
    obj:定义属性的对象
	prop:要定义或修改的属性的名称。
    descriptor:定义或修改属性的描述符。

返回值:传递给函数的对象。
 
注意:数据描述符和访问器描述符,不能同时存在(value,writable 和 get,set)
 get:函数return将被用作属性的值。
set:该函数将仅接收参数赋值给该属性的新值。(在属性改变时调用)
复制代码
使用Object.defineProperty()实现双向数据绑定
<!DOCTYPE html>
 <html>
  <head>
    <meta charset="utf-8">
    <title>使用Object.defineProperty实现简单的双向数据绑定</title>
  </head>
  <body>
    <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>
复制代码

当在input输入框输入值的时候,div也会显示对应的值,实现了UI更改model的效果~~~

当在控制台输入 obj.name="输入任意值"并按回车键运行时,input输入框的值也会跟着变,这就实现了model更改UI的效果~~~

可见,Object.defineProperty()实现双向绑定比发布订阅模式简单得多~~~

脏值检查

是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval()定时轮询检测数据的变动。 脏值检查实现较为复杂,暂时没时间进行研究~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值