MVVM

1,580 阅读6分钟

一、MVVM模式

MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。

二、为什么会出现 MVVM 呢?

  • Model:代表数据模型,数据和业务逻辑都在Model层中定义;

  • View:代表UI视图,负责数据的展示;

  • ViewModel:就是与界面(view)对应的Model。因为,数据库结构往往是不能直接跟界面控件一一对应上的,所以,需要再定义一个数据对象专门对应view上的控件。而ViewModel的职责就是把model对象封装成可以显示和接受输入的界面数据对象。

三、MVVM 的出现,完美解决了以上三个问题。

  • MVVM 由 Model、View、ViewModel 三部分构成,Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。

  • 在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。

  • ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

四、Vue.js 的细节

  • Vue.js 可以说是MVVM 架构的最佳实践,专注于 MVVM 中的 ViewModel,不仅做到了数据双向绑定,而且也是一款相对来比较轻量级的JS 库,API 简洁,很容易上手。Vue的基础知识网上有现成的教程,此处不再赘述, 下面简单了解一下 Vue.js 关于双向绑定的一些实现细节:

  • Vue.js 是采用 Object.defineProperty 的 getter 和 setter,并结合观察者模式来实现数据绑定的。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

五、如何实现MVVM

  • Observer :数据监听器,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用Object.defineProperty的getter和setter来实现
  • Compile :指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  • Watcher :订阅者,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数
  • Dep :消息订阅器,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发notify 函数,再调用订阅者的 update 方法
  • 从封面图中可以看出,当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器Compile 对元素节点的指令进行扫描和解析,初始化视图,并订阅 Watcher 来更新视图, 此时Wather 会将自己添加到消息订阅器中(Dep),初始化完毕
  • 当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。
  • 代码如下:
class Dep{//观察者模式代码
    constructor(){
        this.subs = [] //存储订阅者,存储传进来的观察者
    }
    addSub(watcher){//订阅 let watcher = new Wathcer()
        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.oldData = this.get()
    }
    get(){
        Dep.target = this
        let value = ComplieUtil.getVal(this.vm,this.expr);
        Dep.target = null
        return value
    }
    update(){
        let newVal = ComplieUtil.getVal(this.vm,this.expr);
        if(newVal !== this.oldData){
            this.cb(newVal)
        }
    }
}
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)
                return value
            },
            set: (newVal) => {
                // console.log(newVal,"newVal")
                if (newVal !== value) {
                    this.observer(newVal)//补充
                    value = newVal;
                    dep.notify();//发布功能
                }
            }
        })
    }
}
//基类调度
class Complie {
    constructor(el, vm) {
        //判断el属性是不是⼀个元素 如果不是元素 那就获取它
        this.el = this.isElementNode(el) ? el : document.querySelector(el); //1
        //保证所有⼈拿到实例vm
        this.vm = vm;
        //把当前节点中的元素放到内存中
        let fragment = this.node2fragment(this.el); //2
        //⽤数据编译模板
        this.compile(fragment); //4
        //把内容塞到⻚⾯中
        this.el.appendChild(fragment) //3
    }
    //判断是不是元素节点
    isElementNode(node) {
        return node.nodeType === 1;
    }
    //把每个节点都放⼊到内存中
    node2fragment(node) {
        //创建⼀个⽂档碎⽚
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = node.firstChild) {
            //appendChild具有移动性
            fragment.appendChild(firstChild)
        }
        return fragment
    }
    //核⼼的编译⽅法
    compile(node) { //⽤来编译内存的dom节点
        let childNodes = node.childNodes;
        [...childNodes].forEach(child => {
            //判断是否为节点
            if (this.isElementNode(child)) {
                // 属性节点方法
                //type="text" v-model="className.name"
                this.compileElement(child);
                //如果是元素的话 需要传⼊⾃⼰然后遍历⼦节点
                this.compile(child)
            } else {
                //处理文本节点
                //{{className.name}}
                this.compileText(child);
            }
        });
    }
    //编译元素节点下的属性节点
    //type="text" v-model="className.name"
    isDirective(attrName){
        return attrName.startsWith('v-')
    }
    //元素节点 <input type v-model/> <h1> <h3> <ul>
    compileElement(node){
        let attributes = node.attributes;
        [...attributes].forEach(attr => {
            //[type:'text',v-model:'className.name']
            let {name,value:expr} = attr;//重命名
            // console.log(expr)
            if(this.isDirective(name)){//v-model: v-html
                let [,directive] = name.split('-');
                // [,directive] = [v,model]
                // [a,b]=[b,a]
                // console.log(directive)
                let [directiveName,] = directive.split(":")
                //这个方法为了复用 代码优化 model
                //对应 129行 <input />
                ComplieUtil[directiveName](node,expr,this.vm)
            }
        })
    }
    //文本节点
    //{{className.name}}
    compileText(node){
        let content = node.textContent;
        // console.log(content)正则表达式 {{className.name}}
        if(/\{\{(.+?)\}\}/.test(content)){
            //"{{className.name}}"
            ComplieUtil['text'](node,content,this.vm)
        }
    }
}
ComplieUtil = {
    getVal(vm,expr){//className.name
        return expr.split('.').reduce((data,current)=>{
            return data[current]//1707班
        },vm.$data)
    },
    setVal(vm,expr,value){
        // [className,name]
        expr.split('.').reduce((data,current,index,arr)=>{
            if(index === arr.length - 1){
                return data[current] = value//1707班
            }
            return data[current]
        },vm.$data)
    },
    model(node,expr,vm){
        let fn = this.updater['modelUpdater']
        new Watcher(vm,expr,(newVal)=>{
            // 给输入框添加观察者,如果数据更新就会触发这个方法,会拿到新的值
            fn(node,newVal)
        })
        let value = this.getVal(vm,expr)
        fn(node,value)
        node.addEventListener('input',(e)=>{
            let value = e.target.value;
            this.setVal(vm,expr,value)
        })
    },
    text(node,expr,vm){
        let fn = this.updater["textUpdater"]
        let value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            new Watcher(vm,args[1],(newVal)=>{
                fn(node,newVal)
            })
            return this.getVal(vm,args[1])
        })
        fn(node,value)
    },
    updater:{
        modelUpdater(node,value){
           node.value = value
        },
        textUpdater(node,value){
            node.textContent = value
        },
    }
}