在学习核心原理前,我们先了解两个概念:
在官方文档有这么一段话:
当你把一个普通的JavaScript对象传入Vue实例作为
data
选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty
把这些属性全部转为getter/setter。这些getter/setter对用户来说是不可见的,但是内部它们让Vue能够追踪依赖,在属性被访问和修改时通知变更。
实例一:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据响应式原理title>
head>
<body>
<div id="app">
Hello Vue
div>
<script>
//模拟Vue实例中的data选项
let data={
msg:'Hello Vue'
}
//模拟Vue的实例
let vm={
};
//数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作
Object.defineProperty(vm,'msg',{
//可枚举(即可被遍历)
enumerable:true,
//可配置(可以使用delete删除,可以通过defineProperty重新定义)
configurable:true,
//当获取值时执行
get(){
console.log('getter:',data.msg);
return data.msg;
},
//当设置、更新msg变量时执行
set(newValue){
console.log("setter:",newValue);
if(data.msg===newValue){
return;//前后数据相同,则不用做操作DOM的多余操作
}
data.msg=newValue;
document.querySelector("#app").textContent=newValue;
}
})
//测试setter
vm.msg="Hello 响应式原理";
//测试getter
console.log(vm.msg);
script>
body>
html>
上面实例只是对msg
这个属性实现了响应式,那Vue中data选项有多个属性,怎么做到让它们都成为响应式呢?
示例2:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据响应式原理title>
head>
<body>
<div id="app">
Hello Vue
div>
<script>
//模拟Vue实例中的data选项
let data={
msg:'Hello Vue',
count:0
}
//模拟Vue的实例
let vm={
};
function defineProperties(data){
//循环给每个属性使用Object.defineProperty()
Object.keys(data).forEach(element => {
//数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作
Object.defineProperty(vm,element,{
//可枚举(即可被遍历)
enumerable:true,
//可配置(可以使用delete删除,可以通过defineProperty重新定义)
configurable:true,
//当获取值时执行
get(){
console.log('getter:',data[element]);
return data[element];
},
//当设置、更新msg变量时执行
set(newValue){
console.log("setter:",newValue);
if(data[element]===newValue){
return;//前后数据相同,则不用做操作DOM的多余操作
}
data[element]=newValue;
document.querySelector("#app").textContent=newValue;
}
})
});
}
//执行该函数,使每个属性添加响应式
defineProperties(data);
//测试setter
vm.msg="Hello 响应式原理";
//测试getter
console.log(vm.msg);
script>
body>
html>
Vue2.x的数据响应式核心是Object.defineProperty
,
而Vue3.x核心是Proxy
。两者相比,Proxy的效率更高,原因是Proxy是直接监听对象,而defineProperty是监听每个对象里的属性。Proxy是ES6新增的,IE浏览器不支持。
示例:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy数据响应式原理title>
head>
<body>
<div id="app">
Hello Vue
div>
<script>
//模拟Vue实例中的data选项
let data={
msg:'Hello Vue',
count:0
}
//模拟Vue实例
//第一个参数data就是我们要代理的对象
let vm=new Proxy(data,{
//执行代理行为的函数
//当访问vm的成员会执行
//target其实就是我们的代理对象data
get(target,key){
console.log('getter,key:',key,'-',target[key]);
return target[key];
},
set(target,key,newValue){
console.log('set,key:',key,'-',newValue);
if(target[key]===newValue){
return
}
target[key]=newValue;
document.querySelector("#app").textContent=target[key];
}
})
//测试setter
vm.msg="Hello 响应式原理";
//测试getter
console.log(vm.msg);
script>
body>
html>
控制台设置count:
可以看出Proxy实现数据响应式原理要比defineProperty简便,而且Proxy是直接面向整个对象的属性,而defineProperty对一个对象的多个属性都实现数据响应式,则要循环使用
Object.defineProperty
。
注意:发布/订阅模式
和观察者模式
通常被混为一谈,但它们在Vue中有着不同的应用场景。
发布/订阅模式:
我们假定:存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”一个信号,其它任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候可以开始执行,这就是
发布/订阅
模式。
下面我们看看在Vue中,这种模式的应用:
示例:Vue的自定义事件
let vm=new Vue()
//可以为同一事件设置两个监听事件,两者都会被执行
//监听即是在订阅dataChange
vm.$on('dataChange',()=>{
console.log('dataChange')
})
vm.$on('dataChange',()=>{
console.log('xxxx')
})
//发射dataChange即是发布事件
vm.$emit('dataChange');
示例:兄弟组件通信过程
兄弟组件间通信,通常通过设置事件总线$bus
进行通信,
$bus其实就是Vue实例。
let $bus=new Vue();
//组件A中发布消息
sendMess:function(){
$bus.$emit('send_mess',{
mess:"这是组件A发布的消息" });
}
//组件B中订阅消息,在created钩子函数设置订阅
created:function(){
$bus.$on('send_mess',(params)=>{
console.log("在这里处理接受到的消息")
})
}
下面自己实现订阅/发布模式:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发布/订阅模式title>
head>
<body>
<script>
class EventEmitter{
constructor(){
//this.subs是一个对象,相当于this.subs={},记录着每个事件的订阅函数,即回调函数
//this.subs={'click':[fun1,fun2...]}
//Object.create()的参数是它的原型链,null即是没有原型链
this.subs=Object.create(null);
}
//注册函数,注册即表明是订阅,将它追加到this.subs中
$on(eventType,handler){
//如果this.subs中有eventType这种类型的事件了,就不改变,如果没有这种事件
//就初始化为空数组,然后将handler追加进数组
this.subs[eventType]=this.subs[eventType]||[];
this.subs[eventType].push(handler);
}
//当触发了发布函数,那么就将this.subs存储着对应类型的函数执行
$emit(eventType){
//如果不为空,那就进入执行函数即可
if(this.subs[eventType]){
this.subs[eventType].forEach(func => {
func();
});
}
}
}
let em=new EventEmitter();
//第一次订阅
em.$on('getData',()=>{
console.log("订阅getData(),当发布时,执行我这个回调函数")
})
//第二次订阅
em.$on('getData',()=>{
console.log('我也要订阅getData,哈哈哈');
})
//3秒后我将发布getData
setTimeout(()=>{
em.$emit('getData');
},3000)
script>
body>
html>
3秒后,指定订阅者的回调函数:
上面实现了EventEmitter类,只有一个subs属性和两个方法。subs中记录着订阅者的函数,在发布信息时,就执行订阅者的函数即可。
比如:
在华为要发布新手机时,很多爱国者就提前订阅了这款手机,然后华为后台是记录着每个订阅手机的用户的信息的(subs),当手机一发布,那么就逐一根据用户填写的信息(即上面的回调函数)给订阅者发货。
观察者模式:
每个观察者必须有一个update()方法,当事件发生时,执行观察者的update()。
- subs数组:存储所有的观察者
- addSub():添加观察者
- notify():当事件发生时,调用所有观察者的update(),达到通知目的。
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>观察者模式</title>
</head>
<body>
<script>
//目标-发布者
class Dep{
constructor(){
//记录所有订阅者
this.subs=[];
}
addSub(sub){
//sub必须有update才是合格的订阅者
if(sub&&sub.update){
this.subs.push(sub);
}
}
notify(){
//通知,即执行每个sub的update()
this.subs.forEach(sub=>{
sub.update();
})
}
}
//观察者-订阅者
class Watch{
update(){
console.log('update');
}
}
//测试
//新建观察者
let watch=new Watch();
//新建发布者
let dep=new Dep();
//给发布者添加观察者
dep.addSub(watch);
//3秒后发布通知
setTimeout(()=>{
dep.notify();
},3000)
</script>
</body>
</html>
区别: