MVVM的概念,原理,及实现

1,mvvm的概念

model-view-viewmodel,通过数据劫持+发布者订阅模式来实现

在model中定义数据修改和操作的业务逻辑

在view表示ui组件,负责将数据模型转化为ui展示出来,做的是数据绑定声明,指令的声明,事件绑定的声明

viewmodel是一个同步view和model的对象,view和model没有直接的联系,通过viewmodel来进行交互的

2.MVVM和MVC的优点

a. MVVM是数据驱动,MVC是dom驱动

b.MVVM优点在于不用操作大量的DOM,不用关注view和model之间的关系,而MVC需要

3.MVVM的优缺点

1、低耦合性  view 和 model 之间没有直接的关系,通过 viewModel 来完成数据双向绑定。

2、可复用性 组件是可以复用的。可以把一些数据逻辑放到一个 viewModel 中,让很多 view 来重用。

3、独立开发 开发人员专注于 viewModel ,设计人员专注于view。4、可测试性  ViewModel 的存在可以帮助开发者更好地编写测试代码

a, bug很难调试,因为数据双向绑定,所以问题可能在view中,也可能在model,找到其原始定位难度高

b.一个模块中的model内存可能很大,长期保存会影响性能

c. 对于大型的图形应用程序,视图状态越多,viewmodel的维护成本较高

4.MVVM的双向绑定原理:

MVVM核心是数据劫持,数据代理,数据编译,和‘发布订阅者模式’

a.数据劫持:就是给对象属性添加get,set钩子函数

1、观察对象,给对象增加 Object.defineProperty

 2、vue的特点就是新增不存在的属性不会给该属性添加 get 、 set 钩子函数。

3、深度响应。循环递归遍历 data 的属性,给属性添加 get , set 钩子函数。

4、每次赋予一个新对象时(即调用 set 钩子函数时),会给这个新对象进行数据劫持( defineProperty )。

//通过set、get钩子函数进行数据劫持
function defineReactive(data){
    Object.keys(data).forEach(key=>{
        const dep=new Dep();
        let val=data[key];
        this.observe(val);//深层次的监听
        Object.defineProperty(data,key,{
            get(){
                //添加订阅者watcher(为每一个数据属性添加订阅者,以便实时监听数据属性的变化——订阅)
                Dep.target&&dep.addSub(Dep.target);
                //返回初始值
                return val;
            },set(newVal){
                if(val!==newVal){
                    val=newVal;
                    //通知订阅者,数据变化了(发布)
                    dep.notify();
                    return newVal;
                }
            }
        })
    })
}

b、数据代理

将 data , methods , compted 上的数据挂载到vm实例上。让我们不用每次获取数据时,都通过 mvvm._data.a.b 这种方式,而可以直接通过 mvvm.b.a 来获取。

class MVVM{
    constructor(options){
        this.$options=options;
        this.$data=options.data;
        this.$el=options.el;
        this.$computed=options.computed;
        this.$methods=options.methods;
        //劫持数据,监听数据的变化
        new Observer(this.$data);
        //将数据挂载到vm实例上
        this._proxy(this.$data);
        //将方法也挂载到vm上
        this._proxy(this.$methods);
        //将数据属性挂载到vm实例上
        Object.keys(this.$computed).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return this.$computed[key].call(this);//将vm传入computed中
                }
            })
        })
        //编译数据
        new Compile(this.$el,this)
    };
    //私有方法,用于数据劫持
    _proxy(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                }
            })
        })

    }
}

c.数据编译

 把 {{}} , v-model , v-html , v-on ,里面的对应的变量用data里面的数据进行替换

class Compile{
    constructor(el,vm){
        this.el=this.isElementNode(el)?el:document.querySelector(el);
        this.vm=vm;
        let fragment=this.nodeToFragment(this.el);
        //编译节点
        this.compile(fragment);
        //将编译后的代码添加到页面
        this.el.appendChild(fragment);
    };
    //核心编译方法
    compile(node){
        const childNodes=node.childNodes;
        [...childNodes].forEach(child=>{
            if(this.isElementNode(child)){
                this.compileElementNode(child);
                //如果是元素节点就还得递归编译
                this.compile(child);
            }else{
                this.compileTextNode(child);
            }
        })

    };
    //编译元素节点
    compileElementNode(node){
        const attrs=node.attributes;
        [...attrs].forEach(attr=>{
            //attr是一个对象
            let {name,value:expr}=attr;
            if(this.isDirective(name)){
                //只考虑到v-html和v-model的情况
                let [,directive]=name.split("-");
                //考虑v-on:click的情况
                let [directiveName,eventName]=directive.split(":");
                //调用不同的指令来进行编译
                CompileUtil[directiveName](node,this.vm,expr,eventName);
            }
        })
    };
    //编译文本节点
    compileTextNode(node){
        const textContent=node.textContent;
        if(/\{\{(.+?)\}\}/.test(textContent)){
            CompileUtil["text"](node,this.vm,textContent)
        }
    };
    //将元素节点转化为文档碎片
    nodeToFragment(node){
         //将元素节点缓存起来,统一编译完后再拿出来进行替换
         let fragment=document.createDocumentFragment();
         let firstChild;
         while(firstChild=node.firstChild){
             fragment.appendChild(firstChild);
         }
         return fragment;
    };
    //判断是否是元素节点
    isElementNode(node){
        return node.nodeType===1;
    };
    //判断是否是指令
    isDirective(attr){
        return attr.includes("v-");
    }
}
//存放编译方法的对象
CompileUtil={
    //根据data中的属性获取值,触发观察者的get钩子
    getVal(vm,expr){
        const data= expr.split(".").reduce((initData,curProp)=>{
            //会触发观察者的get钩子
            return initData[curProp];
        },vm)
        return data;
    },
    //触发观察者的set钩子
    setVal(vm,expr,value){
        expr.split(".").reduce((initData,curProp,index,arr)=>{
            if(index===arr.length-1){
                initData[curProp]=value;
                return;
            }
            return initData[curProp];
        },vm)
    },
    getContentValue(vm,expr){
        const data= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1]);
        });
        return data;
    },
    model(node,vm,expr){
        const value=this.getVal(vm,expr);
        const fn=this.updater["modelUpdater"];
        fn(node,value);
        //监听input的输入事件,实现数据响应式
        node.addEventListener('input',e=>{
            const value=e.target.value;
            this.setVal(vm,expr,value);
        })
        //观察数据(expr)的变化,并将watcher添加到订阅者队列中
        new Watcher(vm,expr,newVal=>{
            fn(node,newVal);
        });
    },
    text(node,vm,expr){
        const fn=this.updater["textUpdater"];
        //将{{person.name}}中的person.james替换成james
        const content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            //观察数据的变化
            new Watcher(vm,args[1],()=>{
                // this.getContentValue(vm,expr)获取textContent被编译后的值
                fn(node,this.getContentValue(vm,expr))

            })
            return this.getVal(vm,args[1]);
        })
        fn(node,content);
    },
    html(node,vm,expr){
        const value=this.getVal(vm,expr);
        const fn=this.updater["htmlUpdater"];
        fn(node,value);
        new Watcher(vm,expr,newVal=>{
            //数据改变后,再次替换数据
            fn(node,newVal);
        })
    },
    on(node,vm,expr,eventName){
        node.addEventListener(eventName,e=>{
            //调用call将vm实例(this)传到方法中去
            vm[expr].call(vm,e);
        })
    },
    updater:{
        modelUpdater(node,value){
            node.value=value
        },
        htmlUpdater(node,value){
            node.innerHTML=value;
        },
        textUpdater(node,value){

            node.textContent=value;
        }
    }
}

d.发布订阅

 发布订阅主要靠的是数组关系,订阅就是放入函数(就是将订阅者添加到订阅队列中),发布就是让数组里的函数执行(在数据发生改变的时候,通知订阅者执行相应的操作)。消息的发布和订阅是在观察者的数据绑定中进行数据的——在get钩子函数被调用时进行数据的订阅(在数据编译时通过  new Watcher() 来对数据进行订阅),在set钩子函数被调用时进行数据的发布

//消息管理者(发布者),在数据发生变化时,通知订阅者执行相应的操作
class Dep{
    constructor(){
        this.subs=[];
    };
    //订阅
    addSub(watcher){
        this.subs.push(watcher);
    };
    //发布
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
//订阅者,主要是观察数据的变化
class Watcher{
    constructor(vm,expr,cb){
        this.vm=vm;
        this.expr=expr;
        this.cb=cb;
        this.oldValue=this.get();
    };
    get(){
        Dep.target=this;
        const value=CompileUtil.getVal(this.vm,this.expr);
        Dep.target=null;
        return value;
    };
    update(){
        const newVal=CompileUtil.getVal(this.vm,this.expr);
        if(this.oldValue!==newVal){
            this.cb(newVal);
        }
    }
}
//观察者
class Observer{
    constructor(data){
        this.observe(data);
    };
    //使数据可响应
    observe(data){
        if(data&&typeof data==="object"){
            this.defineReactive(data)
        }
    };
    defineReactive(data){
        Object.keys(data).forEach(key=>{
            const dep=new Dep();
            let val=data[key];
            this.observe(val);//深层次的监听
            Object.defineProperty(data,key,{
                get(){
                    //添加订阅者watcher(为每一个数据属性添加订阅者,以便实时监听数据属性的变化——订阅)
                    Dep.target&&dep.addSub(Dep.target);
                    //返回初始值
                    return val;
                },set(newVal){
                    if(val!==newVal){
                        val=newVal;
                        //通知订阅者,数据变化了(发布)
                        dep.notify();
                        return newVal;
                    }
                }
            })
        })
    }
}

你可能感兴趣的:(mvc)