结合项目食用更佳
一、Vue数据双向绑定原理:
Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,并添加相应的watcher,当数据变化时watcher通知视图更新
二、MVVM数据--v-model双向绑定:
数据变化更新视图,视图变化更新数据。
1.输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
2.data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。
问题:数据变化如何更新视图?
1.如何知道数据发生了变化
2.数据在什么时候发生改变
3.剩下,我们只需在数据变化的时候去通知视图更新即可
三、数据的观测性--getter,setter:
是Vue底层的数据的取值和给值
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
1.一般情况下我们定义个对象,但是这种定义方式,并不知道什么时候修改或者读取属性,换句话说,不具备观测性。
let car = {
'color':'blue',
'price':3000
}
console.log(car.color) //blue
2.我们试着用Object.defineProperty去改写一下,car已经可以主动告诉我们它的属性的读写情况了,也意味着,这个car的数据对象已经是“可观测”的了。
let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
get() {
console.log('price属性被读取了')
return val
},
set(newVal) {
console.log('price属性被修改了')
val = newVal
}
})
car.price=5000
console.log(car.price)// price属性被修改了 price属性被读取了 3000
3. 现在把对象所有属性都变得可观测,我们可以编写如下两个函数:
function observable (obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) =>{
defineReactive(obj,key,obj[key])
})
return obj;
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
Object.defineProperty(obj, key, {
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
现在,我们就可以这样定义car,这个car对象里的所有对象都是可观测的。
let car = observable({
'brand':'BMW',
'price':3000
})
四. 依赖收集(Observer):
让我们知道数据在什么时候被读或写,即可以进行下一步操作
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
一旦数据发生变化,实时刷新
“发布订阅者”模式:
数据变化为“发布者”,依赖对象为“订阅者”。
我们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,new.target必须写在构造方法里面,它指向类本身。具体指向哪个类这是一个全局唯一 的Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组,我们将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。
1.创建一个依赖收集容器(消息订阅器Dep),用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数
class Dep {
constructor(){
this.subs = []
},
//增加订阅者
addSub(sub){
this.subs.push(sub);
},
//判断是否增加订阅者
depend () {
if (Dep.target) {
this.addSub(Dep.target)
}
},
//通知订阅者更新
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}
}
Dep.target = null;
2.有了订阅器,再将defineReactive函数进行改造一下,向其植入订阅器:
function defineReactive (obj,key,val) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){
dep.depend();
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify() //数据变化通知所有订阅者
}
})
}
五、订阅者watcher:
Watcher订阅者是Observer和Compile之间通信的桥梁
主要做的事情是:
①在自身实例化时往属性订阅器,在函数取值时(dep.target)里面赋值this指向
②自身必须有一个update()方法
③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,我们只需要在Watcher初始化的时候触发对应的get函数去调用即可
watch允许执行异步操作
如何触发?
获取对应的属性值就可以我们只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者Watcher的实现如下
class Watcher {
constructor(vm,exp,cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
},
update(){
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
},
get(){
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
订阅者Watcher 是一个 类,在它的构造函数中,定义了一些属性:
vm:一个Vue的实例对象;
exp:是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
cb:是Watcher绑定的更新函数;
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到:
数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行:
Dep.target = this; // 缓存自己
实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行了:
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
在这个过程中会对 vm 上的数据访问,其实就是为了触发数据对象的getter。
每个对象值的 getter都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target 恢复成上一个状态,即:
Dep.target = null; // 释放自己
因为当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。而update()函数是用来当数据发生变化时调用Watcher自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];获取到最新的数据,然后将其与之前get()获得的旧数据进行比较,如果不一样,则调用更新函数cb进行更新。
六、使用这玩意儿进行数据劫持有什么缺点(Object.defineProperty())
有些无法拦截:[数组:大部分操作拦截不到]
属性进行操作时,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,Object.defineProperty 不能拦截到这些操作,Vue 内部通过重写函数的方式解决了这个问题
在 Vue3.0 中不使用Object.defineProperty了,改用 Proxy 对对象进行代理,实现数据劫持。Proxy可以做到的监听到任何方式的数据改变,唯一的缺点是兼容性问题,因为 Proxy 是 ES6 的语法
七、MVVM、MVC、MVP的区别
共同点:分离关注点的方式来组织代码结构,优化开发效率
(1)MVC
通过分离 Model、View 和 Controller 的方式来组织代码结构
View :UI视图,负责数据的展示
Model :负责存储页面的业务数据,及对相应数据的逻辑操作
View 和 Model 应用了观察者模式,Model 层改变, View 层会实时更新
详细看下: 观察者模式具体解说
Controller :Controller 层是 View 层和 Model 层的纽带
主要负责用户与应用的响应操作,及用户与页面产生交互缺点:View 层和 Model 层耦合在一起,Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题
(2)MVVM
MVVM 分为 Model、View、ViewModel:
View :UI视图,负责数据的展示
Model :数据模型,负责存储页面的业务数据,及对相应数据的逻辑操作
ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系
当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步,数据自动同步,开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM
(3)MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 ControllerMVC会造成代码的混乱,MVP 模式解决这一点。
MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新
这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。