源码系列之深入理解MVVM原理与响应式原理

109 阅读4分钟

12730.jpg

大家好我是某行人👋,充电器一拔又是一年的开始,还记的去年年终总结立的FLag,今年从源码出发,作为Vue的喜爱者,理解原理是必不可少的,经过自己几天的研究与总结终于总结出一篇文章,在这里分享给大家。

  • 什么是MVVM?

作为老生常谈的问题在面试的时候也会经常问到,那什么是MVVM呢?MVVM是Model-View-ViewModel的简写,MVVM就是模型-视图-视图模型,M是逻辑方法加上数据,V就是用户看到的界面,VM就是逻辑方法加上界面渲染的代码,双向数据绑定作为MVVM核心,View的变动,自动反映在 ViewModel,数据驱动视图更新的功能。其实MVVM它本质上就是MVC 的改进版。MVC全名Model View Controller,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。其中,View的定义比较清晰,就是用户界面

  • MVVM与MVC的区别?

MVVM 实现了双向数据绑定,而MVC是单项

MVVM是真正将页面与数据逻辑分离放到js里去实现,而MVC里面未分离

  • 实现MVVM方式?
    • 发布者-订阅者模式:一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是vm.set(propertyName, value)
    • 脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,在指定的事件触发时进入脏值检测
    • 数据劫持:Vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持每个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调
  • 响应式原理

Vue2.x是通过Object.defineProperty()实现的,Vue3.x是通过ES6中Proxy实现的,下面简单实现以下Vue2.x的响应式主要为下面MVVM的实现做铺垫。

<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue双向数据绑定</title>
</head>
<body>
<input type="text" name="" id="in_id">
<p id="p_id"></p>
<!-- 实现核心是通过Object.defineProperty对data的每个属性进行劫持 -->
<script>
    var objs = {};
    var inp_dom = document.querySelector('#in_id');
    var p_id = document.querySelector('#p_id')
    Object.defineProperty(objs,'xx_objs',{
        get:function(e){
            
            return e;
        },
        set:function(e){
            p_id.innerHTML=e;
        }
    })
    inp_dom.addEventListener('input',(e)=>{
    objs.xx_objs=inp_dom.value
    }) 
    
</script>
<!-- 观察这模式:一对多的模式 -->
</body>
</html>

关于MVVM与响应式就介绍这么多,接下来我们直接从源码出发实现一个简易的MVVM

  1. 创建文件
| Vue-Mvvm
   | Compiler.js // 模板编译
   | Oberser.js  // 数据劫持
   | Watcher.js  // 观察者对象 
   | Mvvm.js     // vue实例
   | index.html  // 使用文件

  1. 模板编译Compiler.js
// 模板编译
class Compiler{
    constructor(el,vm){
         this.el = this.isElementNode(el)?el:document.querySelector(el);
         this.vm = vm;
       
         //    把当前节点中的元素获取到放到内存中
         let fragment = this.node2fragment(this.el);
      // 把节点中的内容进行替换
      // 模板编译
      this.complie(fragment);
      // 把内容再塞到页面中
      this.el.appendChild(fragment);

    }
  // 编译元素 
    complieElement(node){
          let attrs = node.attributes;
          let _me = this;
         [...attrs].forEach(attr=>{
              let {name,value:expr} = attr;
              if(_me.isDirective(name)){ 
                  let [,directive] = name.split('-');
                  let [directiveName,eventName] = directive.split(':');
                  // 调用不同的指令方法处理
                  
                  CompilerUtil[directiveName](node,expr,_me.vm,eventName);
              }else if(this.isEvent(name)){
                  let eventType = name.replace(/^\@/g,'');
                
                  CompilerUtil['on'](node,expr,_me.vm,eventType)
              }
              
         })          
    }
  // 编译文本 
    complieText(node){
        //当前文本是否包含{{ xxx }}
         
        let content = node.textContent;
          if(/\{\{(.+?)\}\}/.test(content)){
              CompilerUtil['text'](node,content.trim(),this.vm)
          }
    } 
  // 核心编译方法
    complie(node){
        let childNodes = node.childNodes;
        [...childNodes].forEach(child=>{
             if(this.isElementNode(child)){
                  this.complieElement(child);
                  // 递归节点
                  this.complie(child);
             }else if(this.isTextNode(child)){
                 
              this.complieText(child)
             }
        })  
    }
  //   拿到碎片节点- 把节点移动到内存中
    node2fragment(node){
          //  创建文档碎片
          let fragment = document.createDocumentFragment();
          let firstChild;
          while(firstChild = node.firstChild){
              // appendChild具有移动性
              fragment.appendChild(firstChild)
              
          }
          return fragment;
    }
  //   判断el是否是一个元素
  isElementNode(node){
        return node.nodeType===1;
  }
  //  判断是否为指令
  isDirective(attrName){
       return attrName.startsWith('v-');
  }  
  //  判断文本   
  isTextNode(node){
      return node.nodeType == 3;
  } 
  // 判断@绑定的事件   
  isEvent(dir){
      return dir.startsWith('@')
  }
}


// 模板处理工具函数

const CompilerUtil={
    // 根据表达式取到对应的数据
    getVal(vm,expr){
        
        if(/\./g.test(expr)){
            return expr.split('.').reduce((data,current)=>{
                return data[current]
            },vm.$data); 
        }else{
            return vm.$data[expr];
        }
    },
    setVal(vm,expr,value){  //   data 新数组 current key值 index 索引 arr 当前数组
          return expr.split('.').reduce((data,current,index,arr)=>{
               if(index == arr.length-1){
                    data[current] = value;
               }
               return data[current]
          },vm.$data)
    },
    // 事件绑定
    on(node,expr,vm,eventType){
           let fn = vm.$options.methods && vm.$options.methods[expr];
           if(eventType && fn){
               node.addEventListener(eventType, fn.bind(vm), false);
           }
    },
    model(node,expr,vm){
        let fn = this.updater['modelUpdate'];
        let value = this.getVal(vm,expr);
     
        // 元素处理加观察者  每一个元素都添加成观察者模式


        // 每个元素添加观察者都是通过模板编译阶段添加的
        new Watcher(vm,expr,(newVal)=>{ // 给输入框加入观察者模式
            fn(node,newVal);
        })
        node.addEventListener('input',(e)=>{
              let value = e.target.value;
              
              this.setVal(vm,expr,value)
        })
        fn(node,value);
        
    },
    html(node,expr,vm){
        let fn = this.updater['htmlUpdate'];
        let value = this.getVal(vm,expr)
        new Watcher(vm,expr,(newVal)=>{ // 给输入框加入观察者模式
            fn(node,newVal);
        })
        fn(node,value);  
    },
    getContentValue(vm,expr){
         return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
              return this.getVal(vm,args[1].trim());
         })
    },
    text(node,expr,vm){
        let fn = this.updater['textUpdate'];
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            // 给每个 表达式加上观察者
            new Watcher(vm,args[1].trim(),()=>{
                    fn(node,this.getContentValue(vm,expr));
            })
            
             return this.getVal(vm,args[1].trim());
        })
        
        fn(node,content); 
    },
    updater:{
        
         modelUpdate(node,value){
             
              node.value = value
         },
        //  文本节点更新
         textUpdate(node,value){
          
              node.textContent = value;
         },
         htmlUpdate(node,value){
            node.innerHTML = value;
         }
    }

}
  1. 数据劫持Oberser.js 为每一个数据添加数据监听
class Observer{
    constructor(data){
    
        this.observer(data)
    }
    observer(data){
        if(data && typeof data == 'object'){
             for(let key in data){
                 this.defineReactive(data,key,data[key]);
             }
        }
    }
    // 定义响应式
    defineReactive(obj,key,value){
        this.observer(value);
        let dep = new Dep() // 给每一个属性加上响应式功能
        Object.defineProperty(obj,key,{
             get(){
                  Dep.target && dep.addSub(Dep.target);
                  console.log(Dep.target)
                  return value
             },
             set(newValue){
                 if(value==newValue) return;  
                  value = newValue;
                  //  广播之后会执行每个元素的 Watcher.update方法进行视图更新   
                  dep.notify(); // 更新之后进行广播
             }
        })
    }
}
  1. 观察者对象Watcher.js 通过Observer模块的数据劫持,做广播从而导致视图更新
// 观察者(发布订阅) 观察者 被观察者
class Dep{
    constructor(){
         this.subs = [];
    }
   //  添加
    addSub(Watcher){
         this.subs.push(Watcher)
    }
    notify(){
         this.subs.forEach(watcher=>watcher.update())
    }
}
Dep.target=null;

class Watcher{
    constructor(vm,expr,cb){
         this.vm = vm;
         this.expr = expr;
         this.cb = cb;
       //   默认先放一个
         this.oldValue = this.get(vm,expr);
    }
    get(vm,expr){
       //  核心 -- 将模板编译与数据监听相结合
         Dep.target = this;
          
         let value = CompilerUtil.getVal(vm,expr);
         
         // 注意这里的清空
         Dep.target = null; 
         
         return value;
    }
    update(){
         // 更新操作
         let newValue = CompilerUtil.getVal(this.vm,this.expr);
         if(newValue!==this.oldValue){
             this.cb(newValue);
         }
    }
}
  1. Vue实例Mvvm.js 这里就是我们在脚手架里使用到Vue实例了
class Vue{
    constructor(options){
         this.$el = options.el;
         this.$data = options.data || {};
         this.$options = options;
         let computed = options.computed;
         if(this.$el){
              new Observer(this.$data);  // 数据响应式
              for(let key in computed){
                //   computed原理:因为computed存在依赖关系所以 先将每个方法代理到this.$data上然后调用proxyVm方法将数据代理到VM上
                //   也可以向methods类似 通过$options 进行引用  
                   Object.defineProperty(this.$data,key,{
                    get:()=>{
                        // 将捕获的值返回
                            return computed[key].call(this);
                       }
                     
                   })
              }
              this.proxyVm(this.$data);  // 数据代理
              new Compiler(this.$el,this); // 模板编译
         }
    }
    proxyVm(data){
        for(let key in data){ 
            Object.defineProperty(this,key,{
                 get(){
                      return data[key];
                 },
                 set(newVal){
                      data[key]=newVal;
                 }
            })
        }
    }
}
  1. 引入使用index.html 将我们所有编写好的模块每一个都引入进来

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mvvm</title>
</head>
<body>
    <div id="app">
        <input v-model="htmls" type="text">
        <input v-model="infos.age" type="text">
      
        {{ name }}
        <p>
            <a href="aaa">{{ infos.name }}{{ infos.age }}--{{getinfosAge}}</a>
        </p>
        <p v-html="htmls">aa</p>
        <button v-on:click="setAgeNum">点击</button>
        <button @click="setAgeNum">点击1</button>
    </div>
<script src="./Mvvm.js"></script>
<script src="./Compiler.js"></script>
<script src="./Oberser.js"></script>
<script src="./Watcher.js"></script>
<script>
    var vms = new Vue({
           el:'#app',
           data:{
                  name:'asaaaa',
                  htmls:'<h1>标题</h1>',
                  infos:{
                       name:'aaas',
                       age:18
                  }  
           },
        // computed带有缓存--如果视图不变化,视图就不要会进行更新
           computed:{
                getinfosAge(){
                     return this.infos.age*10;
                }
           },
           methods:{
                setAgeNum(){
                    this.infos.age= this.infos.age*100;
                    console.log('asd');
                }   
           }
    }) 
</script>
</body>
</html>

效果

tu

总结

其实MVVM在概念上才是真正将页面与数据逻辑分离的模式,它把数据绑定工作放到一个JS里去实现,通过Compiler 对html中的指令与模板进行编译,然后通过把mode绑定到UI上,更改数据,Oberser对新数据劫持,其次是Watcher派发更新视图实现了MVVM。其实理解了流程再实现很简单,通过探究原理自己也收获了不少。希望这篇文章能够帮助到你!

推荐

# 源码系列之Vue-router的实现原理