不知不覺接觸前端的時間已通過去半年了,愈來愈發覺對知識的學習不該該只停留在會用的層面,這在我學jQuery的一段時間後便有這樣的體會。html
雖然jQuery只是一個JS的代碼庫,只要會一些JS的基本操做學習一兩天就能很快掌握jQuery的基本語法並熟練使用,可是若是不瞭解jQUery庫背後的實現原理,相信只要你一段時間再也不使用jQuery的話就會把jQuery忘得一乾二淨,這也許就是知其然不知其因此然的後果。前端
最近在學vue的時候又再一次經歷了這樣的困惑,雖然可以比較熟練的掌握vue的基本使用,也可以對MV*模式、數據劫持、雙向數據綁定、數據代理侃上兩句。可是要是稍微深刻一點就有點吃力了。因此這幾天痛下決心研究大量技術文章(起初嘗試看早期源碼,無奈vue與jQuery不是一個層級的,相比於jQuery,vue是真正意義上的前端框架。只能無奈棄坑轉而看技術博客),對vue也算有了一個管中窺豹的認識。最後嘗試實踐一下本身學到的知識,基於數據代理、數據劫持、模板解析、雙向綁定實現了一個小型的vue框架。vue
-------------------------------------------------- 分割線,下面介紹vue的具體實現。node
舒適提示:文章是按照每一個模塊的實現依賴關係來進行分析的,可是在閱讀的時候能夠按照vue的執行順序來分析,這樣對初學者更加的友好。推薦的閱讀順序爲:實現VMVM、數據代理、實現Observe、實現Complie、實現Watcher。git
源碼連接,因爲只實現了v-model,v-on,v-bind等比較小的功能,因此更便於理解和掌握vue的實現過程。若是對您有幫助的話,但願點一下star。github
功能演示以下所示:segmentfault
如下面這個模板爲例,要替換的根元素「#mvvm-app」內只有一個文本節點#text,#text的內容爲{{name}}。咱們就如下面這個模板詳細瞭解一下VUE框架的大致實現流程。數組
<body>
<div id="mvvm-app">
{{name}}
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script> let vm = new MVVM({ el: "#mvvm-app", data: { name: "hello world" }, }) </script> </body>
在vue裏面,咱們將數據寫在data對象中。可是咱們在訪問data裏的數據時,既能夠經過vm.data.name訪問,也能夠經過vm.name訪問。這就是數據代理:在一個對象中,能夠動態的訪問和設置另外一個對象的屬性。緩存
咱們知道靜態綁定(如vm.name = vm.data.name)能夠一次性的將結果賦給變量,而使用Object.defineProperty()方法來綁定則能夠經過set和get函數實現賦值的中間過程,從而實現數據的動態綁定。具體實現以下:前端框架
let obj = {}; let obj1 = { name: 'xiaoyu', age: 18, } //實現origin對象代理target對象 function proxyData(origin,target){ Object.keys(target).forEach(function(key){ Object.defineProperty(origin,key,{//定義origin對象的key屬性 enumerable: false, configurable: true, get: function getter(){ return target[key];//origin[key] = target[key]; }, set: function setter(newValue){ target[key] = newValue; } }) }) }
vue中的數據代理也是經過這種方式來實現的。
function MVVM(options) { this.$options = options || {}; var data = this._data = this.$options.data; var _this = this;//當前實例vm // 數據代理 // 實現 vm._data.xxx -> vm.xxx Object.keys(data).forEach(function(key) { _this._proxyData(key); }); observe(data, this); this.$compile = new Compile(options.el || document.body, this); } MVVM.prototype = { _proxyData: function(key) { var _this = this; if (typeof key == 'object' && !(key instanceof Array)){//這裏只實現了對對象的監聽,沒有實現數組的 this._proxyData(key); } Object.defineProperty(_this, key, { configurable: false, enumerable: true, get: function proxyGetter() { return _this._data[key]; }, set: function proxySetter(newVal) { _this._data[key] = newVal; } }); }, };
要想實現當數據變更時視圖更新,首先要作的就是如何知道數據變更了,能夠經過Object.defineProperty()函數監聽data對象裏的數據,當數據變更了就會觸發set()方法。因此咱們須要實現一個數據監聽器Observe,來對數據對象中的全部屬性進行監聽,當某一屬性數據發生變化時,拿到最新的數據通知綁定了該屬性的訂閱器,訂閱器再執行相應的數據更新回調函數,從而實現視圖的刷新。
當設置this.name = 'hello vue'時,就會執行set函數,通知訂閱器裏的訂閱者執行相應的回調函數,實現數據變更,對應視圖更新。
function observe(data){ if (typeof data != 'object') { return ; } return new Observe(data); } function Observe(data){ this.data = data; this.walk(data); } Observe.prototype = { walk: function(data){ let _this = this; for (key in data) { if (data.hasOwnProperty(key)){ let value = data[key]; if (typeof value == 'object'){ observe(value); } _this.defineReactive(data,key,data[key]); } } }, defineReactive: function(data,key,value){ Object.defineProperty(data,key,{ enumerable: true,//可枚舉 configurable: false,//不能再define get: function(){ console.log('你訪問了' + key);return value; }, set: function(newValue){ console.log('你設置了' + key); if (newValue == value) return; value = newValue; observe(newValue);//監聽新設置的值 } }) } }
要想通知訂閱者,首先得要有一個訂閱器(統一管理全部的訂閱者)。爲了方便管理,咱們會爲每個data對象的屬性都添加一個訂閱器(new Dep)。
訂閱器裏存着的是訂閱者Watcher(後面會講到),因爲訂閱者可能會有多個,咱們須要創建一個數組來維護。一旦數據變化,就會觸發訂閱器的notify()方法,訂閱者就會調用自身的update方法實現視圖更新。
function Dep(){ this.subs = []; } Dep.prototype = { addSub: function(sub){this.subs.push(sub); }, notify: function(){ this.subs.forEach(function(sub) { sub.update(); }) } }
每次響應屬性的set()函數調用的時候,都會觸發訂閱器,因此代碼補充完整。
Observe.prototype = { //省略的代碼未做更改 defineReactive: function(data,key,value){ let dep = new Dep();//建立一個訂閱器,會被閉包在key屬性的get/set函數內,所以每一個屬性對應惟一一個訂閱器dep實例 Object.defineProperty(data,key,{ enumerable: true,//可枚舉 configurable: false,//不能再define get: function(){ console.log('你訪問了' + key); return value; }, set: function(newValue){ console.log('你設置了' + key); if (newValue == value) return; value = newValue; observe(newValue);//監聽新設置的值 dep.notify();//通知全部的訂閱者 } }) } }
compile主要作的事情是解析模板指令,將模板中的data屬性替換成data屬性對應的值(好比將{{name}}替換成data.name值),而後初始化渲染頁面視圖,而且爲每一個data屬性添加一個監聽數據的訂閱者(new Watcher),一旦數據有變更,收到通知,更新視圖。
遍歷解析須要替換的根元素el下的HTML標籤必然會涉及到屢次的DOM節點操做,所以不可避免的會引起頁面的重排或重繪,爲了提升性能和效率,咱們把根元素el下的全部節點轉換爲文檔碎片fragment
進行解析編譯操做,解析完成,再將fragment
添加回原來的真實dom節點中。
Compile解析模板,將模板內的子元素#text添加進文檔碎片節點fragment。
function Compile(el,vm){ this.$vm = vm;//vm爲當前實例 this.$el = document.querySelector(el);//得到要解析的根元素 if (this.$el){ this.$fragment = this.nodeToFragment(this.$el); this.init(); this.$el.appendChild(this.$fragment); } } Compile.prototype = { nodeToFragment: function(el){ let fragment = document.createDocumentFragment(); let child; while (child = el.firstChild){ fragment.appendChild(child);//append至關於剪切的功能 } return fragment; }, };
compileElement方法將遍歷全部節點及其子節點,進行掃描解析編譯,調用對應的指令渲染函數進行數據渲染,並調用對應的指令更新函數進行綁定,詳看代碼及註釋說明:
由於咱們的模板只含有一個文本節點#text,所以compileElement方法執行後會進入_this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'
Compile.prototype = { nodeToFragment: function(el){ let fragment = document.createDocumentFragment(); let child; while (child = el.firstChild){ fragment.appendChild(child);//append至關於剪切的功能 } return fragment; }, init: function(){ this.compileElement(this.$fragment); }, compileElement: function(node){ let childNodes = node.childNodes; const _this = this; let reg = /\{\{(.*)\}\}/g; [].slice.call(childNodes).forEach(function(node){ if (_this.isElementNode(node)){//若是爲元素節點,則進行相應操做 _this.compile(node); } else if (_this.isTextNode(node) && reg.test(node.textContent)){ //若是爲文本節點,而且包含data屬性(如{{name}}),則進行相應操做 _this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name' } if (node.childNodes && node.childNodes.length){ //若是節點內還有子節點,則遞歸繼續解析節點 _this.compileElement(node); } }) }, compileText: function(node,exp){//#text,'name' compileUtil.text(node,this.$vm,exp);//#text,vm,'name' },
};
CompileText()函數實現初始化渲染頁面視圖(將data.name的值經過#text.textContent = data.name顯示在頁面上),而且爲每一個DOM節點添加一個監聽數據的訂閱者(這裏是爲#text節點新增一個Wather)。
let updater = { textUpdater: function(node,value){ node.textContent = typeof value == 'undefined' ? '' : value; }, } let compileUtil = { text: function(node,vm,exp){//#text,vm,'name' this.bind(node,vm,exp,'text'); }, bind: function(node,vm,exp,dir){//#text,vm,'name','text' let updaterFn = updater[dir + 'Updater']; updaterFn && updaterFn(node,this._getVMVal(vm,exp)); new Watcher(vm,exp,function(value){ updaterFn && updaterFn(node,value) }); console.log('加進去了'); } };
如今咱們完成了一個能實現文本節點解析的Compile()函數,接下來咱們實現一個Watcher()函數。
咱們前面講過,Observe()函數實現data對象的屬性劫持,並在屬性值改變時觸發訂閱器的notify()通知訂閱者Watcher,訂閱者就會調用自身的update方法實現視圖更新。
Compile()函數負責解析模板,初始化頁面,而且爲每一個data屬性新增一個監聽數據的訂閱者(new Watcher)。
Watcher訂閱者做爲Observer和Compile之間通訊的橋樑,因此咱們能夠大體知道Watcher的做用是什麼。
主要作的事情是:
先給出所有代碼,再分析具體的功能。
//Watcher function Watcher(vm, exp, cb) { this.vm = vm; this.cb = cb; this.exp = exp; this.value = this.get();//初始化時將本身添加進訂閱器 }; Watcher.prototype = { update: function(){ this.run(); }, run: function(){ const value = this.vm[this.exp]; //console.log('me:'+value); if (value != this.value){ this.value = value; this.cb.call(this.vm,value); } }, get: function() { Dep.target = this; // 緩存本身 var value = this.vm[this.exp] // 訪問本身,執行defineProperty裏的get函數 Dep.target = null; // 釋放本身 return value; } } //這裏列出Observe和Dep,方便理解 Observe.prototype = { defineReactive: function(data,key,value){ let dep = new Dep(); Object.defineProperty(data,key,{ enumerable: true,//可枚舉 configurable: false,//不能再define get: function(){ console.log('你訪問了' + key); //說明這是實例化Watcher時引發的,則添加進訂閱器 if (Dep.target){ //console.log('訪問了Dep.target'); dep.addSub(Dep.target); } return value; }, }) } } Dep.prototype = { addSub: function(sub){this.subs.push(sub); }, }
咱們知道在Observe()函數執行時,咱們爲每一個屬性都添加了一個訂閱器dep,而這個dep被閉包在屬性的get/set函數內。因此,咱們能夠在實例化Watcher時調用this.get()函數訪問data.name屬性,這會觸發defineProperty()函數內的get函數,get
方法執行的時候,就會在屬性的訂閱器dep
添加當前watcher實例,從而在屬性值有變化的時候,watcher實例就能收到更新通知。
那麼Watcher()函數中的get()函數內Dep.taeger = this又有什麼特殊的含義呢?咱們但願的是在實例化Watcher時將相應的Watcher實例添加一次進dep訂閱器便可,而不但願在之後每次訪問data.name屬性時都加入一次dep訂閱器。因此咱們在實例化執行this.get()函數時用Dep.target = this來標識當前Watcher實例,當添加進dep訂閱器後設置Dep.target=null。
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; var _this = this; // 數據代理 // 實現 vm._data.xxx -> vm.xxx Object.keys(data).forEach(function(key) { _this._proxyData(key); }); observe(data, this); this.$compile = new Compile(options.el || document.body, this); }
如下是vue的分析文章,對我理解vue起到很大的幫助。感謝做者對本身知識的分享。