作为Vue的核心特性之一,响应式可以说是Vue的灵魂,是数据驱动视图的关键所在,通过数据响应式,能够将对JavaScript数据对象的修改作用于视图,驱动视图进行更新,这使得开发变得尤为简单方便
在使用Vue响应式的同时,理解其原理更能帮助我们在开发中得心应手,Vue的响应式原理大致上可以分为数据劫持 和发布-订阅模式两部分
Vue2.x 数据劫持
在Vue2.x版本中,数据劫持通过 Object.defineProperty
方法进行实现,通过对data 对象上的数据进行递归遍历,对每一个属性设置 getter/setter
方法,通过getter
返回属性值,而setter
方法负责监听数据变化,一旦数据发生修改就进行视图的更新
下面我们来简单模拟一下Vue2.x的数据劫持:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
// 模拟data
const data = {
name: 'Jerry',
age: '18',
height: '178'
}
//模拟vue实例
const Vm = {}
//遍历data中的每一个属性
Object.keys(data).forEach(key => {
//进行数据劫持
Object.defineProperty(Vm,key,{
//设置是否可枚举,即是否可以被遍历
enumerable: true,
//设置属性是否可以删除或编辑
configurable: true,
get() {
return data[key]
},
set(newValue) {
if(newValue === data[key]) {
return
}
data[key] = newValue
document.querySelector('#app').innerText = data[key]
}
})
})
</script>
</body>
</html>
Object.defineProperty
的实现方式有一定的缺陷,需要注意以下几点:
- 直接对对象属性的添加、删除并不能被监视到,这是因为
getter/setter
的方式只能监测到属性的修改,并不能监视属性的添加或删除 - 对于数组类型的数据,
Object.defineProperty
不能监听到数组元素的变化,需要对数组方法进行重写,Vue2.x对于大部分数组方法都进行了重写,例如push pop shift 等,但仍然存在一些情况监视不到,比如直接通过索引修改数组中某一元素,或者直接对数组长度进行更改
Vue3.x 数据劫持
由于ES6中引入了新特性Proxy
,Vue3.x 的数据劫持方式跟2.x发生了很大的改变,Proxy
通过创建一个对象的代理,从而实现对对象属性的监视和拦截,需要注意的是Proxy
的代理是以对象为单位,而不是其中的某一个属性(这也是为什么Vue3.x版本中对于基本类型数据的响应式仍然需要将其包装为对象的原因)
同样我们来模拟一下Vue3.x的数据劫持
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
//模拟数据
const obj = {
name: 'Jerry',
age: 18,
height:178
}
const Vm = new Proxy(obj, {
get(target,key){
return target[key]
},
set(target,key,newValue){
if(target[key] === newValue){
return
}
target[key] = newValue
}
})
Vm.name = 'Tom'
console.log(Vm.name)
</script>
</body>
</html>
发布-订阅者模式
发布者-订阅者模式大致流程是,监听器负责监听数据状态变化, 当数据发生变化,就通知对应的订阅者,订阅者再进行业务逻辑的处理
而在Vue的响应式中,首先通过上述方法要对数据进行劫持监听,构建一个监听器Observer
,它负责对所有数据进行监听,当属性发生变化时,就通知订阅者Watcher
,在Vue中同一个数据我们可能在很多地方都会使用,所有订阅同一数据的Watcher
通常存在多个,需要构建一个订阅器Dep
来存储这些Watcher
,每一个数据都会维护一个自身的Dep
对象
指令解析器Compile
负责对节点进行扫描,简单来说就是知道哪个数据在哪个位置,然后初始化Watchwer
并将更新视图的方法加入到对应的Watcher
中,此时当Watcher
接收到相应属性的变化,就会执行对应的更新函数,从而更新视图
接下来让我们实现一下这几个功能:
首先是Observer
,它主要由遍历属性的方法walk
和添加响应式的方法defineReactive
组成
JavaScript
//Observer
class Observer {
constructor(data) {
// 遍历 data
this.walk(data)
}
// 遍历 data 转为响应式
walk(data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach((key) => {
// 转为响应式
this.defineReactive(data, key, data[key])
})
}
// 转为响应式
defineReactive(obj, key, value) {
// 对于复杂数据类型,递归调用
this.walk(value)
const self = this
// 每个数据维护一个自身的Dep对象
let dep = new Dep()
Object.defineProperty(obj, key, {
//设置是否可枚举,即是否可以被遍历
enumerable: true,
//设置属性是否可以删除或编辑
configurable: true,
// 获取值
get() {
// 将Watcher加入对应的Dep
if(Dep.target){
dep.addSub(Dep.target)
}
return value
},
// 设置值
set(newValue) {
// 判断旧值和新值是否相等
if (newValue === value) return
// 设置新值
value = newValue
// 赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的
self.walk(newValue)
// 触发通知 更新视图
dep.notify()
},
})
}
}
接下来简单实现一下Dep
JavaScript
class Dep {
constructor () {
//用于存放Watcher的数组
this.subs = [];
}
//添加Watcher的方法
addSub (sub) {
this.subs.push(sub);
}
//通知所有Watcher进行更新
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
然后是Watcher
JavaScript
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
//这里触发看getter,已经存储了Watcher
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 更新视图
this.cb(this.value)
}
}
总结
在Vue响应式的整个流程中,执行new Vue()
生成Vue对象后, Vue 会调用 _init
函数进行初始化,在 这个过程Data 通过Observer
转换成了getter/setter的形式,实现对数据的劫持
另一方面,在对DOM的解析过程中,Compile
会初始化对应的Watcher
对象,当进行页面渲染时,因为会读取所需对象的值,所以会触发getter
函数从而将Watcher
添加到Dep
对象中进行依赖收集
在数据的值的时候,会触发对应的setter
, setter
通知之前依赖收集得到的 Dep
中的每一个 Watcher
对象,并调用对应的update
方法重新渲染视图