Vue.js自從在github上開源以來就受到各方的極大關注,並在短暫的時間裏當即火了起來,如今已成爲最流行的前端框架之一;我也使用vue有一段時間了,對vue的雙向綁定有必定的理解,在這和你們分享個人愚見,有錯誤的地方望你們給予指正。html
讓咱們先來看一下官網的這張數據綁定的說明圖:
原理圖告訴咱們,a對象下面的b屬性定義了getter、setter對屬性進行劫持,當屬性值改變是就會notify通知watch對象,而watch對象則會notify到view上對應的位置進行更新(這個地方還沒講清下面再講),而後咱們就看到了視圖的更新了,反過來當在視圖(如input)輸入數據時,也會觸發訂閱者watch,更新最新的數據到data裏面(圖中的a.b),這樣model數據就能實時響應view上的數據變化了,這樣一個過程就是數據的雙向綁定了。
看到這裏就會第一個疑問:那麼setter、getter是怎樣實現的劫持的呢?答案就是vue運用了es5中Object.defineProperty()這個方法,因此要想理解雙向綁定就得先知道Object.defineProperty是怎麼一回事了;前端
它是es5一個方法,能夠直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個對象,對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。數據描述符是一個擁有可寫或不可寫值的屬性。存取描述符是由一對 getter-setter 函數功能來描述的屬性。描述符必須是兩種形式之一;不能同時是二者。
屬性描述符包括:configurable(可配置性至關於屬性的總開關,只有爲true時才能設置,並且不可逆)、Writable(是否可寫,爲false時將不可以修改屬性的值)、Enumerable(是否可枚舉,爲false時for..in以及Object.keys()將不能枚舉出該屬性)、get(一個給屬性提供 getter 的方法)、set(一個給屬性提供 setter 的方法)vue
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這個方法,要了解更多能夠點擊這裏node
咱們在上面一部分講到了es5的Object.defineProperty()這個方法,vue正式經過它來實現對一個對象屬性的劫持的,在建立實例的時候vue會對option中的data對象進行一次數據格式化或者說初始化,給每一個data的屬性都設置上get/set進行對象劫持,代碼以下:git
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。github
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方法通知到訂閱者;咱們如今合併一下這兩段代碼segmentfault
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前端框架
watcher是實現view視圖指令及數據和model層數據聯繫的管道,當在執行編譯時候,他會把對應的屬性建立一個Watcher對象讓他和數據層model創建起聯繫。但數據發生變化是會觸發update方法更新到視圖上view中,反過來亦然。app
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); } }
到目前爲止咱們能夠實現一個簡單的數據雙向綁定了,接下來要作的就是對這一套流程進行整合了,很少說上碼
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的功能,並且還很殘缺,好比對象的深層綁定,以及計算屬性都尚未加入,做爲後續部分吧,最後得膜拜一下尤神,太牛叉了!
參考資料:
1.https://segmentfault.com/a/11...
2.https://segmentfault.com/a/11...
3.https://github.com/youngwind/...