如果你熟悉vue、react
,相信你对数据驱动这个概念不会太陌生,数据驱动的实现主要依托于响应式数据渲染。本篇文章,我们将讲解一下vue中实现响应式数据状态的原理。
在vue2.X
中,实现响应式的方式主要使用了Object.defineProperty
API,而在vue3.X
中采用了Proxy
对象进行了重写,由于vue3.X还不是一个主流版本,而且浏览器对Proxy的支持程度良莠不齐,要使用这个更加高级的API,可能还需要再过一段时间。
一、Object.defineProperty的基本用法
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。我们可以通过这个对象,改变要监测对象属性的get
和set
方法,在获取和设置对象属性的值之前,做一个拦截层,以实现特定的逻辑。
const data = {}
let name = 'antiai'
Object.defineProperty(data, 'name', {
get(){
console.log('get')
return name
},
set(newVal){
console.log('set')
name = newVal
}
})
data.name // 'get'
data.name = 'aiguo' // 'set '
二、Object.defineProperty实现对对象的监听
1、准备要监听改变的数据源
const data = {
name:'antiai',
age: '20'
}
2、定义监听对象属性方法
function observer(target){
if(typeof target !== 'object' || target === null){
// 不是对象或者数组
return target
}
//重新定义各个属性(for in也可以遍历数组)
for(let key in target){
defineReactive(target, key, target[key])
}
}
observer(data)
我们只对对象和数据的改变做监听。
3、重新定义属性,监听起来
function defineReactive(target, key, value){
// 核心API
Object.defineProperty(target, key, {
get(){
return value
},
set(newValue){
if(newValue !== value){
// 设置新值
// 注意,value一值存在闭包中,此处设置完后,在get也是可以获取到的
value = newValue
//触发更新视图
updateView()
}
}
})
}
此处是我们响应式设计的核心。使被监听的对象在使用get、set
操作的时候能够执行特定的逻辑。要注意的是在set
操作的时候,value一值存在闭包中,此处设置完后,在get也是可以获取到的,所以在value = newValue
打上断点,结果如下图所示:

4、触发更新试图
// 触发更新试图
function updateView(){
console.log('视图更新')
}
5、测试代码
data.name = 'aiguo' //视图更新
data.age = 21 //视图更新
在对data
数据源做更新的时候,都执行了监测机制中的相关逻辑。
6、实现对数据源的深度监听
但是还有一个问题。如执行下面的代码
data.info.address = 'shenzhen'
我们发现,并没有触发更新视图操作。这是什么原因呢?因为我们的observer
函数只监听了单层数据,所以当我们碰到多层对象的时候,需要递归的调用defineReactive
实现多层数据监听。
function observer(target){
...
//重新定义各个属性(for in也可以遍历数组)
for(let key in target){
defineReactive(target, key, target[key])
}
}
完善后的代码
function defineReactive(target, key, value){
// 深度监听
+ observer(value)
// 核心API
Object.defineProperty(target, key, {
get(){
return value
},
set(newValue){
if(newValue !== value){
// 深度监听
+ observer(newValue)
// 设置新值
// 注意,value一值存在闭包中,此处设置完后,在get也是可以获取到的
value = newValue
//触发更新视图
updateView()
}
}
})
}
请你注意带+
的两行代码。此处主要用到了递归的操作,没有可以多做解释的。可以通过断点调试一下。现在执行刚刚那段代码,视图函数就被执行了。
7、拓展对数组属性的监听
是不是在上面的操作完成后,我们的响应式操作就完成了呢?你可以下执行下面的的代码。
data.nums.push(4)
发现并没有触发我们的视图更新函数。下面我们就针对数组属性做特殊的处理。原理就是如下:
1、将Array.prototype
备份到oldArrayProperty
中,创建新的数组对象arrProto
,并将其原型设置为oldArrayProperty
,保证arrProto
可以调用到原生的数组的方法。
2、然后对arrProto
创建类方法,在这些方法中可以实现数据视图更新的操作。
3、在调用类方法的时候,一并调用相应的示例方法,保证将数据保存到源对象data
中。
4、在observer
函数中进行调用。
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push','pop','shift','unshift','splice'].forEach(method => {
arrProto[method] = function(){
updateView() // 触发更新视图
oldArrayProperty[method].call(this, ...arguments)
}
})
//监听对象属性
function observer(target){
if(typeof target !== 'object' || target === null){
// 不是对象或者数组
return target
}
+ if(Array.isArray(target)){
+ target.__proto__ = arrProto
+ }
//重新定义各个属性(for in也可以遍历数组)
for(let key in target){
defineReactive(target, key, target[key])
}
}
再次运行上面的代码,就可以触发视图更新了。下面是完整代码:
// 触发更新试图
function updateView(){
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push','pop','shift','unshift','splice'].forEach(method => {
arrProto[method] = function(){
updateView() // 触发更新视图
oldArrayProperty[method].call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value){
// 深度监听
observer(value)
// 核心API
Object.defineProperty(target, key, {
get(){
return value
},
set(newValue){
if(newValue !== value){
// 深度监听
observer(newValue)
// 设置新值
// 注意,value一值存在闭包中,此处设置完后,在get也是可以获取到的
value = newValue
//触发更新视图
updateView()
}
}
})
}
//监听对象属性
function observer(target){
if(typeof target !== 'object' || target === null){
// 不是对象或者数组
return target
}
if(Array.isArray(target)){
target.__proto__ = arrProto
}
//重新定义各个属性(for in也可以遍历数组)
for(let key in target){
defineReactive(target, key, target[key])
}
}
//准备数据源
const data = {
name:'antiai',
age: '20',
info: {
address: '北京'
},
nums: [1,2,3]
}
observer(data)
//测试
data.name = 'aiguo'
data.age = 21
data.info.address = 'shenzhen'
data.nums.push(4)
在vue3.x中使用了proxy
,说明Object.defineProperty
这个方法是有缺点,具体的缺点主要表现在以下几个方面:
1、深度监听需要递归到底,一次性计算量大
2、无法监听新增属性/删除属性,要使用Vue.set、Vue.delete
。
例如下面的测试代码,将不能被监听到。
data.x = '100' // 新增属性,监听不到 ——所以有Vue.set
delete data.name //删除属性,监听不到——所以有Vue.delete
3、无法原生监听数组,需要特殊处理。
网友评论