利用 Object.defineProperty 和 MutationObserver 實作雙向綁定

雙向綁定就是 (一)可以經由改變 model 的值而自動更新 DOM 的內容,並且 (二)經由改變 DOM 的內容而自動更新 model 的值。以下分這兩部份來實作。

程式碼在這裡

利用 Object.defineProperty 實作 model 到 DOM 的更新

Object.defineProperty中有 get 和 set 兩個 method。get method 的回傳值將設定為這個 property 的值,而 set method 的參數 (即傳入值) 將設定為這個 property 的新值。同時必須設定 configurable,表示這個 property 是否可被更新或刪除。由於這邊用到的 property 是需要被更新值或可能是自行設定的 (例如:data-*),因此要設定為 true。

使用Object.defineProperty來實現 model 到 DOM 更新的原因主要是利用 set method,(標註(1)) 因為只要物件的 property 有更動,就會觸發 set method 重設 property 的值,因此當使用 JS 更新 property 的值時,就會立即更新到 DOM 上。

在下面範例中,首先宣告了一個作為 model 的物件objobj內的 key-value pair 即對應到要綁定的 HTML 的屬性。例如obj = { value: 123456789, data: 'apple' },就表示對應到 HTML 上<input class="inputGroup" value="hello" data="world" />的屬性「value」和「data」,而接下來由於 model 的值更新為「987654321」,HTML 上 value 的值也會從現在的「hello」更新為「987654321」。

若同時有多個 DOM element 需要綁定(標註(2)),則在使用twoWayBinding()初始化的時候一起丟進 nodeList 就可以了,每次同步的時候便會一起做更新。這裡要注意,由於使用document.getElementsByClassName抓到的是 HTML Collection,因此當動態加入同樣綁定inputGroup這個 class name 的時候即可連動,也就是說 document 會幫我們自動維護這份清單,但 change 事件並沒有一起幫我們綁定,可以利用document.addEventListener('DOMNodeInserted', handleNewElement, false);綁定(標註(3)),範例程式碼就先忽略這部份了。

範例:

//HTML
<input id="input-1" class="inputGroup" value="hello" />
<input id="input-2" class="inputGroup" value="world" />
function bindModelToDom(dom, list, data) {
  Array.apply(null, dom.attributes).forEach(function(attr, index) {
    var attrName = attr.name;
    Object.defineProperty(data, attrName, {
      get: function() {
        return dom[attrName];
      },
      set: function(newValue) { //(1) trigger when property change
        dom[attrName] = newValue; //set proterty
        dom.setAttribute(attrName, newValue); //set attribute

        //(2)sync list
        for(var i = 0; i < list.length; i++) {
          var item = list[i];
          item[attrName] = newValue;
          item.setAttribute(attrName, newValue);
        }
      },
      configurable: true
    })
  })

  return data
}

function twoWayBinding(list, obj) {
  var target = Object.assign({}, obj);

  for(var i = 0; i < list.length; i++) {
    obj = bindModelToDom(list[i], list, obj);
  }

  for(var property in target) {
    if(target.hasOwnProperty(property)) {
      obj[property] = target[property];
    }
  }

  function handleNewElement(element) {
    //(3)為新加入的DOM element綁定change事件
  }

  document.addEventListener('DOMNodeInserted', handleNewElement, false);
}

var obj = { value: 123456789, data: 'apple' };
var nodeList = document.getElementsByClassName('inputGroup');
twoWayBinding(nodeList, obj);

obj.value = 987654321; //兩個input value皆更新為987654321

利用 MutationObserver 實作 DOM 到 model 的更新

只要DOM有更動,就會觸發 MutationObserver。

因為在 input 中輸入值並非改變 DOM element,也就是不會觸發MutationObserver來修改 model,因此要幫input綁定 change 事件,當 input 輸入值時也能做更新的動作 (標註(4))。

標註(5) 的地方不做 setAttribute,以免觸發 MutationObserver 進入無窮迴圈,有興趣的可以看這篇討論文章。

範例:

function bindDomToModel(target, list) {
  var observer = new MutationObserver(function(mutations) {
    var attrName = mutations[0].attributeName;
    var newVal = target.getAttribute(attrName);

    //sync list
    for(var i = 0; i < list.length; i++) {
      list[i][attrName] = newVal; //(5)
    }
  });

  var config = {
    attributes: true,
    childList: false,
    characterData: false
  };

  observer.observe(target, config);

  //(4)為input綁定change事件
  target.addEventListener("change", function() {
    var value = this.value;

    //sync list
    for(var i = 0; i < list.length; i++) {
      list[i].setAttribute('value', value);
      list[i].value = value;
    }
  });
 };

function twoWayBinding(list) {
  for(var i = 0; i < list.length; i++) {
    bindDomToModel(list[i], list);
  }
};

var nodeList = document.getElementsByClassName('title');
twoWayBinding(nodeList);

參考資料


javascript vue.js 雙向綁定 two-way binding