VUE的數據雙向綁定
1、概述
讓我們先來看一下官網的這張數據綁定的說明圖:
原理圖告訴我們,a對象下面的b屬性定義了getter、setter對屬性進行劫持,當屬性值改變是就會notify通知watch對象,而watch對象則會notify到view上對應的位置進行更新(這個地方還沒講清下面再講),然後我們就看到了視圖的更新了,反過來當在視圖(如input)輸入數據時,也會觸發訂閱者watch,更新最新的數據到data裏面(圖中的a.b),這樣model數據就能實時響應view上的數據變化了,這樣一個過程就是數據的雙向綁定了。
看到這裏就會第一個疑問:那麽setter、getter是怎樣實現的劫持的呢?答案就是vue運用了es5中Object.defineProperty()這個方法,所以要想理解雙向綁定就得先知道Object.defineProperty是怎麽一回事了;
2.Object.defineProperty
它是es5一個方法,可以直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個對象,對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個擁有可寫或不可寫值的屬性。存取描述符是由一對 getter-setter 函數功能來描述的屬性。描述符必須是兩種形式之一;不能同時是兩者。
屬性描述符包括:configurable(可配置性相當於屬性的總開關,只有為true時才能設置,而且不可逆)、Writable(是否可寫,為false時將不能夠修改屬性的值)、Enumerable(是否可枚舉,為false時for..in以及Object.keys()將不能枚舉出該屬性)、get(一個給屬性提供 getter 的方法)、set(一個給屬性提供 setter 的方法)
var o = {name:‘vue‘};
Object.defineProperty(o, "age",{ value : 3,
writable : true,//可以修改屬性a的值
enumerable : true,//能夠在for..in或者Object.keys()中枚舉
configurable : true//可以配置
});
Object.keys(o)//[‘name‘,‘age‘]
o.age = 4;
console.log(o.age) //4
var bValue;
Object.defineProperty(o, "b", {
get : function(){
return bValue;
},
set : function(newValue){
console.log(‘haha..‘)
bValue = newValue;
},
enumerable : true,//默認值是false 及不能被枚舉
configurable : true//默認也是false
});
o.b = ‘something‘;
//haha..
上面分別給出了對象屬性描述符的數據描述符和存取描述的例子,註意一點是這兩種不能同時擁有,也就是valuewritable不能和getset同時具備。在這裏只是很粗淺的說了一下Object.defineProperty這個方法,要了解更多可以點擊這裏
3.實現observer
我們在上面一部分講到了es5的Object.defineProperty()這個方法,vue正式通過它來實現對一個對象屬性的劫持的,在創建實例的時候vue會對option中的data對象進行一次數據格式化或者說初始化,給每個data的屬性都設置上get/set進行對象劫持,代碼如下:
function Observer(data){
this.data = data;
if(Array.isArray(data)){
protoAugment(data,arrayMethods); //arrayMethods實現對Array.prototype原型方法的拷貝;
this.observeArray(data);
}else{
this.walk(data);
}
}
Observer.prototype = {
walk:function walk(data){
var _this = this;
Object.keys(data).forEach(function(key){
_this.convert(key,data[key]);
})
},
convert:function convert(key,val){
this.defineReactive(this.data,key,val);
},
defineReactive:function defineReactive(data,key,val){
var ochildOb = observer(val);
var _this = this;
Object.defineProperty(data,key,{
configurable:false,
enumerable:true,
get:function(){
console.log(`i get the ${key}-->${val}`)
return val;
},
set:function(newVal){
if(newVal == val)return;
console.log(`haha.. ${key} changed oldVal-->${val} newVal-->${newVal}`);
val = newVal;
observer(newVal);//在這裏對新設置的屬性再一次進行get/set
}
})
},
observeArray:function observeArray(items){
for (var i = 0, l = items.length; i < l; i++) {
observer(items[i]);
}
}
}
function observer(data){
if(!data || typeof data !==‘object‘)return;
return new Observer(data);
}
//讓我們來試一下
var obj = {name:‘jasonCloud‘};
var ob = observer(obj);
obj.name = ‘wu‘;
//haha.. name changed oldVal-->jasonCloud newVal-->wu
obj.name;
//i get the name-->wu
到這一步我們只實現了對屬性的set/get監聽,但並沒實現變化後notify,那該怎樣去實現呢?在VUE裏面使用了訂閱器Dep,讓其維持一個訂閱數組,但有訂閱者時就通知相應的訂閱者notify。
let _id = 0;
/*
Dep構造器用於維持$watcher檢測隊列;
*/
function Dep(){
this.id = _id++;
this.subs = [];
}
Dep.prototype = {
constructor:Dep,
addSub:function(sub){
this.subs.push(sub);
},
notify:function(){
this.subs.forEach(function(sub){
if(typeof sub.update == ‘function‘)
sub.update();
})
},
removeSub:function(sub){
var index = this.subs.indexOf(sub);
if(index >-1)
this.subs.splice(index,1);
},
depend:function(){
Dep.target.addDep(this);
}
}
Dep.target = null; //定義Dep的一個屬性,當watcher時Dep.targert=watcher實例對象
在這裏構造器Dep,維持內部一個數組subs,當有訂閱時就addSub進去,通知訂閱者更新時就會調用notify方法通知到訂閱者;我們現在合並一下這兩段代碼
function Observer(data){
//省略的代碼..
this.dep = new Dep();
//省略的代碼..
}
Observer.prototype = {
//省略的代碼..
defineReactive:function defineReactive(data,key,val){
//省略的代碼..
var dep = new Dep();
Object.defineProperty(data,key,{
configurable:false,
enumerable:true,
get:function(){
if(Dep.target){
dep.depend();
//省略的代碼..
}
return val;
},
set:function(newVal){
//省略的代碼..
dep.notify();
}
})
},
observeArray:function observeArray(items){
for (var i = 0, l = items.length; i < l; i++) {
observer(items[i]);
}
}
}
function observer(data){
if(!data || typeof data !==‘object‘)return;
return new Observer(data);
}
上面代碼中有一個protoAugment方法,在vue中是實現對數組一些方法的重寫,但他並不是直接在Array.prototype.[xxx]直接進行重寫這樣會影響到所有的數組中的方法,顯然是不明智的,vue很巧妙的進行了處理,使其並不會影響到所有的Array上的方法,代碼可以點擊這裏
到這裏我們實現了數據的劫持,並定義了一個訂閱器來存放訂閱者,那麽誰是訂閱者呢?那就是Watcher,下面讓我們看看怎樣實現watcher
4.實現一個Watcher
watcher是實現view視圖指令及數據和model層數據聯系的管道,當在執行編譯時候,他會把對應的屬性創建一個Watcher對象讓他和數據層model建立起聯系。但數據發生變化是會觸發update方法更新到視圖上view中,反過來亦然。
function Watcher(vm,expOrFn,cb){
this.vm = vm;
this.cb = cb;
this.expOrFn = expOrFn;
this.depIds = {};
var value = this.get(),valuetemp;
if(typeof value === ‘object‘ && value !== null){
if(Array.isArray(value)){
valuetemp = [];
for(var i = 0,len = value.length;i<len;i++){
valuetemp.push(value[i]);
}
}else{
valuetemp = {};
for(var j in value){
valuetemp[j] = value[j];
}
}
this.value = valuetemp;
}else{
this.value = value;
}
};
Watcher.prototype = {
update:function(){
this.run();
},
run:function(){
var val = this.get(),valuetemp;
var oldVal = this.value;
if(val!==oldVal){
if(typeof val === ‘object‘ && val !== null){
if(Array.isArray(val)){
valuetemp = [];
for(var i = 0,len = val.length;i<len;i++){
valuetemp.push(val[i]);
}
}else{
valuetemp = {};
for(var j in val){
valuetemp[j] = val[j];
}
}
this.value = valuetemp;
}else{
this.value = val;
}
this.cb.call(this,val,oldVal);
}
},
get:function(){
Dep.target = this;
var val = this.getVMVal();
Dep.target = null;
return val;
},
getVMVal:function(){
var exps = this.expOrFn.split(‘.‘);
var val = this.vm._data;
exps.forEach(function(key){
val = val[key];
})
return val;
},
addDep:function(dep){
if(!this.depIds.hasOwnProperty(dep.id)){
dep.addSub(this);
this.depIds[dep.id] = dep;
}
}
}
到現在還差一步就是將我們在容器中寫的指令和{{}}讓他和我們的model建立起連續並轉化成,我們平時熟悉的html文檔,這個過程也就是編譯;編譯簡單的實現就是將我們定義的容器裏面所有的子節點都獲取到,然後通過對應的規則進行轉換編譯,為了提高性能,先創建一個文檔碎片createDocumentFragment(),然後操作都在碎片中進行,等操作成功後一次性appendChild進去;
function Compile(el,vm){
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if(this.$el){
this.$fragment = this.nodeToFragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
this.$vm.$option[‘mount‘] && this.$vm.$option[‘mount‘].call(this.$vm);
}
}
5.實現一個簡易版的vue
到目前為止我們可以實現一個簡單的數據雙向綁定了,接下來要做的就是對這一套流程進行整合了,不多說上碼
function Wue(option){
this.$option = option;
var data = this._data = this.$option.data;
var _this = this;
//數據代理實現數據從vm.xx == vm.$data.xx;
Object.keys(data).forEach(function(val){
_this._proxy(val)
});
observer(data)
this.$compile = new Compile(this.$option.el , this);
}
Wue.prototype = {
$watch:function(expOrFn,cb){
return new Watcher(this,expOrFn,cb);
},
_proxy:function(key){
var _this = this;
Object.defineProperty(_this,key,{
configurable: false,
enumerable: true,
get:function(){
return _this._data[key];
},
set:function(newVal){
_this._data[key] = newVal;
}
})
}
}
在這裏定義了一個Wue構造函數,當實例化的時候他會對option的data屬性進行格式化(劫持),然後再進行編譯,讓數據和視圖建立起聯系;在這裏用_proxy進行數據代理是為了當訪問數據時可以直接vm.xx而不需要vm._data.xx;
VUE的數據雙向綁定