利用 Object.defineProperty 和 MutationObserver 實作雙向綁定
22 Jan 2017雙向綁定就是 (一)可以經由改變 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 的物件obj
,obj
內的 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);