代碼實現來源於珠峰公開課 mvvm 原理的講解。此文在此記錄一下,通過手寫幾遍代碼加深一下自己對 mvvm 理解。
1、MVVM的概念
model-view-viewModel,通過數據劫持+發布訂閱模式來實現。
mvvm是一種設計思想。Model代表數據模型,可以在model中定義數據修改和操作的業務邏輯;view表示ui組件,負責將數據模型轉換為ui展現出來,它做的是數據綁定的聲明、 指令的聲明、 事件綁定的聲明。;而viewModel是一個同步view和model的對象。在mvvm框架中,view和model之間沒有直接的關系,它們是通過viewModel來進行交互的。mvvm不需要手動操作dom,只需要關注業務邏輯就可以了。
mvvm和mvc的區別在於:mvvm是數據驅動的,而MVC是dom驅動的。mvvm的優點在於不用操作大量的dom,不需要關注model和view之間的關系,而MVC需要在model發生改變時,需要手動的去更新view。大量操作dom使頁面渲染性能降低,使加載速度變慢,影響用戶體驗。
2、mvvm的優點
- 1、低耦合性 view 和 model 之間沒有直接的關系,通過 viewModel 來完成數據雙向綁定。
- 2、可復用性 組件是可以復用的。可以把一些數據邏輯放到一個 viewModel 中,讓很多 view 來重用。
- 3、獨立開發 開發人員專注於 viewModel ,設計人員專注於view。
- 4、可測試性 ViewModel 的存在可以幫助開發者更好地編寫測試代碼。
3、mvvm的缺點
- 1、bug很難被調試,因為數據雙向綁定,所以問題可能在 view 中,也可能在 model 中,要定位原始bug的位置比較難,同時view里面的代碼沒法調試,也添加了bug定位的難度。
- 2、一個大的模塊中的 model 可能會很大,長期保存在內存中會影響性能。
- 3、對於大型的圖形應用程序,視圖狀態越多, viewModel 的構建和維護的成本都會比較高。
4、mvvm的雙向綁定原理
mvvm 的核心是數據劫持、數據代理、數據編譯和"發布訂閱模式"。
1、數據劫持——就是給對象屬性添加get,set鈎子函數。
- 1、觀察對象,給對象增加 Object.defineProperty
- 2、vue的特點就是新增不存在的屬性不會給該屬性添加 get 、 set 鈎子函數。
- 3、深度響應。循環遞歸遍歷 data 的屬性,給屬性添加 get , set 鈎子函數。
- 4、每次賦予一個新對象時(即調用 set 鈎子函數時),會給這個新對象進行數據劫持( defineProperty )。
1 //通過set、get鈎子函數進行數據劫持 2 function defineReactive(data){ 3 Object.keys(data).forEach(key=>{ 4 const dep=new Dep(); 5 let val=data[key]; 6 this.observe(val);//深層次的監聽 7 Object.defineProperty(data,key,{ 8 get(){ 9 //添加訂閱者watcher(為每一個數據屬性添加訂閱者,以便實時監聽數據屬性的變化——訂閱) 10 Dep.target&&dep.addSub(Dep.target); 11 //返回初始值 12 return val; 13 },set(newVal){ 14 if(val!==newVal){ 15 val=newVal; 16 //通知訂閱者,數據變化了(發布) 17 dep.notify(); 18 return newVal; 19 } 20 } 21 }) 22 }) 23 }
2、數據代理
將 data , methods , compted
上的數據掛載到vm
實例上。讓我們不用每次獲取數據時,都通過 mvvm._data.a.b 這種方式,而可以直接通過 mvvm.b.a 來獲取。
1 class MVVM{ 2 constructor(options){ 3 this.$options=options; 4 this.$data=options.data; 5 this.$el=options.el; 6 this.$computed=options.computed; 7 this.$methods=options.methods; 8 //劫持數據,監聽數據的變化 9 new Observer(this.$data); 10 //將數據掛載到vm實例上 11 this._proxy(this.$data); 12 //將方法也掛載到vm上 13 this._proxy(this.$methods); 14 //將數據屬性掛載到vm實例上 15 Object.keys(this.$computed).forEach(key=>{ 16 Object.defineProperty(this,key,{ 17 get(){ 18 return this.$computed[key].call(this);//將vm傳入computed中 19 } 20 }) 21 }) 22 //編譯數據 23 new Compile(this.$el,this) 24 }; 25 //私有方法,用於數據劫持 26 _proxy(data){ 27 Object.keys(data).forEach(key=>{ 28 Object.defineProperty(this,key,{ 29 get(){ 30 return data[key] 31 } 32 }) 33 }) 34 35 } 36 }
3、數據編譯
把 {{}} , v-model , v-html , v-on
,里面的對應的變量用data里面的數據進行替換。
1 class Compile{ 2 constructor(el,vm){ 3 this.el=this.isElementNode(el)?el:document.querySelector(el); 4 this.vm=vm; 5 let fragment=this.nodeToFragment(this.el); 6 //編譯節點 7 this.compile(fragment); 8 //將編譯后的代碼添加到頁面 9 this.el.appendChild(fragment); 10 }; 11 //核心編譯方法 12 compile(node){ 13 const childNodes=node.childNodes; 14 [...childNodes].forEach(child=>{ 15 if(this.isElementNode(child)){ 16 this.compileElementNode(child); 17 //如果是元素節點就還得遞歸編譯 18 this.compile(child); 19 }else{ 20 this.compileTextNode(child); 21 } 22 }) 23 24 }; 25 //編譯元素節點 26 compileElementNode(node){ 27 const attrs=node.attributes; 28 [...attrs].forEach(attr=>{ 29 //attr是一個對象 30 let {name,value:expr}=attr; 31 if(this.isDirective(name)){ 32 //只考慮到v-html和v-model的情況 33 let [,directive]=name.split("-"); 34 //考慮v-on:click的情況 35 let [directiveName,eventName]=directive.split(":"); 36 //調用不同的指令來進行編譯 37 CompileUtil[directiveName](node,this.vm,expr,eventName); 38 } 39 }) 40 }; 41 //編譯文本節點 42 compileTextNode(node){ 43 const textContent=node.textContent; 44 if(/\{\{(.+?)\}\}/.test(textContent)){ 45 CompileUtil["text"](node,this.vm,textContent) 46 } 47 }; 48 //將元素節點轉化為文檔碎片 49 nodeToFragment(node){ 50 //將元素節點緩存起來,統一編譯完后再拿出來進行替換 51 let fragment=document.createDocumentFragment(); 52 let firstChild; 53 while(firstChild=node.firstChild){ 54 fragment.appendChild(firstChild); 55 } 56 return fragment; 57 }; 58 //判斷是否是元素節點 59 isElementNode(node){ 60 return node.nodeType===1; 61 }; 62 //判斷是否是指令 63 isDirective(attr){ 64 return attr.includes("v-"); 65 } 66 } 67 //存放編譯方法的對象 68 CompileUtil={ 69 //根據data中的屬性獲取值,觸發觀察者的get鈎子 70 getVal(vm,expr){ 71 const data= expr.split(".").reduce((initData,curProp)=>{ 72 //會觸發觀察者的get鈎子 73 return initData[curProp]; 74 },vm) 75 return data; 76 }, 77 //觸發觀察者的set鈎子 78 setVal(vm,expr,value){ 79 expr.split(".").reduce((initData,curProp,index,arr)=>{ 80 if(index===arr.length-1){ 81 initData[curProp]=value; 82 return; 83 } 84 return initData[curProp]; 85 },vm) 86 }, 87 getContentValue(vm,expr){ 88 const data= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 89 return this.getVal(vm,args[1]); 90 }); 91 return data; 92 }, 93 model(node,vm,expr){ 94 const value=this.getVal(vm,expr); 95 const fn=this.updater["modelUpdater"]; 96 fn(node,value); 97 //監聽input的輸入事件,實現數據響應式 98 node.addEventListener('input',e=>{ 99 const value=e.target.value; 100 this.setVal(vm,expr,value); 101 }) 102 //觀察數據(expr)的變化,並將watcher添加到訂閱者隊列中 103 new Watcher(vm,expr,newVal=>{ 104 fn(node,newVal); 105 }); 106 }, 107 text(node,vm,expr){ 108 const fn=this.updater["textUpdater"]; 109 //將{{person.name}}中的person.james替換成james 110 const content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 111 //觀察數據的變化 112 new Watcher(vm,args[1],()=>{ 113 // this.getContentValue(vm,expr)獲取textContent被編譯后的值 114 fn(node,this.getContentValue(vm,expr)) 115 116 }) 117 return this.getVal(vm,args[1]); 118 }) 119 fn(node,content); 120 }, 121 html(node,vm,expr){ 122 const value=this.getVal(vm,expr); 123 const fn=this.updater["htmlUpdater"]; 124 fn(node,value); 125 new Watcher(vm,expr,newVal=>{ 126 //數據改變后,再次替換數據 127 fn(node,newVal); 128 }) 129 }, 130 on(node,vm,expr,eventName){ 131 node.addEventListener(eventName,e=>{ 132 //調用call將vm實例(this)傳到方法中去 133 vm[expr].call(vm,e); 134 }) 135 }, 136 updater:{ 137 modelUpdater(node,value){ 138 node.value=value 139 }, 140 htmlUpdater(node,value){ 141 node.innerHTML=value; 142 }, 143 textUpdater(node,value){ 144 145 node.textContent=value; 146 } 147 } 148 }
4、發布訂閱
發布訂閱主要靠的是數組關系,訂閱就是放入函數(就是將訂閱者添加到訂閱隊列中),發布就是讓數組里的函數執行(在數據發生改變的時候,通知訂閱者執行相應的操作)。消息的發布和訂閱是在觀察者的數據綁定中進行數據的——在get鈎子函數被調用時進行數據的訂閱(在數據編譯時通過 new Watcher() 來對數據進行訂閱
),在set鈎子函數被調用時進行數據的發布。
1 //消息管理者(發布者),在數據發生變化時,通知訂閱者執行相應的操作 2 class Dep{ 3 constructor(){ 4 this.subs=[]; 5 }; 6 //訂閱 7 addSub(watcher){ 8 this.subs.push(watcher); 9 }; 10 //發布 11 notify(){ 12 this.subs.forEach(watcher=>watcher.update()); 13 } 14 } 15 //訂閱者,主要是觀察數據的變化 16 class Watcher{ 17 constructor(vm,expr,cb){ 18 this.vm=vm; 19 this.expr=expr; 20 this.cb=cb; 21 this.oldValue=this.get(); 22 }; 23 get(){ 24 Dep.target=this; 25 const value=CompileUtil.getVal(this.vm,this.expr); 26 Dep.target=null; 27 return value; 28 }; 29 update(){ 30 const newVal=CompileUtil.getVal(this.vm,this.expr); 31 if(this.oldValue!==newVal){ 32 this.cb(newVal); 33 } 34 } 35 } 36 //觀察者 37 class Observer{ 38 constructor(data){ 39 this.observe(data); 40 }; 41 //使數據可響應 42 observe(data){ 43 if(data&&typeof data==="object"){ 44 this.defineReactive(data) 45 } 46 }; 47 defineReactive(data){ 48 Object.keys(data).forEach(key=>{ 49 const dep=new Dep(); 50 let val=data[key]; 51 this.observe(val);//深層次的監聽 52 Object.defineProperty(data,key,{ 53 get(){ 54 //添加訂閱者watcher(為每一個數據屬性添加訂閱者,以便實時監聽數據屬性的變化——訂閱) 55 Dep.target&&dep.addSub(Dep.target); 56 //返回初始值 57 return val; 58 },set(newVal){ 59 if(val!==newVal){ 60 val=newVal; 61 //通知訂閱者,數據變化了(發布) 62 dep.notify(); 63 return newVal; 64 } 65 } 66 }) 67 }) 68 } 69 } 70