JS雙向資料繫結

weixin_33924312發表於2018-12-05

雙向資料繫結簡述

雙向資料繫結,可以將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="輸入任意值"並按Enter鍵執行時,input輸入框的值也會跟著變,這就實現了model更改UI的效果~~~

可見,Object.defineProperty()實現雙向繫結比釋出訂閱模式簡單得多~~~

髒值檢查

是通過髒值檢測的方式比對資料是否有變更,來決定是否更新檢視,最簡單的方式就是通過 setInterval()定時輪詢檢測資料的變動。 髒值檢查實現較為複雜,暫時沒時間進行研究~~~

相關文章