MVVM 是 Model-View-ViewModel(模型-视图-视图模型)的缩写;是一种软件架构设计模式。
其中:
在MVVM的框架下,View和Model是不能直接通信的,Model与View之间通过ViewMode关联。ViewModel负责将Model数据的变化显示到View上,将View的改变反馈到Model上。这就是我们常说的双向绑定数据机制。
那如何设计MVVM模型模型呢。需要解决两个关键问题:
下面就分别介绍vue是如何实现的以上两点,理解了这两点,基本上也就明白了双向绑定的机制。
Vue2 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty
来劫持数据的setter
,getter
,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。
要实现MVVM的双向绑定,就必须要实现以下几点:
mvvm入口函数,整合以上三者,具体如图所示:
下面分别说明数据劫持与发布者-订阅者模式。
Object.defineProperty()
在ES5中有Object.defineProperty()
方法,它能监听各个属性的set和get方法。
Object.defineProperty()方法,有三个参数,分别为待监听的数据对象,待监听的属性,以及对set,get的监听方法。
例如:对data对象的name属性进行监听,当执行"data.name='fyn'"
触发set方法,执行"data.name"
触发get方法。
let data ={name:'tcy'}
Object.defineProperty(data,'name',{
set: function(newValue) {
console.log('更新了data的name:' + newValue);
},
get: function() {
console.log('获取data数据name');
}
})
data.name="fyn";//更新了data的name:fyn
data.name;//获取data数据name
什么是数据劫持
通过上面对Object.defineProperty的介绍,我们不难发现,当我们访问或设置对象的属性的时候,都会触发相对应的函数,然后在这个函数里返回或设置属性的值。
既然如此,我们当然可以在触发函数的时候动一些手脚做点我们自己想做的事情,这也就是“劫持”操作。
模拟Vue实现数据劫持
vue正是采用了Object.defineProperty()
方法,对data的属性进行劫持。我们来模拟实现其劫持的过程。
/*
模拟vue的data数据
var vm = new Vue(
{
data:{
name:'tcy',
age:'20'
}
}
)
*/
let data ={name:'tcy',age:'20'}
function observe(data){
//获取所有的data数据对象中的所有属性进行遍历
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
let val = data[keys[i]];
defineReactive(data, keys[i],val)//为每个属性增加监听
}
}
function defineReactive(obj,key,val){
Object.defineProperty(obj, key, {
enumerable: true,//可枚举
configurable: true,//可配置
get: function reactiveGetter () {
//模拟get劫持
console.log("get劫持");
return val;
},
set: function reactiveSetter (newVal) {
//模拟set劫持
console.log("set劫持,新值:"+newVal);
val = newVal;
}
})
}
observe(data);
data.name="fyn";//set劫持,新值:fyn
console.log(data.name);//get劫持,fyn
data 模拟 vue.data 对象,observer 中对 data 的属性进行遍历,调用 defineReactive 对每个属性的 get 和 set 方法进行劫持。
由此,data数据的任何属性值变化,都可以监听和劫持,上述的第一个问题(如何知道数据更新)就解决了。那view端的数据变化是如何知道的呢,view端改变数据的组件无外乎input,select等,可以用组件的 onchange 事件监听,这里就不再重点描述。
vue在双向绑定的设计中,采用的是观察-订阅模式,前面所讲的数据劫持,其实就是为属性创建了一个观察者对象,监听数据的变化。接下来就是创建发布类和订阅类。
来看一下下面的图:
图中:
observer,创建数据监听,并为每个属性建立一个发布类。
Dep是发布订阅者模型中的发布者:get数据的时候,收集订阅者,触发Watcher的依赖收集;set数据时发布更新,通知Watcher 。一个Dep实例对应一个对象属性或一个被观察的对象,用来收集订阅者和在数据改变时,发布更新。
Watcher是发布订阅者模型中的订阅者:订阅的数据改变时执行相应的回调函数(更新视图或表达式的值)。一个Watcher可以更新视图,如html模板中用到的{{test}}
,也可以执行一个$watch
监督的表达式的回调函数(Vue实例中的watch项底层是调用的$watch
实现的),还可以更新一个计算属性(即Vue实例中的computed项)。
其中:
图中红色的箭头表示的是收集依赖时获取数据的流程。Watcher会收集依赖的时候(这个时机可能是实例创建时,解析模板、初始化watch、初始化computed,也可能是数据改变后,Watcher执行回调函数前),会获取数据的值,此时Observer会拦截数据(即调用get函数),然后通知Dep可以收集订阅者啦。Dep将订阅数据的Watcher保存下来,便于后面通知更新。
图中绿色的箭头表示的是数据改变时,发布更新的流程。当数据改变时,即设置数据时,此时Observer会拦截数据(即调用set函数),然后通知Dep,数据改变了,此时Dep通知Watcher,可以更新视图啦。
具体过程:
VUE探索第一篇–双向绑定原理
Vue源码解读一:Vue数据响应式原理
学习参考于:
VUE探索第一篇–双向绑定原理
Vue源码解读一:Vue数据响应式原理
梳理vue双向绑定的实现原理