對於vue.js
的動態數據綁定,通過反覆地看源碼和博客講解,總算可以理解它的實現了,心累~ 分享一下學習成果,同時也算是作個記錄。完整代碼GitHub地址:https://github.com/hanrenguang/Dynamic-data-binding。也能夠到倉庫的 README 閱讀本文。javascript
不知道有沒有同窗和我同樣,看着vue
的源碼殊不知從何開始,真叫人頭大。硬生生地看了observer
, watcher
, compile
這幾部分的源碼,只以爲一臉懵逼。最終,從這裏獲得啓發,做者寫得很好,值得一讀。html
關於動態數據綁定呢,須要搞定的是 Dep
, Observer
, Watcher
, Compile
這幾個類,他們之間有着各類聯繫,想要搞懂源碼,就得先了解他們之間的聯繫。下面來理一理:vue
Observer
所作的就是劫持監聽全部屬性,當有變更時通知 Dep
java
Watcher
向 Dep
添加訂閱,同時,屬性有變化時,Observer
通知 Dep
,Dep
則通知 Watcher
node
Watcher
獲得通知後,調用回調函數更新視圖git
Compile
則是解析所綁定元素的 DOM
結構,對全部須要綁定的屬性添加 Watcher
訂閱github
由此能夠看出,當屬性發生變化時,是由Observer
-> Dep
-> Watcher
-> update view
,Compile
在最開始解析 DOM
並添加 Watcher
訂閱後就功成身退了。數組
從程序執行的順序來看的話,即 new Vue({})
以後,應該是這樣的:先經過 Observer
劫持全部屬性,而後 Compile
解析 DOM
結構,並添加 Watcher
訂閱,再以後就是屬性變化 -> Observer
-> Dep
-> Watcher
-> update view
,接下來就說說具體的實現。閉包
網上的不少源碼解讀都是從 Observer
開始的,而我會從 new
一個MVVM實例開始,按照程序執行順序去解釋或許更容易理解。先來看一個簡單的例子:app
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> <div class="test"> <p>{{user.name}}</p> <p>{{user.age}}</p> </div> <script type="text/javascript" src="hue.js"></script> <script type="text/javascript"> let vm = new Hue({ el: '.test', data: { user: { name: 'Jack', age: '18' } } }); </script> </body> </html>
接下來都將以其爲例來分析。下面來看一個簡略的 MVVM
的實現,在此將其命名爲 hue
。爲了方便起見,爲 data
屬性設置了一個代理,經過 vm._data
來訪問 data
的屬性顯得麻煩且冗餘,經過代理,能夠很好地解決這個問題,在註釋中也有說明。添加完屬性代理後,調用了一個 observe
函數,這一步作的就是 Observer
的屬性劫持了,這一步具體怎麼實現,暫時先不展開。先記住他爲 data
的屬性添加了 getter
和 setter
。
function Hue(options) { this.$options = options || {}; let data = this._data = this.$options.data, self = this; Object.keys(data).forEach(function(key) { self._proxyData(key); }); observe(data); self.$compile = new Compile(self, options.el || document.body); } // 爲 data 作了一個代理, // 訪問 vm.xxx 會觸發 vm._data[xxx] 的getter,取得 vm._data[xxx] 的值, // 爲 vm.xxx 賦值則會觸發 vm._data[xxx] 的setter Hue.prototype._proxyData = function(key) { let self = this; Object.defineProperty(self, key, { configurable: false, enumerable: true, get: function proxyGetter() { return self._data[key]; }, set: function proxySetter(newVal) { self._data[key] = newVal; } }); };
再往下看,最後一步 new
了一個 Compile
,下面咱們就來說講 Compile
。
new Compile(self, options.el || document.body)
這一行代碼中,第一個參數是當前 Hue
實例,第二個參數是綁定的元素,在上面的示例中爲class爲 .test
的div。
關於 Compile
,這裏只實現最簡單的 textContent
的綁定。而 Compile
的代碼沒什麼難點,很輕易就能讀懂,所作的就是解析 DOM
,並添加 Watcher
訂閱。關於 DOM
的解析,先將根節點 el
轉換成文檔碎片 fragment
進行解析編譯操做,解析完成後,再將 fragment
添加回原來的真實 DOM
節點中。來看看這部分的代碼:
function Compile(vm, el) { this.$vm = vm; this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) { this.$fragment = this.node2Fragment(this.$el); this.init(); this.$el.appendChild(this.$fragment); } } Compile.prototype.node2Fragment = function(el) { let fragment = document.createDocumentFragment(), child; // 也許有同窗不太理解這一步,不妨動手寫個小例子觀察一下他的行爲 while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }; Compile.prototype.init = function() { // 解析 fragment this.compileElement(this.$fragment); };
以上面示例爲例,此時若打印出 fragment
,可觀察到其包含兩個p元素:
<p>{{user.name}}</p> <p>{{user.age}}</p>
下一步就是解析 fragment
,直接看代碼及註釋吧:
Compile.prototype.compileElement = function(el) { let childNodes = Array.from(el.childNodes), self = this; childNodes.forEach(function(node) { let text = node.textContent, reg = /\{\{(.*)\}\}/; // 若爲 textNode 元素,且匹配 reg 正則 // 在上例中會匹配 '{{user.name}}' 及 '{{user.age}}' if (self.isTextNode(node) && reg.test(text)) { // 解析 textContent,RegExp.$1 爲匹配到的內容,在上例中爲 'user.name' 及 'user.age' self.compileText(node, RegExp.$1); } // 遞歸 if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }; Compile.prototype.compileText = function(node, exp) { // this.$vm 即爲 Hue 實例,exp 爲正則匹配到的內容,即 'user.name' 或 'user.age' compileUtil.text(node, this.$vm, exp); }; let compileUtil = { text: function(node, vm, exp) { this.bind(node, vm, exp, 'text'); }, bind: function(node, vm, exp, dir) { // 獲取更新視圖的回調函數 let updaterFn = updater[dir + 'Updater']; // 先調用一次 updaterFn,更新視圖 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); // 添加 Watcher 訂閱 new Watcher(vm, exp, function(value, oldValue) { updaterFn && updaterFn(node, value, oldValue); }); }, // 根據 exp,得到其值,在上例中即 'vm.user.name' 或 'vm.user.age' _getVMVal: function(vm, exp) { let val = vm; exp = exp.trim().split('.'); exp.forEach(function(k) { val = val[k]; }); return val; } }; let updater = { // Watcher 訂閱的回調函數 // 在此即更新 node.textContent,即 update view textUpdater: function(node, value) { node.textContent = typeof value === 'undefined' ? '' : value; } };
正如代碼中所看到的,Compile
在解析到 {{xxx}}
後便添加了 xxx
屬性的訂閱,即 new Watcher(vm, exp, callback)
。理解了這一步後,接下來就須要瞭解怎麼實現相關屬性的訂閱了。先從 Observer
開始談起。
從最簡單的狀況來考慮,即不考慮數組元素的變化。暫時先不考慮 Dep
與 Observer
的聯繫。先看看 Observer
構造函數:
function Observer(data) { this.data = data; this.walk(data); } Observer.prototype.walk = function(data) { const keys = Object.keys(data); // 遍歷 data 的全部屬性 for (let i = 0; i < keys.length; i++) { // 調用 defineReactive 添加 getter 和 setter defineReactive(data, keys[i], data[keys[i]]); } };
接下來經過 Object.defineProperty
方法給全部屬性添加 getter
和 setter
,就達到了咱們的目的。屬性有可能也是對象,所以須要對屬性值進行遞歸調用。
function defineReactive(obj, key, val) { // 對屬性值遞歸,對應屬性值爲對象的狀況 let childObj = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { // 直接返回屬性值 return val; }, set: function(newVal) { if (newVal === val) { return; } // 值發生變化時修改閉包中的 val, // 保證在觸發 getter 時返回正確的值 val = newVal; // 對新賦的值進行遞歸,防止賦的值爲對象的狀況 childObj = observe(newVal); } }); }
最後補充上 observe
函數,也即 Hue
構造函數中調用的 observe
函數:
function observe(val) { // 若 val 是對象且非數組,則 new 一個 Observer 實例,val 做爲參數 // 簡單點說:是對象就繼續。 if (!Array.isArray(val) && typeof val === "object") { return new Observer(val); } }
這樣一來就對 data
的全部子孫屬性(不知有沒有這種說法。。)都進行了「劫持」。顯然到目前爲止,這並沒什麼用,或者說若是隻作到這裏,那麼和什麼都不作沒差異。因而 Dep
上場了。我認爲理解 Dep
與 Observer
和 Watcher
之間的聯繫是最重要的,先來談談 Dep
在 Observer
裏作了什麼。
在每一次 defineReactive
函數被調用以後,都會在閉包中新建一個 Dep
實例,即 let dep = new Dep()
。Dep
提供了一些方法,先來講說 notify
這個方法,它作了什麼事?就是在屬性值發生變化的時候通知 Dep
,那麼咱們的代碼能夠增長以下:
function defineReactive(obj, key, val) { let childObj = observe(val); const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; childObj = observe(newVal); // 發生變更 dep.notify(); } }); }
若是僅考慮 Observer
與 Dep
的聯繫,即有變更時通知 Dep
,那麼這裏就算完了,然而在 vue.js
的源碼中,咱們還能夠看到一段增長在 getter
中的代碼:
// ... get: function() { if (Dep.target) { dep.depend(); } return val; } // ...
這個 depend
方法呢,它又作了啥?答案是爲閉包中的 Dep
實例添加了一個 Watcher
的訂閱,而 Dep.target
又是啥?他實際上是一個 Watcher
實例,???一臉懵逼,先記住就好,先看一部份的 Dep
源碼:
// 標識符,在 Watcher 中有用到,先不用管 let uid = 0; function Dep() { this.id = uid++; this.subs = []; } Dep.prototype.depend = function() { // 這一步至關於作了這麼一件事:this.subs.push(Dep.target) // 即添加了 Watcher 訂閱,addDep 是 Watcher 的方法 Dep.target.addDep(this); }; // 通知更新 Dep.prototype.notify = function() { // this.subs 的每一項都爲一個 Watcher 實例 this.subs.forEach(function(sub) { // update 爲 Watcher 的一個方法,更新視圖 // 沒錯,實際上這個方法最終會調用到 Compile 中的 updaterFn, // 也即 new Watcher(vm, exp, callback) 中的 callback sub.update(); }); }; // 在 Watcher 中調用 Dep.prototype.addSub = function(sub) { this.subs.push(sub); }; // 初始時引用爲空 Dep.target = null;
也許看到這仍是一臉懵逼,不要緊,接着往下。大概有同窗會疑惑,爲何要把添加 Watcher
訂閱放在 getter
中,接下來咱們來講說這 Watcher
和 Dep
的故事。
先讓咱們回顧一下 Compile
作的事,解析 fragment
,而後給相應屬性添加訂閱:new Watcher(vm, exp, cb)
。new
了這個 Watcher
以後,Watcher
怎麼辦呢,就有了下面這樣的對話:
Watcher:hey Dep
,我須要訂閱 exp
屬性的變更。
Dep:這我可作不到,你得去找 exp
屬性中的 dep
,他能作到這件事。
Watcher:但是他在閉包中啊,我沒法和他聯繫。
Dep:你拿到了整個 Hue
實例 vm
,又知道屬性 exp
,你能夠觸發他的 getter
啊,你在 getter
裏動些手腳不就好了。
Watcher:有道理,但是我得讓 dep
知道是我訂閱的啊,否則他通知不到我。
Dep:這個簡單,我幫你,你每次觸發 getter
前,把你的引用告訴 Dep.target
就好了。記得辦完過後給 Dep.target
置空。
因而就有了上面 getter
中的代碼:
// ... get: function() { // 是不是 Watcher 觸發的 if (Dep.target) { // 是就添加進來 dep.depend(); } return val; } // ...
如今再回頭看看 Dep
部分的代碼,是否是好理解些了。如此一來, Watcher
須要作的事情就簡單明瞭了:
function Watcher(vm, exp, cb) { this.$vm = vm; this.cb = cb; this.exp = exp; this.depIds = new Set(); // 返回一個用於獲取相應屬性值的函數 this.getter = parseGetter(exp.trim()); // 調用 get 方法,觸發 getter this.value = this.get(); } Watcher.prototype.get = function() { const vm = this.$vm; // 將 Dep.target 指向當前 Watcher 實例 Dep.target = this; // 觸發 getter let value = this.getter.call(vm, vm); // Dep.target 置空 Dep.target = null; return value; }; Watcher.prototype.addDep = function(dep) { const id = dep.id; if (!this.depIds.has(id)) { // 添加訂閱,至關於 dep.subs.push(this) dep.addSub(this); this.depIds.add(id); } }; function parseGetter(exp) { if (/[^\w.$]/.test(exp)) { return; } let exps = exp.split("."); return function(obj) { for (let i = 0; i < exps.length; i++) { if (!obj) return; obj = obj[exps[i]]; } return obj; }; }
最後還差一部分,即 Dep
通知變化後,Watcher
的處理,具體的函數調用流程是這樣的:dep.notify()
-> sub.update()
,直接上代碼:
Watcher.prototype.update = function() { this.run(); }; Watcher.prototype.run = function() { let value = this.get(); let oldVal = this.value; if (value !== oldVal) { this.value = value; // 調用回調函數更新視圖 this.cb.call(this.$vm, value, oldVal); } };
到這就算寫完了,本人水平有限,如有不足之處歡迎指出,一塊兒探討。