javascript中的雙向繫結

龍恩0707發表於2017-09-03

閱讀目錄

前言:

   雙向資料繫結的含義:可以將物件的屬性繫結到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; width:200px; height: 24px; "/>
    <div id="modelView" style="border: 1px solid red; width: 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="輸入任意值"的時候,按Enter鍵執行下,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>

檢視效果

相關文章