MVVM——Model-View-ViewModle的縮寫,MVC設計模式的改進版。Model是咱們應用中的數據模型,View是咱們的UI層,經過ViewModle,能夠把咱們Modle中的數據映射到View視圖上,同時,在View層修改了一些數據,也會反應更新咱們的Modle。javascript
上面的話,未免太官方了。簡單理解就是雙向數據綁定,即當數據發生變化的時候,視圖也就發生變化,當視圖發生變化的時候,數據也會跟着同步變化。html
MVVM這種思想的前端框架其實老早就有了,我記得是在13年,本身在公司的主要工做是作後臺管理系統的UI設計和開發,當時就思考,如何讓那些專一後臺的開發,既簡單又方便的使用前端開發的一些組件。當時有三種方案:前端
後來的評估是:vue
當時本身仍是比較推崇Angular的,我記得後來還買了一本《基於MVC的Javascript Web富應用開發》專門去了解這種模式在工做中可能用的狀況,以及實現它的一些基本思路。java
當時熱點比較高的MVVM框架有:node
當年的環境和條件都沒有如今好,不管從技術完善的狀況,仍是工做的實際狀況上面看,都是如此——那時候先後端分離都是理想。nginx
固然如今環境好了,各類框架的出現也極大方便了咱們,提升了咱們開發的工做效率。時代老是在進步,大浪淘沙,MVVM的框架如今比較熱門和流行的,我相信你們如今都知道,就是下面三種了:正則表達式
如今Angular除了一些忠實的擁躉,基本上也就沒落了。Angular不管從入門仍是實際應用方面,都要比其餘兩個框架發費的時間成本更大。
Angular如今有種英雄末路的感受,但不能不認可,以前它確實散發了光芒。segmentfault
Angular的1.x版本,是經過髒值檢測來實現雙向綁定的。後端
而最新的Angular版本和Vue,以及React都是經過數據劫持+發佈訂閱模式來實現的。
髒值檢測
簡單理解就是,把老數據和新數據進行比較,髒就表示以前存在過,有過痕跡,經過比較新舊數據,來判斷是否要更新。感興趣的能夠看看這篇文章 構建本身的AngularJS,第一部分:做用域和digest。
數據劫持 發佈訂閱
數據劫持:在訪問或者修改對象的某個屬性時,經過代碼攔截這個行爲,進行額外的操做或者修改返回結果。在ES5當中新增了Object.defineProperty()能夠幫咱們實現這個功能。
發佈訂閱:如今每一個人應該都用微信吧,一我的能夠關注多個公衆號,多我的能夠同時關注相同的公衆號。關注的動做就至關於訂閱。公衆號每週都會更新內容,並推送給咱們,把寫好的文章在微信管理平臺更新就行了,點擊推送,就至關於發佈。更詳細的能夠深刻閱讀 javascript設計模式——發佈訂閱模式
咱們靜下心好好思考下,若是才能實現雙向數據綁定的功能。可能須要:
經過上面這樣的思考,咱們能夠簡單的寫一下大概的方法。
class MVVM { constructor(data){ this.$option = option; const data = this._data = this.$option.data; //數據劫持 observe(data) //數據代理 proxyData(data) //編譯模板 const dom = this._el = this.$option.el; complie(dom,this); //發佈訂閱 //鏈接視圖和數據 //實現雙向數據綁定 } } // Observe類 function Observe(){} // Observe實例化函數 function observe(data){ return new Observe(data); } // Compile類 function Compile(){} // Compile實例化函數 function compile(el){ return new Compile(el) }
咱們有下面這樣一個對象
let obj = { name:"mc", age:"29", friends:{ name:"hanghang", name:"jiejie" } }
咱們要對這個對象執行某些操做(讀取,修改),一般像下面就能夠
// 取值 const name = obj.name; console.log(obj.age) const friends = obj.friends; // 修改 obj.name = "mmcai"; obj.age = 30;
在VUE中,咱們知道,若是data對象中的某個屬性,在template當中綁定的話,當咱們修改了這個屬性值,咱們的視圖也就更新了。這就是雙向數據綁定,數據變化,視圖更新,同時反過來也同樣。
要實現這個功能,咱們就須要知道data當中的數據是如何變更了,ES5當中提供了Object.defineProperty()函數,咱們能夠經過這個函數對咱們data對象當中的數據進行監聽。當數據變更,就會觸發這個函數裏面的set方法,經過判斷數據是否變化,就能夠執行一些方法,更新咱們的視圖了。因此咱們如今須要實現一個數據監聽器Observe,來對咱們data中的全部屬性進行監聽。
// Observe類的實例化函數 function observe(data){ // 判斷數據是不是一個對象 if(typeof data !== 'object'){ return; } // 返回一個Observe的實例化對象 return new Observe(data) } // Observer類的實現 class Observe{ constructor(data){ this.data = data; this.init(data) } init(data){ for(let k in data){ let val = data[k]; //若是data是一個對象,咱們遞歸調用自身 if(typeof val === 'object'){ observe(val); } Object.defineProperty(data,k,{ enumerable:true, get(){ return val; }, set(newVal){ //若是值相同,直接返回 if(newVal === val){ return; }; //賦值 val = newVal; //若是新設置的值是一個對象,遞歸調用observe方法,給新數據也添加上監聽 if(typeof newVal === 'object'){ observe(newVal); } } }) } } }
瞭解了數據劫持,咱們就能夠明白,爲何咱們實例化vue的時候,必須事先在data當中定義好咱們的須要的屬性了,由於咱們新增的屬性,沒有通過observe進行監聽,沒有經過observe監聽,後面complie(模板解析)也就不會執行。
因此,雖然你能夠在data上面設置新的屬性,並讀取,但視圖卻不能更新。
咱們常見的代理有nginx,就是咱們不直接去訪問(操做)咱們實際要訪問的數據,而是經過訪問一個代理,而後代理幫咱們去拿咱們真正須要的數據。
通常的特色是:
下面是VUE簡單的一個使用實例:
cosnt vm = new Vue({ el:"#app", data:{ name:"mmcai" } });
咱們的實例化對象vm,想要讀取data裏面的數據的時候,不作任何處理的正常狀況下,使用下面方式讀取:
const name = vm.data.name;
這樣操做起來,顯然麻煩了一些,咱們就能夠經過數據代理,直接把data綁定到咱們的實例上,因此在vue當中,咱們通常獲取數據像下面同樣:
cosnt vm = new Vue({ el:"#app", data:{ name:"mmcai" }, created(){ // 直接經過實例就能夠訪問到data當中的數據 const name = this.name; // 經過this.data.name 也能夠訪問,可是顯然,麻煩了一些 } });
一樣,咱們經過Object.defineProperty函數,把data對象中的數據,綁定到咱們的實例上就能夠了,代碼以下:
class MVVM { constructor(option){ //此處代碼省略 this.$option = option; const data = this._data = this.$option.data; //調用代理 this._proxyData(data); } _proxyData(data){ const that = this; for(let k in data){ let val = data[k]; Object.defineProperty(that,k,{ enumerable:true, get(){ return that._data[k]; }, set(newVal){ that._data[k] = newVal; } }) } } }
利用正則表達式識別模板標識符,並利用數據替換其中的標識符。
VUE裏面的標識符是 {{}} 雙大括號,數據就是咱們定義在data上面的內容。
實現原理
遍歷解析須要替換的根元素el下的HTML標籤,必定會使用遍歷對DOM節點進行操做,對DOM操做就會引起頁面的重排和重繪,爲了提升性能和效率,能夠把el根節點下的全部節點替換爲文檔碎片fragment進行解析編譯操做,解析完成,再將fragment添加到根節點el中
若是想對文檔碎片進行,更多的瞭解,能夠查看文章底部的參考資料
<!--定義模板編譯類--> class Complie{ constructor(el,vm){ this.$vm = vm; this.$el = document.querySelector(el); //第一步,把DOM轉換成文檔碎片 this.$fragment = this.nodeToFragment(this.$el); //第二步,匹配標識符,填充數據 this.compileElement(this.$fragment); //把文檔碎片,添加到el根節點上面 this.$el.appendChild(this.$fragment); } // 把DOM節點轉換成文檔碎片 nodeToFragment(el){ let nodeFragment = document.createDocumentFragment(); // 循環遍歷el下面的節點,填充到文檔碎片nodeFragment中 while(child = el.firstChild){ nodeFragment.appendChild(child); } // 把文檔碎片返回 return nodeFragment; } // 遍歷目標,查找標識符,並替換 compileElement(node){ let reg = /\{\{(.*)\}\}/; Array.from(node.childNodes).forEach((node)=>{ let text = node.textContent; if(node.nodeType === 3 && reg.test(text)){ let arr = RegExp.$1.split('.'); // vm 是實例的整個data對象 let val = vm; arr.forEach((k)=>{ val = val[k] }) node.textContent = text.replace(/\{\{(.*)\}\}/,val); } // 若是節點包含字節的,遞歸調用自身 if(node.childNodes){ this.compileElement(node) } }) } } <!--實例化的方法--> const complie = (el,vm)=>{ return new Compile(el,vm) }
在軟件架構中,發佈訂閱是一種消息範式,消息的發送者(成爲發佈者)不會將消息直接發送給特定的接收者(成爲訂閱者)。二十將發佈的消息分爲不一樣的類別,無需瞭解哪些訂閱者是否存在。一樣的,訂閱者能夠表達對一個或多個類別的興趣,直接受感興趣的消息,無需瞭解哪些發佈者是否存在——維基。
上述的表達中,既然說發佈者不關心訂閱者,訂閱者也不關心發佈者,那麼他們是如何通訊呢?
其實就是經過第三方,一般在函數中咱們,稱他們爲觀察者watcher
在VUE的裏面,咱們要確認幾個概念,誰是發佈者,誰是訂閱者,爲何須要發佈訂閱?
上面咱們說了數據劫持Observe,也說了Compile,其實,Observe和Compile 他們即便發佈者,也是訂閱者,幫助他們之間的通信,就是watcher的工做。
經過下面的代碼,咱們簡單瞭解下,發佈訂閱模式的實現狀況。
// 建立一個類 // 發佈訂閱,本質上是維護一個函數的數組列表,訂閱就是放入函數,發佈就是讓函數執行 class Dep{ consturctor(){ this.subs=[]; } // 添加訂閱者 addSub(sub){ this.subs.push(sub); } // 通知訂閱者 notify(){ // 訂閱者,都有 this.subs.forEach((sub=>sub.update()); } } // 監聽函數,watcher // 經過Watcher類建立的實例,都有update方法 class Watcher{ // watcher的實例,都須要傳入一個函數 constructor(fn){ this.fn = fn; } // watcher的實例,都擁有update方法 update(){ this.fn(); } } // 把函數做爲參數傳入,實例化一個watcher const watcher = new Watcher(()=>{ consoole.log('1') }); // 實例化Dep 類 const dep = new Dep(); // 將watcher放到dep維護的數組中,watcher實例自己具備update方法 // 能夠理解成函數的訂閱 dep.addSub(watcher); // 執行,能夠理解成,函數的發佈, // 不關心,addSub方法訂閱了誰,只要訂閱了,就經過遍歷循環subs數組,執行數組每一項的update dep.notify();
經過以上代碼的瞭解,咱們繼續實現咱們MVVM中的代碼,實現數據和視圖的關聯。
這種關聯的結果就是,當咱們修改data中的數據的時候,咱們的視圖更新。或者咱們視圖中修改了相關內容,咱們的data也進行相關的更新,因此這裏主要的邏輯代碼,就是咱們watcher當中的update方法。
咱們根據上面的內容,對咱們的Observe和Compile以及Watcher進行修改,代碼以下:
class MVVM{ constructor(option){ this.$option = option; const data = this._data = this.$option.data; this.$el = this.$option.el; // 數據劫持 this._observe(data); // 數據代理 this._proxyData(data); //模板解析 this._compile(this.$el,this) } // 數據代理 _proxyData(data){ for(let k in data){ let val = data[k]; Object.defineProperty(this,k,{ enumerable:true, get(){ return this._data[k]; }, set(newVal){ this._data[k] = newVal; } }) } } } // 數據劫持 class Observe{ constructor(data){ this.init(data); } init(data){ let dep = new Dep(); for(let k in data){ let val = data[k]; // val 多是一個對象,遞歸調用 if(typeof val === 'object'){ observe(val); } Object.defineProperty(data,k,{ enumerable:true, get(){ // 訂閱, // Dep.target 是Watcher的實例 Dep.target && dep.addSub(Dep.target); return val; }, set(newVal){ if(newVal === val){ return; } val = newVal; observe(newVal); dep.notify(); } }) } } } // 數據劫持實例 function observe(data){ if(typeof data !== 'object'){ return }; return new Observe(data); } // 模板編譯 class Compile{ constructor(el,vm){ vm.$el = document.querySelector(el); //1.把DOM節點,轉換成文檔碎片 const Fragment = this.nodeToFragment(vm.$el) //2.經過正則匹配,填充數據 this.replace(Fragment,vm); //3.把填充過數據的文檔碎片,插入模板根節點 vm.$el.appendChild(Fragment); } // DOM節點轉換 nodeToFragment(el){ // 建立文檔碎片, const fragment = document.createDocumentFragment(); //遍歷DOM節點,把DOM節點,添加到文檔碎片上 while(child ===el.firstChild){ fragment.appendChild(child); } // 返回文檔碎片 return fragment; } //匹配標識,填充數據 replace(fragment,vm){ // 使用Array.from方法,把DOM節點,轉化成數據,進行循環遍歷 Array.from(fragment.childNodes).forEach((node)=>{ // 遍歷節點,拿到每一個內容節點 let text = node.textContent; // 定義標識符的正則 let reg = /\{\{(.*)\}\}/; //若是節點是文本,且節點的內容當中匹配到了模板標識符 // 數據渲染視圖 if(node.nodeType===3 && reg.test(text)){ // 用數據替換標識符 let arr = RegExp.$1.split('.'); let val = vm; arr.forEach((item)=>{ val = val[item]; }) // 添加一個watcher,當咱們的數據發生變化的時候,更新咱們的view new Watcher(vm,RegExp.$1,(newVal)=>{ node.textContent = text.replace(reg,newVal); }) //把數據填充到節點上 node.textContent = text.replace(reg,val); } // 視圖更新數據 if(node.nodeType === 1){ let nodeAttrs = node.attributes; Array.from(nodeAttrs).forEach((attr)=>{ let name = attr.name; // 獲取標識符的內容,也就是v-mode="a"的內容 let exp = attr.value; if(name.indexOf('v-model')===0){ node.value = vm[exp]; }; new Watcher(vm,exp,(newVal)=>{ node.value = newVal; }); node.addEventListener('input',function(e){ let newVal = e.target.value; vm[exp] = newVal; }); }); } // 若是節點包含子節點,遞歸調用自身 if(node.childNodes){ this.replace(node,vm); } }) } } // 模板編譯實例 function compile(el,vm){ return new Compile(el,vm) } // 發佈訂閱 class Dep{ constructor(){ this.subs = []; } // 訂閱函數 addSub(fn){ this.subs.push(fn); } // 發佈執行函數 notify(){ this.subs.forEach((fn)=>{ fn(); }) } } // Dep實例 function dep(){ return new Dep(); } // 觀察者 class Watcher{ // vm,咱們的實例 // exp,咱們的標識符 // fn,回調 constructor(vm,exp,fn){ this.fn = fn; this.vm = vm; this.exp = exp; Dep.target = this; let val = vm; let arr = exp.split('.'); arr.forEach((k)=>{ val = val[k] }); // 完成以後,咱們把target 刪除; Dep.target = null; } update(){ let val = this.vm; let arr = this.exp.split('.'); arr.forEach((k)=>{ val = val[k]; }) this.fn(); } } function watcher(){ return new Watcher() }
Wathcer幹了那些好事:
Watcher鏈接了兩個部分,包括Observe和Compile;
在Observe方法執行的時候,咱們給data的每一個屬性都添加了一個dep,這個dep被閉包在get/set函數內。
當咱們new Watcher,在以後訪問data當中屬性的時候,就會觸發經過Object.defineProperty()函數當中的get方法。
get方法的調用,就會在屬性的訂閱器實例dep中,添加當前Watcher的實例。
當咱們嘗試修改data屬性的時候,就會出發dep.notify()方法,該方法會調用每一個Watcher實例的update方法,從而更新咱們的視圖。
回顧下整個MVVM實現的整個過程
我這裏有一個簡短的視頻,是某培訓機構講解MVVM的內容,你們有興趣,能夠自取。
提取碼:1i0r
若是失效,能夠私聊我。