我们都知道:vue 数据双向绑定是通过 数据劫持 结合 发布者-订阅者模式 的方式来实现的
MVC 和 MVVM 的区别
意义解析:
- M 表示 Modal,模型
- V 表示 View,视图
- C 表示 Controller,控制器
- VM 表示 ViewModal,是 MVVM 相对于 MVC 改进的
核心思想
假设有个需求
在输入框输入times的值后,会在输入的times的值是:文本后实时显示输入结果,点击“确定”按钮会提交数据
MVC 和 MVVM 的处理方式
- MVC 的处理方式:
<html>
<head></head>
<body>
<input id="inputAge" type="number" />
<div id="times">输入的times值是:</div>
<button id="submit">确定</button>
<script>
document.getElementById('confirm').oninput = function(){
let times = document.getElementById('inputTimes').value;
document.getElementById('times').innerHTML = "输入的times值是:" + times;
};
document.getElementById('submit').addEventListener('click',function(){
alert('提交数据:' + { times: times });
})
</script>
</body>
</html>
- MVVM 的处理方式:(采用 vue 框架)
<template>
<input v-modal type="number" />
<div>输入的times值是:{{times}}</div>
<button @click="submit">确定</button>
</template>
<script>
export default {
data(){
return {
times: 1
}
},
methods: {
submit(){
alert('提交数据:' + { times: this.times });
}
}
}
</script>
图中的Model就是data方法返回的{times:1},View是最终在浏览器中显示的DOM,模型通过Observer,Dep,Watcher,Directive等一系列对象的关联,最终和视图建立起关系。总的来说,vue在些做了3件事:
- 通过
Observer对data做监听,并提供了订阅某个数据项变化的能力。 - 把
template编译成一段document fragment,然后解析其中的Directive,得到每一个Directive所依赖的数据项和update方法。 - 通过
Watcher把上述2部分结合起来,即把Directive中的数据依赖通过Watcher订阅在对应数据的Observer的Dep上,当数据变化时,就会触发Observer的Dep上的notify方法通知对应的Watcher的update,进而触发Directive的update方法来更新dom视图,最后达到模型和视图关联起来。
实例图解
Observer 是如何监听到属性的值的变化的?
vue 2.x:Object.defineProperty()
vue 2.x 是通过 Object.defineProperty() 来实现数据劫持的。
那么 Object.defineProperty() 是什么呢?它是 JavaScript 中 Object 的一个原生方法,可以用来控制一个对象属性的一些特有操作,比如读写权、是否可以枚举,这里我们主要先来研究下它对应的两个访问器属性 get 和 set,这块详见文章:JavaScript系列 -- Object 详解
在平常,我们很容易就可以打印出一个对象的属性数据:
var Book = {
name: 'vue权威指南'
};
console.log(Book.name); // vue权威指南
如果想要在执行 console.log(book.name) 的同时,如果想给书名加个书名号《》,那要怎么处理呢?或者说要通过什么监听对象 Book 的属性值。这时候 Object.defineProperty() 就派上用场了,代码如下:
var Book = {}
var name = '';
Object.defineProperty(Book, 'name', {
set: function (value) {
name = value;
console.log('你取了一个书名叫做' + value); // 做一个反馈,表示【修改成功】
},
get: function () {
return '《' + name + '》'
}
})
console.log(Book.name); // 《》
Book.name = 'vue权威指南'; // 你取了一个书名叫做vue权威指南
console.log(Book.name); // 《vue权威指南》
我们通过Object.defineProperty()设置了对象 Book 的 name 属性,同时对其 get 和 set 进行重写操作,get 方法是在读取 name 属性这个值触发的函数,set 方法是在设置 name 属性这个值触发的函数,所以当执行 Book.name = 'vue权威指南' 这个语句时,控制台会打印出 "你取了一个书名叫做vue权威指南",紧接着,当读取这个属性时,就会输出 "《vue权威指南》",因为我们在get函数里面对该值做了加工了。如果这个时候我们执行下下面的语句,控制台会输出什么?
console.log(Book);
是不是跟我们在上面打印 vue 数据长得有点类似,说明 vue 2.x 确实是通过Object.defineProperty()方法来进行 数据劫持 的。
vue 2.x 实现数据双向绑定的两大缺陷
vue 2.x 的第一个缺陷,无法完整地监听数组变化。vue 2.x 只能监听数组的这八种变化方法:
push/pop/unshift/shift/splice/sort/reverse
有两种数组变化方法,vue 2.x 监听不到数组变化:
- arr[indexofitem] = newValue
- arr.length = newLength
vue 2.x 的第二个缺陷,只能劫持对象的属性, 因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要 深度遍历,如果对象新增属性也是要走一遍 Object.defineProperty(),显然能劫持一个完整的对象是更好的选择
vue 3.x:Proxy
ES6新方法,Proxy 可以理解成:在访问目标对象之前加一层“代理”
Proxy() 语法
const proxy = new Proxy(target, handle)
- target:要使用 Proxy 包装的目标对象
- handle:用来定制拦截行为
handle 包含的方法
使用:给对象架起一层代理
const origin = {}
const obj = new Proxy(origin, {
get: function (target, propKey, receiver) {
return '10'
}
});
obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined
代理只会对proxy代理对象生效,而上方的origin原生对象就没有任何效果
get 方法监听到属性变化
语法:get(target, propKey, ?receiver)
参数:
- target:目标对象
- propkey:属性名
- receiver:Proxy 实例本身
先指定对象,再指定属性
const person = {
like: "vuejs"
}
const obj = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name "" + propKey + "" does not exist.");
}
}
})
obj.like // vuejs
obj.test // Uncaught ReferenceError: Prop name "test" does not exist.
- 如果有值则直接返回
- 没有值就抛出一个自定义的错误
vue 3.x 注意的点
- Proxy 不兼容 IE,也没有 polyfill, defineProperty 能支持到 IE9
- 使用 defineProperty 时,我们修改 原来的 obj 对象就可以触发拦截,而使用 proxy,就 必须修改代理对象,即 Proxy 的实例才可以触发拦截
- Proxy 能观察的 类型 比 defineProperty 更丰富
- Object.definedProperty 是劫持对象的属性,新增元素 需要再次 definedProperty。而 Proxy 劫持的是整个对象,不需要做特殊处理