常见的架构设计模式有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元素的场景下可以很好的减少重排重绘问题。之后会在继续深挖。未完待续。