Vue响应式原理
1、课程目标
模拟一个最小版本的Vue
响应式原理在面试的常问问题
实际项目中出现问题的原理层面的解决
给Vue
实例新增一个成员是否是响应式的?
给属性重新赋值成对象,是否是响应式的?
为学习Vue
源码做铺垫。
2、数据驱动
在实现整个Vue
响应式代码之前,我们先来了解几个概念。
第一个:数据驱动
第二个:响应式的核心原理
第三个:发布订阅模式和观察者模式
我们先来看一下数据驱动的内容:
数据响应式,双向绑定,数据驱动(我们经常看到这几个词)
数据响应式:数据模型仅仅是普通的JavaScript
对象,而当我们修改数据时,视图会进行更新,避免了频繁的DOM
操作,提高开发效率,这与Jquery
不一样,Jquery
是频繁的操作Dom
双向绑定:
数据改变,视图改变,视图改变,数据也随之改变( 通过这句话,我们可以看到在双向绑定中是包含了数据响应式的内容)
我们可以使用v-model
在表单元素上创建双向数据绑定
数据驱动是Vue
最独特的特性之一
开发过程中仅仅需要关注数据本身,不需要关心数据是如何渲染到视图中的。主流的MVVM
框架都已经实现了数据响应式与双向绑定,所以可以将数据绑定到DOM
上。
3、响应式的核心原理
3.1 Vue2.x
响应式原理
关于Vue2.x
的响应式原理在官方文档中也有介绍。
https://cn.vuejs.org/v2/guide/reactivity.html
在该文档中,我们注意如下一段内容:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,
Vue 将遍历此对象所有的 property,
并使用 Object.defineProperty
把这些 property 全部转为 getter/setter。
Object.defineProperty 是 ES5 中一个无法 shim 的特性,
这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
通过以上的文字,我们可以看到,在Vue2.x
中响应式的实现是通过Object.defineProperty
来完成的,注意该属性无法降级(shim)处理,所以Vue
不支持IE8
以及更低版本的浏览器的原因。
下面我们来看一下Object.defineProperty
基本使用
修改data
对象中的msg
属性的值,实现视图的更新.(这也就是我们所说的响应式)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>defineProperty</title>
</head>
<body>
<div id="app">hello</div>
<script>
//模拟Vue中的data选项(当)
let data = {
msg: "hello",
};
//模拟Vue的实例
let vm = {
};
//数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作
Object.defineProperty(vm, "msg", {
//可枚举(可遍历)
enumerable: true,
//可配置(可以使用delete删除,可以通过defineProperty重新定义)
configurable: true,
//当获取值的时候执行
get() {
console.log("get:", data.msg);
return data.msg;
},
// 当设置值的时候执行
set(newValue) {
console.log("set:", newValue);
//设置的值与原有的值相同,则没有更改,所以不做任何操作
if (newValue === data.msg) {
return;
}
data.msg = newValue;
//数据更改,更新DOM的值
document.querySelector("#app").textContent = data.msg;
},
});
//测试
//执行set操作
vm.msg = "abc";
//执行get操作
console.log(vm.msg);
</script>
</body>
</html>
在进行测试的时候,可以在浏览器的控制台中,输入vm.msg
进行测试。
在上面的代码中,我们是将一个对象中的属性转换成了getter/setter
的形式,那么这里我们还有一个问题:
如果有一个对象中多个属性需要转换getter/setter
,那么应该如何处理?
我们可以通过循环遍历的方式,将对象中的多个属性转换成getter/setter
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>defineProperty多个属性</title>
</head>
<body>
<div id="app">hello</div>
<script>
//模拟Vue中的data选项
let data = {
msg: "hello",
count: 10,
};
//模拟Vue实例
let vm = {
};
proxyData(data);
function proxyData(data) {
//遍历data对象中的所有属性
Object.keys(data).forEach((key) => {
// 把data中的属性,转换成vm的setter/getter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() {
console.log("get", key, data[key]);
return data[key];
},
set(newValue) {
console.log("set:", key, newValue);
if (newValue === data[key]) {
return;
}
data[key] = newValue;
document.querySelector("#app").textContent = data[key];
},
});
});
}
vm.msg = "hello world";
console.log(vm.msg);
</script>
</body>
</html>
在上面的代码中,我们通过循环的方式给data
对象中的每个属性添加了getter/setter
.
这里我们只是在视图中展示了msg
属性的值,如果想展示count
属性的值,可以在浏览器的控制台中,通过vm.count=20
这种形式来展示,当然,在后期的课程中我们会分别展示出msg
与count
属性的值,
3.2 Vue3
响应式原理
Vue3
的响应式原理是通过Proxy
来完成的。
Proxy
直接监听对象,而非属性,所以将多个属性转换成getter/setter
的时候,不需要使用循环。
Proxy
是ES6
课程中新增的,IE
不支持
Proxy
实现响应式的基本代码如下(该代码的功能与上面所讲解的是一样的):
<!DOCTYPE html>
<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</div>
<script>
//模拟Vue中的data选项
let data = {
msg: "hello",
count: 0,
};
//模拟Vue实例
//为data创建一个代理对象vm,这样就可以通过vm.msg来获取data中的msg属性的值,而这时候会执行get方法
let vm = new Proxy(data, {
// 当访问vm的成员时会执行
//target表示代理的对象(这里为data对象),key表示所代理的对象中的属性
get(target, key) {
console.log("get key:", key, target[key]);
return target[key];
},
//当设置vm的成员时会执行
set(target, key, newValue) {
console.log("set key:", key, newValue);
if (target[key] === newValue) {
return;
}
target[key] = newValue;
document.querySelector("#app").textContent = target[key];
},
});
//测试
vm.msg = "aaaa";
console.log(vm.msg);
</script>
</body>
</html>
通过以上的代码我们发现使用Proxy
的代码是给对象中所有属性添加getter/setter
,而不需要通过循环的方式来实现,所以代码更加的简洁。
4、发布订阅模式
发布订阅模式:订阅者,发布者,信号中心
我们假定,存在一个“信号中心”,某个任务执行完成,
就向信号中心"发布"(publish)一个信号,
其它任务可以向信号中心“订阅”(subscribe)这个信号,
从而知道什么时候自己可以开始执行。
这就叫做"发布/订阅模式"(publish-subscribe pattern)
家长向学生所在的班级订阅了获取学生考试成绩的事件,当老师公布学生的成绩后,就会自动通知学生的家长。
在整个案例中,学生所在的班级为信号中心,老师为发布者,家长为订阅者
Vue
的自定义事件就是基于发布订阅模式来实现的。
下面通过Vue
中兄弟组件通信过程,来理解发布订阅模式
// eventBus.js
// 事件中心
let eventHub=new Vue()
//ComponentA.vue
addTodo:function(){
//发布消息(事件)
eventHub.$emit('add-todo',{
text:this.newTodoText})
this.newTodoText=''
}
//ComponentB.vue
//订阅者
created:function(){
//订阅消息(事件)
eventHub.$on('add-todo',this.addTodo)
}
通过以上代码,我们可以理解发布订阅模式中的核心概念。
下面我们模拟Vue
中的自定义事件的实现
下面我们先来做一个基本的分析:
先来看如下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 自定义事件</title>
</head>
<body>
<script