學習前端也有半年多了,我的的學習慾望還比較強烈,很喜歡那種新知識在本身的演練下一點點實現的過程。最近一直在學vue框架,像網上大佬說的,入門容易深究難。不論是跟着開發文檔學仍是視頻教程,按步驟操做老是最膚淺,想要把這門功課作好畢竟得下足功夫。所以,特地花了好幾天時間閱讀相關技術博客和源碼,簡單實現了一個數據雙向綁定的vue框架,但願能讓各位有點啓發...html
MVVM即modle-view-viewmole,MVVM最先由微軟提出來,在前端頁面中,把Model用純JavaScript對象表示,View負責顯示,二者作到了最大限度的分離。把Model和View關聯起來的就是ViewModel。ViewModel負責把Model的數據同步到View顯示出來,還負責把View的修改同步回Model。前端
在vue框架中,經過控制檯或者Vue Devtools修改data裏的一些屬性時會看到頁面也會更新,而在頁面修改數據時,data裏的屬性值也會發生改變。咱們就把這種model和view同步顯示稱爲是雙向綁定。其實單向綁定原理也差很少,視圖改變data更新經過事件監聽就能輕鬆實現了,重點都在但願data改變視圖也發生改變,而這就是咱們下面要講的原理。vue
首先要知道的是vue的數據綁定經過數據劫持配合發佈訂閱者模式實現的,那麼什麼是數據劫持呢?咱們能夠在控制檯看一下它的初始化對象是什麼樣的:node
let vm = new Vue({ el:"#app", data:{ obj:{ a:1 } }, created() { console.log(this.obj) }, })
能夠看到屬性a分別對應着一個get 和set方法,這裏引伸出Object.defineProperty()方法,傳遞三個參數,obj(要在其上定義屬性的對象)、prop(要定義或修改的屬性的名稱)、descriptor(將被定義或修改的屬性描述符)。該方法更多信息參考:參考更多用法,着重強調一下get和set這兩個屬性描述鍵值。git
日常咱們在打印一個對象屬性時會這樣作: github
var obj = { name:"tnagj" } console.log(obj.name) //tangj
若是咱們想要在輸出的同時監聽obj的屬性值,而且輸出的是tangjSir呢?這時候咱們的set和get屬性就起到了很好的做用數組
var obj ={}; var name = ''; Object.defineProperty(obj,'name',{ set:function(value){ name = value console.log('我叫:' + name) }, get:function(){ console.log(name + 'Sir') } }) obj.name = 'tangj'; //我叫tangj obj.name; //tangjSir
首先咱們定義了一個obj空對象以及name空屬性,再用一個Object.defineProperty()方法來判斷obj.name的訪問狀況,若是是讀值則調用get函數,若是是賦值則調用set函數。在這兩個函數裏面咱們分別對輸出的內容做了更改,所以在get方法調用時打印tangjSir,在set方法調用時打印我叫tangj。緩存
其實這就是vue數據綁定的監聽原理,咱們能經過這個簡單實現MVVM雙向綁定。app
view的變化,好比input值改變咱們很容易就能知道經過input事件反應到data中,數據綁定的關鍵在於怎樣讓data更新view。首先咱們要知道數據何時變的,上文提過能夠用Object.defineProperty()的set屬性描述鍵值來監聽這個變化,當數據改變時就調用set方法。框架
那麼咱們能夠設置一個監聽器Observe,用來監聽全部的屬性,當屬性變化的時候就須要告訴訂閱者Watcher看是否須要更新。由於訂閱者是有不少個,因此咱們須要有一個消息訂閱器Dep來專門收集這些訂閱者,而後在監聽器Observer和訂閱者Watcher之間進行統一管理的。固然咱們還須要有一個指令解析器Compile,對每一個節點元素進行掃描和解析,將相關指令對應初始化成一個訂閱者Watcher,並替換模板數據或者綁定相應的函數,此時當訂閱者Watcher接收到相應屬性的變化,就會執行對應的更新函數,從而更新視圖。因此,咱們大體能夠把整個過程拆分紅五個部分,MVVM.html,MVVM.js,compile.js,observer.js,watcher.js,咱們在MVVM.js中建立所須要的實例,在.html文件中引入這些js文件,這樣拆分更容易理解也更好維護。
爲了和Vue保持一致,咱們向MVVM.js傳入一個空對象options,並讓vm.$el = options.el,vm.$data = options.data,若是能取到vm.$el再進行編譯和監聽
class MVVM { constructor(options){ this.$el = options.el, //把東西掛載在實例上 this.$data = options.data if(this.$el){ // 若是有要編譯的就開始編譯 new Observer(this.$data); //數據劫持,就是把對象的全部屬性改爲get和set方法 new Compile(this.$el,this);//用數據和元素進行編譯 } } }
編譯的時候有一個問題須要注意,若是直接操做DOM元素會特別消耗性能,因此咱們但願先把DOM元素都放在內存中即文檔碎片,待編譯完成再把文檔碎片放進真實的元素中
class Complie{ constructor(el,vm){ this.el = this.isElementNode(el)?el:document.querySelector(el); this.vm = vm; if(this.el){//若是這個元素能獲取到,咱們纔開始編譯 let fragment = this.nodeToFragment(this.el); //1.先把真實的DOM移入到內存中,fragment this.compile(fragment); //2.編譯=>提取想要的元素節點v-modle 和文本節點{{}} this.el.appendChild(fragment) //3.把編譯好的fragment塞回頁面 } nodeToFragment(el){ //須要el元素放到內存中 let fragment = document.createDocumentFragment(); let Child; while(Child = el.firstChild){ fragment.appendChild(Child); } return fragment; } }
}
接下來咱們要判斷須要編譯的是元素節點仍是文檔節點,還記得Vue中有不少頗有用的指令嗎?好比"v-modle"、"v-for"等,因此咱們還要判斷元素節點內是否包含指令,若是是指令,它應該包含一些特殊的方法
/* 省略.... */ isElementNode(node){ //是否是元素節點 return node.nodeType === 1; } isDirective(name){ //是否是指令 return name.includes('v-') } compileElement(node){ //帶v-modle let attrs = node.attributes; Array.from(attrs).forEach( attr =>{ let attrName = attr.name; if(this.isDirective(attrName)){ // 取到對應的值放到節點中 let expr = attr.value; // node vm.$data expr let [,type] = attrName.split('-') //解構賦值 CompileUtil[type](node,this.vm,expr) } } ) } compileText(node){ // 帶{{}} let expr = node.textContent; //取文本的內容 let reg = /\{\{([^}]+)\}\}/g //全局匹配 if(reg.test(expr)){ // node this.vm.$data expr CompileUtil['text'](node,this.vm,expr) } } compile(fragment){ //須要遞歸,拿到的childNodes只是第一層 let childNodes = fragment.childNodes; Array.from(childNodes).forEach( node=>{ if(this.isElementNode(node)){ //是元素節點,還須要遞歸檢查 this.compileElement(node) //編譯元素 this.compile(node) //箭頭函數this指向上一層的實例 }else{ //文本節點 this.compileText(node) //編譯文本 } } ) }
根據獲取的節點類型不一樣,執行不一樣的方法,咱們可把這些方法統一都放到一個對象裏面去
CompileUtil = { getVal(vm,expr){ //獲取實例上的數據 expr = expr.split('.'); //若是遇到vm.$data[a.a],但願先拿到vm.$data[a] return expr.reduce((prev,next)=>{ return prev[next] },vm.$data) }, getTextVal(vm,expr){ //獲取編譯文本之後的結果 return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{ return this.getVal(vm,arguments[1]); }) }, text(node,vm,expr){ // 文本處理 let updateFn = this.updater['textUpdater']; let value = this.getTextVal(vm,expr); updateFn && updateFn(node,value); }, modle(node,vm,expr){ // 輸入框處理 let updateFn = this.updater['modleUpdater'] updateFn && updateFn(node,this.getVal(vm,expr)) }, updater:{ textUpdater(ndoe,value){ ndoe.textContent = value //文本更新 }, modleUpdater(node,value){ node.value = value } } }
編譯的時候咱們還須要一個監聽者,當數據變化調用get和set方法
class Observer{ constructor(data){ this.observer(data) } observer(data){ if(!data || typeof data !== 'object') return; Object.keys(data).forEach(key =>{ this.defineReactive(data,key,data[key]); this.observer(data[key]) }) } defineReactive(obj,key,value){ let that = this; Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get(){ Dep.target && dep.addSub(Dep.target); return value; }, set(newvalue){ if(value === newvalue) return; that.observer(newvalue); //若是新值是對象,繼續劫持 value = newvalue; }, }) } }
前面已經實現了監聽和編譯,可是怎麼樣才能讓它們之間進行通訊呢,也就是當監聽到變化了怎麼通知呢?這裏就用到了發佈訂閱模式。默認觀察者watcher有一個update方法,它會更新數據。Dep裏面建立一個數組,把觀察者都放在這個數組裏面,當監聽到變化,一個個調用監聽者update方法。
// Watcher.js //觀察者的目的就是給須要變化的那個元素增長一個觀察者,當數據變化後執行對應的方法 class Watcher{ constructor(vm,expr,cb){ this.vm = vm; this.expr = expr; this.cb = cb; //先獲取老的值 this.value = this.get() } getVal(vm,expr){ //獲取實例上的數據 expr = expr.split('.'); //若是遇到vm.$data[a.a],但願先拿到vm.$data[a] // console.log(expr) return expr.reduce((prev,next)=>{ return prev[next] },vm.$data) } get(){ Dep.target = this; //緩存本身 let value = this.getVal(this.vm,this.expr); Dep.target = null; //釋放本身 return value; } update(){ let newValue = this.getVal(this.vm,this.expr); let oldValue = this.value; if(newValue != oldValue){ this.cb(newValue); } } } //Dep.js class Dep{ constructor(){ //訂閱的數組 this.subs = [] } addSub(watcher){ this.subs.push(watcher) } notify(){ this.subs.forEach(watcher =>{ watcher.update() }) } }
watcher邏輯: 當建立watcher實例的時候,先拿到這個值,數據變化又拿到一個新值,若是新值和老值不同,那麼調用callback,實現更新;
dep邏輯:建立數組把觀察者放在這個數組裏,當監聽到變化,執行watcher.update()
咱們再它們分別添加到Observer和compile中
// complie.js // 省略.... text(node,vm,expr){ // 文本處理 let updateFn = this.updater['textUpdater']; //{{message.a}} => tangj let value = this.getTextVal(vm,expr); expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{ new Watcher(vm,arguments[1],(newVaule)=>{ // 若是數據變化,文本節點須要從新獲取依賴的屬性更新文本的的內容 updateFn && updateFn(node,this.getTextVal(vm,expr)); }) }) updateFn && updateFn(node,value); }, modle(node,vm,expr){ // 輸入框處理 let updateFn = this.updater['modleUpdater'] // 'message.a' => [message.a] vm.$data['message'].a // 這裏應該加一個監控,數據變化,調用這個watch的cb new Watcher(vm,expr,(newVaule)=>{ //當值變化後將調用cb,將新的值傳遞過來 updateFn && updateFn(node,this.getVal(vm,expr)) }); node.addEventListener('input',(e)=>{ let newVaule = e.target.value; this.setVal(vm,expr,newVaule) }) updateFn && updateFn(node,this.getVal(vm,expr)) } // 省略...
// observer.js
class Observer{ constructor(data){ this.observer(data) } observer(data){ //要對這個data數據原有屬性改爲set和get的形式 if(!data || typeof data !== 'object') return; Object.keys(data).forEach(key =>{ this.defineReactive(data,key,data[key]); this.observer(data[key]) }) } defineReactive(obj,key,value){ let that = this; let dep = new Dep(); //每一個變化的數據都會對應一個數組,這個數據存放了全部數據的更新 Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get(){ Dep.target && dep.addSub(Dep.target); return value; }, set(newvalue){ if(value === newvalue) return; that.observer(newvalue); //若是新值是對象,繼續劫持 value = newvalue; dep.notify(); //通知全部人數據更新 }, }) } } class Dep{ constructor(){ //訂閱的數組 this.subs = [] } addSub(watcher){ this.subs.push(watcher) } notify(){ this.subs.forEach(watcher =>{ watcher.update() }) } }
到這裏咱們就實現了數據的雙向綁定,MVVM做爲數據綁定的入口,整合Observer、Compile和Watcher三者,經過Observer來監聽本身的model數據變化,經過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通訊橋樑,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變動的雙向綁定效果。
固然咱們還須要數據代理,用vm代理vm.$data,也是經過Object.defineProperty()實現
proxyData(data){ Object.keys(data).forEach(key =>{ let val = data[key] Object.defineProperty(this,key,{ enumerable:true, configurable:true, get(){ return val }, set(newval){ if(val == newval){ return; } val = newval } }) }) }
本次學習源碼已上傳github:https://github.com/Tangjj1996/MVVM,喜歡的朋友能夠stars
PS:MVVM是學習框架很是重要的一步,掌握了這些原理才能更好地運用,知其然更要知其因此然,水平有限有錯誤的地方煩請多多指教