JavaScript實現簡單的雙向資料繫結(Ember、Angular、Vue)
什麼是雙向資料繫結呢?
簡單的說
就是UI檢視與資料繫結在了一塊
也就是資料和檢視是同步改變的
雙向資料繫結最常見的應用場景就是表單
(應用場景還是很有限的)
現在我們要實現這樣一個簡單的資料繫結
輸入欄中輸入字元
和它繫結的節點內容同步改變
此外還有一個按鈕用於生成隨機數改變input和div內的資料
首先我們先把需要把html的簡單結構實現
<input id="input" data-bind="demo">
<div id="output" data-bind="demo"></div>
<button id ="random">隨機數</button>
還需要在js中獲取這些DOM節點
let $ = document.querySelector.bind(document);
let $i = $('#input');
let $o = $('#output');
let $random = $('#random');
簡易實現
如果僅僅是為了實現這樣的效果實際上非常簡單
我們很容易就可以想到input事件,然後動態改變
那麼我們首先就來簡單的實現一下
let def = 'default';
$i.value = def;
$o.textContent = def;
$i .oninput = function(){
$o.textContent = $i.value;
}
$random.onclick = function(){
let rand = Math.floor(Math.random()*10e5)
$i.value = rand;
$o.textContent = rand;
}
雖然實現了效果
但是實際上只有檢視改變,影響資料改變的過程
而且也沒有把節點聯絡在一起
資料模型(Ember.js原理)
Ember.js使用了這種資料模型的方法
雖然很麻煩,但是很容易讓我們理解
實際上就是把資料還有要節點封裝在了一起
這樣後續的更新一定會經過這個模型
模型就瞭解了變化
從而做出處理
首先我們來實現一個數據模型的類
當我們需要繫結一組節點時
就可以例項化這個資料模型
(為了方便下面我都使用ES6語法)
class DataModel {
constructor(str = ''){
this.data = str;
this.nodes = [];
}
bindTo(node){
this.nodes.push(node);
this.update();
}
update(){
const INPUT_NODE = ['INPUT','TEXTAREA'];
let {nodes} = this;
for(let i = 0, node; node = nodes[i++];){
if(INPUT_NODE.includes(node.nodeName)){
if(node.value !== this.data){ //避免游標跳到尾部
node.value = this.data;
}
}else{
node.textContent = this.data;
}
}
}
set(str){
if(str !== this.value){
this.data = str;
this.update();
}
}
get(){
return this.data;
}
}
this.data
就是我們模型的資料
而this.nodes
是我們繫結的節點列表
bindTo
方法接受我們的Dom節點並傳入節點列表
既然有新節點進加入組織了(節點繫結),那麼也肯定要讓它接受新的資料(資料與UI改變)
update
方法用於更新檢視,實際上是遍歷所有繫結節點,判斷型別然後做出改變
聲明瞭資料模型類後我們就可以為input和div繫結到一個模型中了
let gModel = {
demo: new DataModel('default')
};
//資料->檢視
gModel[$i.getAttribute('data-bind')].bindTo($i);
gModel[$o.getAttribute('data-bind')].bindTo($o);
gModel
是我們宣告的一個全域性資料模型物件
因為頁面中不一定只有這一組資料繫結
$i
與$o
的data-bind
屬性值就相當於它們的“組織名”
這裡我就起名demo了
使用模型的API來繫結這兩個節點
//檢視->資料
$i.addEventListener('input', function(){
gModel[this.getAttribute('data-bind')].set(this.value);
});
$random.onclick = function(){
gModel.demo.set(Math.floor(Math.random()*10e5));
}
最後繫結input事件還有按鈕的click事件
當輸入值後,就改變這個模型的data
set方法改變data的同時還會觸發所有與之繫結在一起的節點做出更新
髒檢查(Angular.js原理)
Augular.js採用髒檢查的方式來實現雙向資料繫結
其原理是不會去監聽資料的變化
而是我覺得你可能要發生資料變化的時候(使用者互動,DOM操作等)
就去檢查你的所有資料,看看到底有沒有變化
不過這個資料檢查是元件級別的
雖然如此,很多時候還是會產生很多沒用的檢查
我們需要模擬以下元件的核心函式
class Scope {
constructor(){
this.nodes = [];
this.watchers = [];
}
watch(watchExp, listener = function(){}){
this.watchers.push({
watchExp,
listener
});
}
digest(){
let dirty;
let {watchers} = this;
do {
dirty = false;
for(var i = 0, watcher; watcher = watchers[i++];){
let newValue = watcher.watchExp();
let oldValue = watcher.last;
if(newValue !== oldValue){
dirty = true;
watcher.listener(newValue, oldValue);
watcher.last = newValue;
}
}
}while(dirty);
}
update(newValue){
const INPUT_NODE = ['INPUT','TEXTAREA'];
let {nodes} = this;
for(let i = 0, node; node = nodes[i++];){
if(INPUT_NODE.includes(node.nodeName)){
if(node.value !== newValue){
node.value = newValue;
}
}else{
node.textContent = newValue;
}
}
}
bindTo(node){
let {nodes} = this;
let key = node.getAttribute('data-bind');
if(!key){
return;
}
nodes.push(node);
this.update(this[key]);
this.watch(() => {
return this[key];
}, (newValue, oldValue) => {
this.update(newValue);
});
}
}
區別於上一種方法
這裡的this.nodes
代表某元件的全部節點
this.watchers
陣列儲存著watcher
每一個watcher封裝著用於髒檢查的函式
而watch方法就負責向watchers中新增watcher
它接受兩個引數,一個取值函式watchExp和一個回撥函式listener
digest方法會遍歷整個watcher
last儲存著上一個值,再通過取值函式獲取值
通過比較可以知道值有沒有變髒
如果髒了,就觸發回撥函式(渲染資料)並且更新last值
還要重新檢查一遍watchers確保last和資料一致
(這裡沒有處理互相繫結死迴圈的問題,可以設定檢查上限)
宣告完元件類,我們就可以例項化一個元件
繫結節點,監聽事件,還要手動進行髒檢查
let scope = new Scope();
scope.demo = 'default';
//資料->檢視
scope.bindTo($i);
scope.bindTo($o);
//檢視->資料
$i.addEventListener('input', function(){
scope[this.getAttribute('data-bind')] = this.value;
scope.digest();
});
$random.onclick = function(){
scope.demo = Math.floor(Math.random()*10e5);
scope.digest();
}
訪問器監聽(Vue.js原理)
vue.js實現資料變化影響檢視變化的方式便是利用了ES5的setter
資料改變,觸發setter渲染檢視
檢視影響資料沒什麼好說的,肯定需要監聽input事件
這裡我就寫的簡單點了
let data = {};
let def = 'default';
$i.value = def;
$o.textContent = def;
//資料->檢視
Object.defineProperty(data, 'demo', {
set: function(newValue){
$i.value = newValue;
$o.textContent = newValue;
}
});
//檢視->資料
$i.addEventListener('input', function() {
data[this.getAttribute('data-bind')] = this.value;
});
$random.onclick = function() {
data.demo = Math.floor(Math.random()*10e5);
};
實際上vue實現的要比這複雜多得多
因為setter在很多情況下並不是萬金油
也就是說並不是物件屬性的任何變動它都能夠監聽的到
比如說以下場景:
- 向物件新增新屬性
- 刪除現有屬性
- 陣列改變
關於這些問題這裡就不討論了
如果有時間的同學可以去研究以下原始碼
此外還要說明一下
原本ES7草案中的Object.observe()由於嚴重的效能問題已經被移除了