vue的MVVM架构设计模式

常见的架构设计模式有MVC、MVP、MVVM。三者的共同点在于MV,既Model模型层和View视图层,模型层主要是业务逻辑相关的数据以及数据的处理,视图层主要是负责将数据渲染到页面上,展示给用户。那不同点在哪里。

一、MVC架构模式

MVC的C是controller,即控制层,负责响应用户的输入的业务逻辑,MVC的通信是单向循环的。模型层将数据给到试图成进行展示,视图成将用户的输入交给控制层进行业务处理,控制层调用模型层相关数据的处理,模型成将新数据给到视图层展示。这就是MVC的事件处理流程。

二、MVP架构模式

MVP的P是Presenter,可以理解为是中间人,它移除了MVC中视图层和模型层的直接交互,两者通过Presenter来进行交互。Presenter获取模型层的数据渲染至视图层,响应处理用户在视图层的输入行为,修改模型层数据,然后再将新数据渲染至视图层。

三、MVVM架构模式

现在讲讲这篇文章的重点MVVM,VM即view model,基本上与MVP模式类似,区别在于vm是通过双向数据绑定来实现视图与模型的自动同步的。现在流行的vue框架底层架构模式就是MVVM。VM主要分为两个部分,一部分是DOMListener,响应处理用户对view的一些交互事件,然后处理响应的model层数据; 还有一部分是DataBinding,就是将model里的数据与view中的对应元素进行绑定,实现数据的展示。这两部分就是MVVM中的双向数据绑定,在vue中实现这两个部分主要通过数据挟持和发布者订阅者模式。

3.1数据挟持

数据挟持就是挟持数据的操作,在对数据进行修改或者获取等操作的时候可以执行额外的业务逻辑。vue2的数据挟持采用的是Object.defineProperty来进行数据的挟持,现在的vue3采用了Proxy代理来实现数据的挟持。
以下演示了通过Object.defineProperty进行数据的挟持:

class MyVue{
    constructor({data}){
        this.hijack(data)
    }
    hijack(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    console.log(`get${key}`)
                    return data[key]
                },
                set(newValue){
                    console.log(`set${key}`)
                    data[key]=newValue;
                }
            })
        })
    }
}
let data={
    name:'zhangsan',
    age:11,
    list:[1,2,3,4]
}
let vue=new MyVue({data})
vue.name='lisi';
console.log(vue.name)

以下演示了用proxy来实现数据挟持:

class MyVue{
    constructor({data}){
        this.hijack(data)
    }
    hijack(data){
        this.data=new Proxy(data,{
            get:function(target,key){
                console.log(`get${key}`)
                return target[key];
            },
            set:function(target,key,newValue){
                console.log(`set${key}`)
                target[key]=newValue
            }
        })
    }
}
let data={
    name:'zhangsan',
    age:11,
    list:[1,2,3,4]
}
let vue=new MyVue({data})
vue.data.list.push(3)
console.log(vue.data.list)

3.2发布订阅者模式

发布订阅者模式有三个部分,一个是发布者,作为事件源发布事件;一个是事件调度中心,事件调度中心监听发布者的事件,通知对应订阅者,执行响应的操作;还有一个是订阅者,响应事件执行额外操作。
以下演示发布订阅者模式


class Pubsub{
    constructor(){
        this.events={};
    }
    publish(eventType,data){
        this.events[eventType]&&this.events[eventType].forEach(fn=>{
            fn(data);
        })
    }
    subscribe(eventType,func){
        if(!this.events[eventType]){
            this.events[eventType]=[];
        }
        this.events[eventType].push(func);
    }
}
// 事件调度中心
let pubsub=new Pubsub();
// 订阅者订阅事件,处理事件
pubsub.subscribe('dataChange',(data)=>{
    console.log(`${data.name}发生,执行额外操作`)
})
// 发布者发布事件
pubsub.publish('dataChange',{name:'publiser'})

四、结合发布订阅者模式和数据挟持,简单实现vue的双向数据绑定

双向数据绑定有两点,第一点就是模型层数据的变化会更新视图层展示的内容,基本思路就是对数据进行挟持,但数据变化时,更新视图;第二点是用户对视图层的交互会反过来影响数据,通过监听用户的交互,执行相应数据处理,来改变模型层数据。
这里引入一个新的概念,complier编译模块,用于编译视图,用过vue的应该知道视图层是通过{{变量}}的声明式模板来将引用model里的数据的,通过v-指令名的方式来绑定指令的,这些转化是需要额外进行编译的。
以下是实现双向数据绑定的简易版代码:


class MyView{
    constructor({el,data,methods}){
        this.el=document.querySelector(el);
        this.methods=methods||{};
        // 发布订阅中间人
        this.pubsuber=new PubSub();
        // 数据挟持
        this.hijack(data);
        //解析指令和差值表达式
        new Compiler(this)
    }
    hijack(data){
        this.data=new Proxy(data,{
            get:(target,key)=>{
                return target[key]
            },
            set:(target,key,newValue)=>{
                this.pubsuber.publish(key);
                target[key]=newValue;
            }
        })
    }

}
class Compiler{
    constructor(vm){
        this.vm=vm;
        this.compile(vm.el);
    }
    compile(el){
        let childNodes=el.childNodes;
        if(childNodes&&childNodes.length){
            Array.from(childNodes).forEach(node=>{
                if(node.nodeType===3){
                    this.compileTextNode(node);
                }else if(node.nodeType === 1){
                    this.compileElementNode(node)
                }
                if(node.childNodes&&node.childNodes.length){
                    this.compile(node);
                }
            })
        }
    }
    compileTextNode(node){
        let reg=/\{\{(.+?)\}\}/;
        if(reg.test(node.textContent)){
            node.textContent=node.textContent.replace(reg,(all,key)=>{
                this.vm.pubsuber.subscribe(key,new Subscriber(this.vm,key,(newValue)=>{
                    node.textContent=newValue;
                }))
                console.log(this.vm.data[key])
                return this.vm.data[key]
            })
        }
    }
    compileElementNode(node){
        if(node.attributes&&node.attributes.length){
            Array.from(node.attributes).forEach(attr=>{
                console.log(attr)
                if(attr.name.startsWith('v-')){
                    let [directive,key]=attr.name.slice(2).split(':');
                    switch(directive){
                        case 'bind':
                            node[key]=this.vm.data[attr.value];
                            break;
                        case 'on':
                            document.addEventListener(key,this.vm.methods[attr.value].bind(this.vm))
                            break;
                    }
                }
            })
        }
    }
}
class PubSub{
    constructor(){
        this.subscribers={};
    }
    publish(type){
        this.subscribers[type].forEach(subscriber=>{
            subscriber.update();
        })
    }
    subscribe(type,subscriber){
        if(!this.subscribers[type]){
            this.subscribers[type]=[]
        }
        this.subscribers[type].push(subscriber);
    }
}
class Subscriber{
    constructor(vm,key,cb){
        this.vm=vm;
        this.key=key;
        this.cb=cb;
    }
    update(){
        let newValue=this.vm.data[this.key];
        this.cb(newValue);
    }
}

实际在页面中使用的代码如下:

<!DOCTYPE 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>Document</title>
</head>
<body>
    <div id="app">
        <p>{{name}}</p>
        <p>{{age}}</p>
        <input type="text" v-bind:value="name" v-on:input="changeName">
    </div>
</body>
</html>
<script src="myVue.js"></script>
<script>
    new MyView({
        el:'#app',
        data:{
            name:'lin',
            age:21
        },
        methods: {
            changeName:function(e){
                this.data.name=e.target.value;
            }
        },
    })
</script>

以上代码仅仅是实现了双向绑定,vue还有很多细节深挖,比方说我这个简易版是直接对dom进行修改,而vue则是通过修改虚拟dom,然后通过diff算法找出虚拟dom和真实dom之间不同的节点,然后修改更新那些不一样的节点,这种渲染方式在需要频繁修改dom元素的场景下可以很好的减少重排重绘问题。之后会在继续深挖。未完待续。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值