Vue之九數據劫持實現MVVM的數據雙向綁定
vue是通過數據劫持的方式來做數據綁定的,其中最核心的方法便是通過Object.defineProperty()
來實現對屬性的劫持,達到監聽數據變動的目的。
如果不熟悉defineProperty,猛戳這裏
整理了一下,要實現mvvm的雙向綁定,就必須要實現以下幾點:
1、實現一個數據監聽器Observer,能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值並通知訂閱者
2、實現一個指令解析器Compile,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數
3、實現一個Watcher,作為連接Observer和Compile的橋梁,能夠訂閱並收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖
4、mvvm入口函數,整合以上三者
1、實現Observer
我們知道可以利用Obeject.defineProperty()
來監聽屬性變動
那麽將需要observe的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 setter
和getter
這樣的話,給這個對象的某個值賦值,就會觸發setter
,那麽就能監聽到了數據變化。。相關代碼可以是這樣:
var data = {name: ‘kindeng‘}; observe(data); data.name = ‘dmq‘; // 監聽到值變化了 kindeng --> dmq function observe(data) { if (!data || typeof data !== ‘object‘) { return; } // 取出所有屬性遍歷 Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; function defineReactive(data, key, val) { observe(val); // 監聽子屬性 Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 不能再define get: function() { return val; }, set: function(newValue) { console.log(‘哈哈哈,監聽到值變化了 ‘, val, ‘ --> ‘, newValue); val = newValue; } }); }
上面的思路整理中我們已經明確訂閱者應該是Watcher, 而且var dep = new Dep();
是在 defineReactive
方法內部定義的,所以想通過dep
添加訂閱者
完整方法
function Observer(data) { this.data = data; this.walk(data); } Observer.prototype = { walk: function(data) { var me = this; Object.keys(data).forEach(function(key) { me.convert(key, data[key]); }); }, convert: function(key, val) { this.defineReactive(this.data, key, val); }, defineReactive: function(data, key, val) { var dep = new Dep(); var childObj = observe(val); Object.defineProperty(data, key, { enumerable: true, // 可枚舉 configurable: false, // 不能再define get: function() { if(Dep.target) { dep.depend(); } return val; }, set: function(newVal) { if(newVal === val) { return; } val = newVal; // 新的值是object的話,進行監聽 childObj = observe(newVal); // 通知訂閱者 dep.notify(); } }); } }; function observe(value, vm) { if(!value || typeof value !== ‘object‘) { return; } return new Observer(value); }; var uid = 0; function Dep() { this.id = uid++; this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); console.log(this.subs) }, depend: function() { Dep.target.addDep(this); }, removeSub: function(sub) { var index = this.subs.indexOf(sub); if(index != -1) { this.subs.splice(index, 1); } }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } }; Dep.target = null;
這裏已經實現了一個Observer了,已經具備了監聽數據和數據變化通知訂閱者的功能,那麽接下來就是實現Compile了
2、實現Compile
compile主要做的事情是解析模板指令,將模板中的變量替換成數據,然後初始化渲染頁面視圖,並將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖,如圖所示:
因為遍歷解析的過程有多次操作dom節點,為提高性能和效率,會先將跟節點el
轉換成文檔碎片fragment
進行解析編譯操作,解析完成,再將fragment
添加回原來的真實dom節點中
3、實現Watcher
Watcher訂閱者作為Observer和Compile之間通信的橋梁,主要做的事情是:
1、在自身實例化時往屬性訂閱器(dep)裏面添加自己
2、自身必須有一個update()方法
3、待屬性變動dep.notice()通知時,能調用自身的update()方法,並觸發Compile中綁定的回調,則功成身退。
實例化Watcher
的時候,調用get()
方法,通過Dep.target = watcherInstance
標記訂閱者是當前watcher實例,強行觸發屬性定義的getter
方法,getter
方法執行的時候,就會在屬性的訂閱器dep
添加當前watcher實例,從而在屬性值有變化的時候,watcherInstance就能收到更新通
4、實現MVVM
MVVM作為數據綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model數據變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變更的雙向綁定效果。
知。
function MVVM(options) { this.$options = options; var data = this._data = this.$options.data, me = this; // 屬性代理,實現 vm.xxx -> vm._data.xxx Object.keys(data).forEach(function(key) { me._proxy(key); }); observe(data, this); this.$compile = new Compile(options.el || document.body, this) } MVVM.prototype = { _proxy: function(key) { var me = this; Object.defineProperty(me, key, { configurable: false, enumerable: true, get: function proxyGetter() { return me._data[key]; }, set: function proxySetter(newVal) { me._data[key] = newVal; } }); } };
這裏主要還是利用了Object.defineProperty()
這個方法來劫持了vm實例對象的屬性的讀寫權,使讀寫vm實例的屬性轉成讀寫了vm._data
的屬性值
Vue之九數據劫持實現MVVM的數據雙向綁定