做者:殷榮檜@騰訊javascript
建議你先把倉庫中的代碼clone下來跑一遍,執行git checkout aec6a75 切換到倉庫的第一個commit,本身運行運行,嘗試着去看一看代碼,本身先理解理解,斷點調試調試。而後再來看這篇文章代碼中寫的註釋,遇到不太理解的再來看看這篇文章,看看是否是可以更易於理解些。代碼真的已經簡化到不能再簡化,總計才150行左右,仔細看你必定能理解。第一個commit實現了雙向綁定功能,後面每一個commit都只實現一個完整的細小的功能(如v-model、computed、watch。method等),並且代碼量都儘量的少,你若是想看必定能看懂。html
Vue最精華的部分就是雙向綁定,在此基礎上,又添加了computed,watch, methods等方法。因此要看懂Vue內核,那第一步確定就是要了解Vue雙向綁定的原理,可是說實話,看了網上不少,好多代碼都是通過重構優化後的代碼,失去了代碼原始的面貌,不太易於理解。因此決定寫一個原始一點可是又儘量簡潔一點的,可是原理絕對是Vue雙向綁定的原理,確保你看懂這篇文章,就可以瞭解Vue內核。採用最少的代碼,來實現一個個功能。有什麼寫的不妥的地方,煩請在倉庫issue中指出,我好及時修正。 這個項目的github地址爲build-your-own-vue 歡迎starvue
若是你對當前流行的輪子的原理感興趣,下面這些都是我用盡量少的代碼,儘量易於理解的方式實現的框架的原理,這些你也能夠看看,有疑問歡迎在各個倉庫下留言:java
build-your-own-vuexreact
build-your-own-fluxgithub
接下來所講的這些就爲了實現下面這個簡單的雙向綁定:正則表達式
<div id="app"> {{name}} </div> <script type="text/javascript"> let vue = new Vue({ el: '#app', data: { name: 'jackieyin' } }) window.vue = vue; </script> 複製代碼
在chrome devtools控制檯中經過this.vue.name = 'willen'能夠自動更新頁面中的name爲’willen‘。看看結果:vuex
(1)從最容易的Dependency.js開始說。
先來看代碼:
let Watcher = null; // 用來代表有沒有監視器實例,這會你可能不懂,下面會遇到它,而後講解 class Dep { // 把與一個變量相關的監聽器都存在subs這個變量中 constructor() { this.subs = []; // 定義一個subs容器 } notify() { // 執行全部與變量相關的回調函數,容器中的watcher一個個都執行掉(看不懂watcher不要緊,第二結中就會講解) this.subs.forEach(sub => sub.update()); } addSub(watcher) { // 將一個一個的watcher放入到sub的容器中(看不懂watcher不要緊,第二結中就會講解) // 添加與變量相關的訂閱回調 this.subs.push(watcher); } } 複製代碼
從代碼看下來,Dep就是subs容器,是一個數組,將一個個的watcher都放到subs容器中。watcher就是一個個的回調函數,都放在subs的容器中等待觸發。addSub中的this.subs.push(watcher)就是將一個個的watcher回調函數放入到其中。notify就是用來將subs中的watcher都觸發掉。watcher中就是一個一個更新頁面中對應的變量的函數。這個下面會說到。
(2)接下來就看看這個watcher是什麼?
class Watch { constructor(vue, exp, cb) { this.vue = vue; // 將vue實例傳入到watcher中 this.exp = exp; // 須要對那個表達式進行監控,好比對上例中的'name'進行監控,那麼這裏的exp就是'name' this.cb = cb; // 一但監聽到上述exp表達式子的值發生變化,須要通知到的cb(callback)回調函數 this.hasAddedAsSub = false; // 有沒有被添加到Dep中的Subscriber中去,有的話就不須要重複添加 this.value = this.get(); // 獲得當前vue實例上對應表達式exp的最新的值 } get() { Watcher = this; // 這邊的Watcher爲何須要放入this,並在下面又置空,你須要繼續向下看,暫且先記着,這邊把如今的watcher實例放到了Watcher中了。 var value = this.vue[this.exp]; // 獲得表達式的值,就是獲得'name'表達式的值爲‘willen’(經過chrome devtools控制檯中經過this.vue.name = 'willen'修改了name爲’willen‘。) Watcher = null; // 將Watcher置空,讓給下一個值 return value; // 將獲取到的表達式的值返回出去 } update() { let value = this.get(); // 經過get()函數獲得當前的watcher監聽的表達式的值,例如上面的‘willen’ let oldVal = this.value; // 獲取舊的值 if(value !== oldVal) { // 對比新舊錶達式‘name’的值,發現修改前爲'jackieyin',修改後爲'willen',說明須要更新頁面 this.value = value; // 把如今的值記錄下來,用於和下次比較。 this.cb.call(this.vue, value); // 用如今的值willen去執行回調函數,其實就是更新一下頁面中的{{name}}從‘jackieyin’ 爲‘willen’ } } } 複製代碼
(3) 接下來看一下Observer,這個類是作什麼工做的。
class Observer { constructor(data) { this.defineReactive(data); // 將用戶自定義的data中的元素都進行劫持觀察,從而來實現雙向綁定 } defineReactive(data) { // 開始對用戶定義的數據進行劫持 var dep = new Dep(); //這個就是第一節中說起到的Dependency類。用來收集雙向綁定的各個數據變化時都有的依賴watcher Object.keys(data).forEach(key => { // 遍歷用戶定義的data,其實如今也就一個‘name’字段 var val = data[key]; // 獲得data['name']的值爲jackieyin Object.defineProperty(data, key, { get() { // 使用get對data中的name字段進行劫持 if(Watcher) { // 這個就是第二結中說起的Watcher了,(第二結中Watcher = this賦值後這邊纔會進入if) if(!Watcher.hasAddedAsSub) { // 對於已經添加到訂閱列表中的監視器則無需再重複添加了,防止將watcher重複添加到subs容器中,沒有意義,由於一下子更新{{name}}從‘jackieyin’到‘willen’,更新兩三次也還仍是一個結果 dep.addSub(Watcher); // 將監視器watcher添加到subs訂閱列表中 Watcher.hasAddedAsSub = true; // 代表這個結果已經添加到subs容器中了 } } return val; // 將name中的值返回出去 }, set(newVal) { // 對this.vue.name = 'willen'這個set行爲進行劫持 if(newVal === val) { // 新值(例如仍是this.vue.name = 'jackieyin')與以前的值相同,不作任何修改 return; } val = newVal; // 將vue實例上對應的值(name的值)修改成新的值 dep.notify(); // 通知subs中watcher都觸發來對頁面進行更新,將頁面中的{{name}}處的‘jackieyin’更新爲'willen' } }) }); } } 複製代碼
(4) 最後再一塊兒來看看編譯類Compile,這個是用來對{{name}}進行編譯,說白了就是在你的實例的data對象中,找到name: 'jackieyin',而後在頁面上將{{name}}替換爲‘jackieyin’
class Compile { constructor(el, vue) { this.$vue = vue; // 拷貝vue實例,之因此加$符號,表示暴露給用戶的,常常在Vue中看到這種帶$標誌的,說明是暴露給用戶使用的。 this.$el = document.querySelector(el); // 獲取到dom對象,其實就是document.querySelector('#app'); if(this.$el) { // 若是存在能夠掛在的實例 // 在$fragment中操做,比this.$el中操做節省不少性能,因此要賦值給fragment let $fragment = this.node2Fragment(this.$el); // 將獲取到的el的地方使用片斷替代,這是爲了便於在內存中操做,使得更新頁面更加快速 this.compileText($fragment.childNodes[0]); // 將模板中的{{}}替換成對應的變量,如{{name}}替換爲'jackieyin' this.$el.appendChild($fragment); // 將el獲取到的dom節點使用內存中的片斷進行替換 } } node2Fragment(el) { // 用來把dom中的節點賦值到內存fragment變量中去 // 將node節點都放到fragment中去 var fragment = document.createDocumentFragment(); fragment.appendChild(el.firstChild);// 將el中的元素放到fragment中去,並刪除el中原有的,這個是appendChild自帶的功能 return fragment; } compileText(node) { // 對包含可能出現vue標識的部分進行編譯,主要是將{{xxx}}替換成對應的值,這邊是用正則表達式檢測{{}}進行替換 var reg = /\{\{(.*)\}\}/; // 用來判斷有沒有vue的雙括號的 if(reg.test(node.textContent)) { let matchedName = RegExp.$1; node.textContent = this.$vue[matchedName]; new Watch(this.$vue, matchedName, function(value) { // 對當前的表達式‘name’添加watcher監聽器,其實後來就是把這個watcher放入到了dep中的subs的數組中了。當'name'更新爲‘willen’後,其實就是執行了這邊的node.textContent = value就把頁面中的jackieyin替換成了willen了。這就是雙向綁定了。node其實就是剛纔存放在內存中的$fragement的節點,因此至關於直接操做了內存,因此更新頁面就比修改DOM更新頁面快多了。 node.textContent = value; }); } } } 複製代碼
(5)這個時候就能夠來組裝出一個咱們本身的小型的Vue了。
class Vue { constructor(options) { let data = this._data = options.data || undefined; this._initData(); // 將data中的數據都掛載到this上去,使得this.name 至關於就是獲得了this._data.name new Observer(data); // 將data中的數據進行劫持 new Compile(options.el, this); // 將{{name}}用data中的’jackieyin‘數據替換掉 } _initData() { // 這個函數的功能很簡單,就是把用戶定義在data中的變量,都掛載到Vue實例(this)上 let that = this; Object.keys(that._data).forEach((key) => { Object.defineProperty(that, key, { get: () => { return that._data[key]; }, set: (newVal) => { that._data[key] = newVal; } }) }); } } 複製代碼
(6)大功告成,把咱們所寫的零件組裝在一塊兒試一下咱們的小型的vue是否工做正常。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="app"> {{name}} </div> <script src="./js/Dependency.js"></script> <script src="./js/Observer.js"></script> <script src="./js/Watch.js"></script> <script src="./js/Compile.js"></script> <script src="./js/Vue.js"></script> <script type="text/javascript"> let vue = new Vue({ el: '#app', data: { name: 'jackie' } }) window.vue = vue; </script> </body> </html> 複製代碼
怎麼樣,搞定了,其實,這只是Vue的冰山一角(下圖中的綠色框框的部分),在這個倉庫中還實現了一系列vue的功能,若是你有興趣能夠一個commit一個commit的往上看,每一個commit都只實現一個完整的細小的功能,並且代碼量都儘量的少,你若是想看必定能看懂。這倉庫都是沒有使用虛擬DOM來實現,更新顆粒度細,如今的Vue下降了更新的顆粒度,用了虛擬DOM,可是Vue中雙向綁定的原理始終未變,因此這篇文章仍是須要看懂的,老弟。之後有時間我再研究研究虛擬DOM寫個倉庫。