mvvm的概念、原理及實現


代碼實現來源於珠峰公開課 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 
作者:慕斯不想說話
鏈接: https://juejin.cn/post/6844904030905303053
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM