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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2021-05-14 15:57  神奇的小胖子  阅读(7057)  评论(0编辑  收藏  举报