什麼是雙向數據綁定?Vue是一個MVVM框架,數據綁定簡樸來講,就是當數據發生變化時,相應的視圖會舉行更新,當視圖更新時,數據也會隨着變化。
完成數據綁定的體式格局大抵有以下幾種:
- 1、宣布者-定閱者形式(backbone.js)
- 2、臟值搜檢(angular.js)
- 3、數據挾制(vue.js)
宣布者-定閱者形式
平常經由歷程sub, pub的體式格局完成數據和視圖的綁定監聽,更新數據體式格局一般做法是 vm.set(‘property’, value),有興緻可參考這裏
我們更願望能夠經由歷程 vm.property = value 這類體式格局舉行數據更新,同時自動更新視圖。
臟值搜檢
angular是經由歷程臟值搜檢體式格局來對照數據是不是變化,來決議是不是更新視圖,最常見的體式格局是經由歷程setInterval()來監測數據變化,固然,只會在某些指定事宜觸發時下才舉行臟值搜檢。大抵以下:
- DOM事宜,比如用戶輸入文本,點擊按鈕等。( ng-click )
- XHR相應事宜 ( $http )
- 瀏覽器Location更改事宜 ( $location )
- Timer事宜( $timeout , $interval )
- 實行 $digest() 或 $apply()
數據挾制
Vue.js則是經由歷程數據挾制以及連繫宣布者-定閱者來完成的,數據挾制是應用ES5的Object.defineProperty(obj, key, val)來挾制各個屬性的的setter以及getter,在數據更改時宣布音訊給定閱者,從而觸發相應的回調來更新視圖。
一、完成最基本的數據綁定
<input type="text" id="in"/>
輸入的值為:<span id="out"></span>
<script>
var int = document.getElementById('in');
var out = document.getElementById('out');
var obj = {};
Object.defineProperty(obj, 'msg', {
enumerable: true,
configurable: true,
set (newVal) {
out.innerHTML = newVal;
}
})
int.addEventListener('input', function(e) {
obj.msg = e.target.value;
})
</script>
二、雙向數據綁定完成(此處用MVue替代)
上面的只是簡樸的運用了Object.defineProperty(),並非我們終究想要的結果,終究想要的結果以下:
<div id="app">
<input type="text" v-model="text">
輸入的值為:{{text}}
<div>
<input type="text" v-model="text">
</div>
</div>
<script>
var vm = new MVue({
el: '#app',
data: {
text: 'hello world'
}
})
</script>
完成思緒:
1、輸入框以及文本節點和data中的數據舉行綁定
2、輸入框內容變化時,data中的對應數據同步變化,即 view => model
3、data中數據變化時,對應的文本節點內容同步變化 即 model => view
上述流程如圖所示:
1、完成一個數據監聽器Obverser,對data中的數據舉行監聽,如有變化,關照相應的定閱者。
2、完成一個指令剖析器Compile,關於每一個元素上的指令舉行剖析,依據指令替代數據,更新視圖。
3、完成一個Watcher,用來銜接Obverser和Compile, 併為每一個屬性綁定相應的定閱者,當數據發生變化時,實行相應的回調函數,從而更新視圖。
4、組織函數 (new MVue({}))
MVue組織函數
在初始化MVue實例時,對data中每一個屬性挾制監聽,同時舉行模板編譯,指令剖析,末了掛載到相應的DOM中。
function MVue (options) {
this.$el = options.el;
this.$data = options.data;
// 初始化操縱,背面會說
// ...
}
1、完成 view => model
DocumentFragment(文檔片斷)
vue舉行編譯時,將掛載目的的一切子節點挾制到DocumentFragment中,經由一份剖析等處置懲罰后,再將DocumentFragment團體掛載到目的節點上。
function nodeToFragment (node, vm) {
var flag = document.createDocumentFragment();
var child;
while (child = node.firstChild) {
compile(child, vm);
if (child.firstChild) {
var dom = nodeToFragment(child, vm);
child.appendChild(dom);
}
flag.appendChild(child);
}
return flag;
}
模板編譯(指令剖析,事宜綁定、初始化數據綁定)
編譯歷程圖
代碼以下:
function compile (node, vm) {
let reg = /\{\{(.*)\}\}/;
// 元素節點
if (node.nodeType === 1) {
var attrs = node.attributes;
for (let attr of attrs) {
if (attr.nodeName === 'v-model') {
// 獵取v-model指令綁定的data屬性
var name = attr.nodeValue;
// 綁定事宜
node.addEventListener('input', function(e) {
vm.$data[name] = e.target.value;
})
// 初始化數據綁定
node.value = vm.$data[name];
// 移除v-model 屬性
node.removeAttribute('v-model')
}
}
}
// 文本節點
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 && (RegExp.$1.trim());
// 綁定數據到文本節點中
node.nodeValue = node.nodeValue.replace(new RegExp('\\{\\{\\s*(' + name + ')\\s*\\}\\}'), vm.$data[name]);
}
}
}
如今,我們修正下MVue組織函數,增添模板編譯,以下:
function MVue (options) {
this.$el = options.el;
this.$data = options.data;
// 模板編譯
let elem = document.querySelector(this.$el);
elem.appendChild(nodeToFragment(elem, this))
}
那末,我們的view => model 已完成了,包含初始化綁定默認值,只需修正了input中的值,data中對應的值相應變化,並觸發了setter, 更新屬性值等(能夠自行在set要領中打印看結果,或許在掌握台手動輸入vm.$data.text也會看到結果)。
2、完成 model => view
上面能夠看出,雖然我們完成了初始化數據綁定,以及輸入框變化時,data中text也會變化,然則文本節點依然沒有任何變化,那末假如做到文本節點也同步變化呢,這裏用的是宣布者-定閱者形式。
宣布者-定閱者形式
宣布者-定閱者形式又稱為觀察者形式,讓多個觀察者同時監聽某個主題對象,當主題對象發生變化時,會關照一切的觀察者對象,即:宣布者發出關照給主題對象 => 主題對象接收到關照后推送給一切定閱者 => 定閱者實行相應的操縱。
1)起首,定義一個主題對象,用來網絡一切的定閱者,並供應notify要領,用來挪用定閱者的update要領,從而實行相應的操縱。
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub (sub) {
this.subs.push(sub);
},
notify () {
this.subs.forEach(sub => {
// 實行定閱者的update要領
sub.update();
})
}
}
不難看出,當text屬性變化時,會觸發set要領,作為宣布者,將數據更新音訊經由歷程主題對象發送給定閱者, 那末該怎樣關照呢?
我們曉得,在new一個vue時,會實行兩個操縱,一個事編譯模板,一個監聽data數據,在監聽data時,vue為data的每一個屬性都天生一個主題對象Dep,而在編譯模板時,會為每一個與數據綁定的節點天生一個Watcher,那末只需關聯了Dep與Watcher,是不是是就完成了音訊關照呢,癥結邏輯是完成兩者關聯。
已完成:輸入框變化 => 觸發相應的事宜,修正值 => 觸發set要領
須要完成:發出關照dep.notify() => 觸發定閱者update要領 => 更新視圖
我們修正下compile中文本節點內容(只修正部份)
// 文本節點
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 && (RegExp.$1.trim());
// 綁定數據到文本節點中
// node.nodeValue = node.nodeValue.replace(new RegExp('\\{\\{\\s*(' + name + ')\\s*\\}\\}'), vm.$data[name]);
new Watcher(vm, node, name);
}
}
2)其次、完成定閱者Watcher
function Watcher (vm, node, name) {
// 全局的、唯一
Dep.target = this;
this.node = node;
this.name = name;
this.vm = vm;
this.index = index;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update () {
this.get();
this.node.nodeValue = this.value;
},
get () {
this.value = this.vm.$data[this.name]
}
}
起首,定義了一個全局的Dep.target,然後實行了update要領,進而實行了get要領,都去了this.vm的接見器屬性, 從而將定閱的音訊保存在該屬性的主題對象中,並終究將Dep.target設置為空,全局變量,是watcher和dep之間的唯一橋樑
,必需保證Dep.target只要一個值。
3)接着、完成一個obverser給data中每一個屬性增加一個主題對象
遍歷data中的一切屬性,包含子屬性對象的屬性
function obverser (obj) {
Object.keys(obj).forEach(key => {
if (obj.hasOwnProperty(key)) {
if (obj[key].constructor === 'Object') {
obverser(obj[key])
}
defineReactive(obj, key);
}
})
}
運用Object.definePeoperty()來監聽屬性更改,給屬性增加setter和getter
function defineReactive (obj, key) {
var _value= obj[key];
// new一個主題對象
var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
set (newVal) {
if (_value= newVal) {
return;
}
_value= newVal;
console.log(value)
// 作為宣布者發出關照給主題對象
dep.notify();
},
get () {
// 假如定閱者存在,增加到主題對象中
if (Dep.target) {
dep.addSub(Dep.target);
}
return _value
}
})
}
末了,我們須要再次修正組織函數MVue
function MVue (options) {
this.$el = options.el;
this.$data = options.data;
// 數據監聽
obverser(this.$data);
// 模板編譯
let elem = document.querySelector(this.$el);
elem.appendChild(nodeToFragment(elem, this))
}
如今,已完成了model => view的變化
當輸入框值變化時 => text也會變化 => 文本節點值變化
但假如仔細的話,會發明另有一個題目,當我們手動轉變text的值時(如在掌握台上輸入vm.$data.text = ‘xxx’),會發明,文本節點值已變化了,然則輸入框的值沒有變化。
假如給輸入框也增加一個Watcher,是不是是也就和文本節點一樣完成了呢,但須要注重的是,輸入框、文本框、下拉框等,是經由歷程value轉變值的,而不是nodeValuefa,由於能夠做以下修正:
compile中:
// 初始化數據綁定
// node.value = vm.$data[name];
new Watcher(vm, node, name);
// 移除v-model 屬性
node.removeAttribute('v-model')
wather中:
Watcher.prototype = {
update () {
this.get();
let _name;
if (this.index === 1) {
_name = this.name;
} else {
_name = this.value;
}
if (this.node.nodeName === 'INPUT') {
// 能夠增加TEXTAREA、SELECT等
this.node.value = this.value;
} else {
// this.node.nodeValue = this.value;
this.node.nodeValue = this.node.nodeValue.replace(new RegExp('\\{?\\{?\\s*(' + _name + ')\\s*\\}?\\}?'), this.value);
}
++this.index;
},
get () {
this.value = this.vm.$data[this.name]
}
}
OK,基本上落成。
獵取完全代碼,猛戳這裏
個人博客也能夠獵取完全代碼(https://jefferye.github.io)