• 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
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    Linux下PHP升级的方法
    centos6 授权文件夹所有用户可用
    重置密码遇到ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using passwor:yes)问题
    MySQL Daemon failed to start. 正在启动 mysqld:[失败]
    MySql取消密码强度验证功能
    twbsPagination.js分页插件
    同一个Tomcat部署多个springboot项目问题
    同一个tomcat部署多个项目导致启动失败
    启动Spring boot项目报错:java.lang.IllegalArgumentException: LoggerFactory is not a Logback
    Vue中关于vue-awesome-swiper插件使用以及要注意的 “坑”
  • 原文地址:https://www.cnblogs.com/joyco773/p/14768877.html
Copyright © 2020-2023  润新知