文章目录
- 一、设计思路
- 二、数据劫持
- 三、发布与订阅
- 四、总结
一、设计思路
MVVM框架相对传统的dom操作模式,最大的优点就是数据的双向绑定。它借鉴了桌面应用程序MVC的设计思想,Model与View之间通过ViewMode关联,ViewModel负责将Model数据的变化显示到View上,通过将View的改变反馈到Model上这就是我们常说的双向绑定数据机制。
那如何设计MVVM模型模型呢。需要解决两个关键问题:
- 1、如何知道数据更新。
- 2、数据更新后,如何通知变化。
下面我们就分别介绍下vue是如何实现的,理解了这两点,基本上也就明白了双向绑定的机制。
二、数据劫持
在ES5中有Object.defineProperty()方法,它能监听各个属性的set和get方法。
let data ={name:'tcy'}
Object.defineProperty(data,'name',{
set: function(newValue) {
console.log('更新了data的name:' + newValue);
},
get: function() {
console.log('获取data数据name');
}
})
="fyn";//更新了data的name:fyn
;//获取data数据name
- Object.defineProperty()方法,有三个参数,分别为待监听的数据对象,待监听的属性,以及对set,get的监听方法。
- 上例中,对data对象的name属性进行监听,当执行"=‘fyn’"触发set方法,执行""触发get方法。
- vue正是采用了该方法,对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);
="fyn";//set劫持,新值:fyn
console.log();//get劫持,fyn
- data模拟vue.data对象,observer中对data的属性进行遍历,调用defineReactive对每个属性的get和set方法进行劫持。
- 由此,data数据的任何属性值变化,都可以监听和劫持,上述的第一个问题就解决了。
- 那view端的数据变化是如何知道的呢,view端改变数据的组件无外乎input,select等,可以用组件的onchange事件监听,这里就不再重点描述。
- 接下来就要解决第二个问题,监听到数据变化后如何通知。
三、发布与订阅
vue在双向绑定的设计中,采用的是观察-订阅模式,前面所讲的数据劫持,其实就是为属性创建了一个观察者对象,监听数据的变化。接下来就是创建发布类和订阅类,如下:
- observer,创建数据监听,并为每个属性建立一个发布类。
- Dep是发布类,维护与该属性相关的订阅实例,当数据发生更新时,会通知所有的订阅实例。
- Watcher是订阅类,注册到所有相关属性的Dep发布类中,接受发布类的数据变更通知,通过回调,实现视图的更新。
下面我们就模式发布和订阅的过程。
//对象数据
let data ={name:'tcy',age:'20',sex:'male'};
//发布器的uid
let uid=0;
//发布器
class Dep{
constructor () {
this.id = uid++//发布器的标识,每次加1
this.subs = []//订阅者集合
}
//添加订阅者实例对象
addSub(watcher){
this.subs.push(watcher);
}
//移除订阅者实例对象
removeSub (watcher) {
remove(this.subs, watcher)
}
// 依赖收集函数,在 getter 中执行,在 Dep.target 上找到当前 watcher,并添加依赖
depend() {
Dep.target && Dep.target.addDep(this)
}
//通知所有订阅者
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();//更新
}
}
}
//记录当前的watcher实例
Dep.target = null;
const targetStack = []
function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
function popTarget () {
Dep.target = targetStack.pop()
}
//订阅者
class Watcher{
constructor (
vm,//vm数据对象
expOrFn,//待监听的属性表达式
cb//监听到变化后的回调函数
){
this.vm=vm;
this.expOrFn = expOrFn;
this.cb = cb;
this.value= this.get();
}
//添加自身订阅者到发布器
addDep (dep) {
dep.addSub(this)
}
//通知更新
update(){
this.run();
}
//实现视图的更新
run(){
let oldValue = this.value//更新前数据
let value = this.get();//获取最新值
if(value != oldValue){
this.cb.call(this.vm);
}
}
//获取value值,并进行依赖收集
get(){
//将自身watcher订阅实例设置给Dep.target
pushTarget(this);
//这一步很重要,获取属性值,同时将订阅者实例添加到发布器中
let value = this.expOrFn.call(this.vm);
//将订阅实例从target栈中取出并设置给Dep.target
popTarget();
return value;
}
}
function observer(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){
//为每个键都创建一个 dep
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,//可枚举
configurable: true,//可配置
get: function reactiveGetter () {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function reactiveSetter (newVal) {
val = newVal;
dep.notify(); // 通知所有订阅者
}
})
}
//第一步,为每个属性创建一个发布器,并设置set,get劫持
observer(data);
//第二步,创建监听并实现依赖收集
var watcher = new Watcher(data,() => {
"name"+ + "age"+data.age
},() => {
console.log("实现视图更新");
});
//第三步,数据变化,触发视图更新
="fyn";//实现视图更新
data.sex="female";
下面简单介绍下这几个类
- 1、发布器Dep类
构造函数中定义了subs集合,保存所有注册的订阅者实例,uid为发布器对象的标识。
addSub,removeSub方法,添加和移除的订阅者,维护实例集合。
depend方法,将当前的watcher实例对象添加到subs集合中。这是会涉及到依赖收集,后面会讲到。
notify方法,遍历subs中所有维护的订阅者实例,并进行更新操作。
- 2、订阅者Watcher类,
- 在构造函数的入参中设置
- vm-组件对象,即本例的data
- expOrFn–属性或者表达式
- cb–更新视图的回调函数
在实例化的时候,调用get方法,获取当前的监听属性的值,同时触发该属性的get方法(参见defineReactive的get方法),调用dep.depend()将订阅实例添加上发布器Dep中(记住,这步很重要)
run方法,收到变化通知,比较数据的前后值,调用cb实现视图的更新。
- 3、defineReactive方法
该方法与前面的比较,总体框架是一致的,但增加了一些代码。
let dep = new Dep();
为数据对象中的每个属性建立一个发布器。
在get发方法中,将订阅者加入到该属性的发布器subs中
在set中,数据发生变化,调用dep.notify()通知所有的订阅者,最终执行run方法,比较value值是否发生了变化,如果是,则调用cb的回调方法进行视图更新。
如果上面的分析没看明白,没关系,我们用具体的实例,分析下其调用过程。
- 1、为每个属性创建一个发布器,并设置set,get劫持
执行observer(data);
- 2、第二步,创建监听,并实现依赖收集
我们考虑下,模板上有一段代码,“
name:{{}},age:{{data.age}}”,暂不考虑编译过程,翻译过来,
"name"+ + "age"+data.age
需要对这个表达式进行监听,我们期望name,age的属性发生任何变化,立即通知模板实现页面的刷新,因为data中还有个sex的属性,这个表达式没有涉及到,所以为了提高效率,这个属性的任何变化,无需实现监,这就是依赖收集的概念。
我们看下是如何做到的。
在创建这个实例的过程中,会调用get方法,执行下面一段代码:
let value = this.expOrFn.call(this.vm);
它会触发name,age属性的get劫持方法,调用dep.depend方法,将该监听加入到对应属性的dep对象中。
- 3、数据变化,触发视图更新
- 当执行="fyn"时,会被name属性的set方法劫持,调用dep.notify(),该属性的dep对象的subs是有watcher实例的,故执行该watcher实例的updata方法,实现视图更新。
- 当执行data.sex="female"时,会被sex属性的set方法劫持,调用dep.notify(),但该属性的dep对象的subs是空值,不会有任何更新。
整个流程的方法调用示意图:
四、总结
原文链接:
1.扩展:
VUE实现双向数据绑定的原理就是利用了 Object.defineProperty()
这个方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。它接收三个参数,要操作的对象,要定义或修改的对象属性名,属性描述符。重点就是最后的属性描述符。属性描述符是一个对象,主要有两种形式:数据描述符和存取描述符。这两种对象只能选择一种使用,不能混合两种描述符的属性同时使用。上面说的get和set就是属于存取描述符对象的属性。在面试中如何应对?
2.VUE双向绑定的原理?
答:VUE实现双向数据绑定的原理就是利用了 Object.defineProperty()
这个方法重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。 代码演示:
- defineProperty的用法
var obj = { };var name;
//第一个参数:定义属性的对象。//第二个参数:要定义或修改的属性的名称。//第三个参数:将被定义或修改的属性描述符。
Object.defineProperty(obj, "data", {
//获取值get:
function () {return name;},
//设置值set:
function (val) {name = val;console.log(val)}})//赋值调用setobj.data = 'aaa';
//取值调用getconsole.log(obj.data);
代码演示:defineProperty的双向绑定
var obj={};
Object.defineProperty(obj, 'val',{
set:function (newVal) {
document.getElementById("a").value =newVal==undefined?'':newVal;
document.getElementById("b").innerHTML=newVal==undefined?'':newVal;
}});
document.getElementById("a").addEventListener("keyup",function (e) {obj.val = e.target.value;})