概述
在个人看来,vue响应式原理实际上就是对数据进行劫持,也称之为代理,具体的效果为:我们能够监测到数据被读取或改变的时机,并在这个时机执行处理相应情况的代码。
vue2.X
vue2.X使用Object.defineProperty()方法来实现数据劫持,该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
该方法接收三个参数,语法为Object.defineProperty(obj, prop, descriptor) ,其中obj是要定义属性的对象,prop是要定义或修改的属性的名称,descriptor是要定义或修改的属性描述符。在descriptor中我们可以定义getter和setter,当我们访问obj.prop时会调用get函数,当我们为obj.prop赋值时会调用set函数。
听起来有点抽象,但结合代码看一下就很明白了。
let obj = {}
let value = 1
Object.defineProperty(obj, 'prop', {
get(){
return value++
},
set(newValue){
value = newValue * 10
}
})
console.log(obj.prop) // 调用get函数,返回值就是obj.prop的值
console.log(obj.prop)
obj.prop = 10 //调用set函数,参数为10
console.log(obj.prop)
console.log(obj.prop)
//控制台输出依次为 1 2 100 101
上述代码仅仅是对prop属性进行了劫持,要实现vue的响应式,我们需要对一个对象的所有属性均进行劫持,当属性的值是object类型时,还需要对该值进行递归操作,对它的所有子属性进行劫持。
具体实现代码如下,当我们想对obj对象进行劫持时,执行observe(obj)即可。
// 为object的key属性设置getter和setter
function toReactive(obj, key, value){
// 这里形成了一个闭包,value的值不会被销毁,会一直存在于内存中
Object.defineProperty(obj, key, {
get(){
// 模拟数据被读取时要执行的代码
console.log(`读取属性${key}, 执行相关代码...`)
return value
},
set(newValue){
// 如果新值和旧值相同,直接return
if(newValue === value){
return
}
// 模拟数据被改变时要执行的代码
console.log(`设置属性${key}的值为${newValue}, 执行相关代码...`)
// 如果新值是一个对象,则调用observe方法,对所有属性进行劫持
if(typeof newValue === 'object'){
observe(newValue)
}
value = newValue
}
})
}
// 对obj的所有属性进行劫持
function observe(obj){
if(typeof obj === 'object'){
// 遍历所有属性
for(let key in obj){
// 该属性的值是对象,递归调用observe函数
if(typeof obj[key] === 'object'){
observe(obj[key])
}
// 为该属性设置getter和setter
toReactive(obj, key, obj[key])
}
}
}
// 测试函数
function test(){
// 定义测试对象
const data = {
id: 1,
student:{
name: 'tom',
age: 10
}
}
// 对data对象的所有属性进行劫持
observe(data)
// 读取和更改data的属性
data.id
data.id = 2
console.log('--------------------分隔符1-------------------')
data.student.age
data.student.age = 20
console.log('--------------------分隔符2-------------------')
data.student
data.student = {
name: 'li',
age: 30,
}
console.log('--------------------分隔符3-------------------')
data.student.name
data.student.name = 'jack'
console.log('--------------------分隔符4-------------------')
data.newProp = 'new' // 不能劫持到新增属性
data.newProp
console.log('--------------------分隔符5-------------------')
console.log(data.id)
console.log(data.student.name)
console.log(data.student.age)
}
test()
ps:我感觉比较难理解的就是那个toReactive()方法内部作用域形成的闭包。
代码中测试函数test()的输出如下,可以看到,使用Object.defineProperty()对数据进行劫持时,不能劫持到新增属性。
读取属性id, 执行相关代码...
设置属性id的值为2, 执行相关代码...
--------------------分隔符1-------------------
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
读取属性student, 执行相关代码...
设置属性age的值为20, 执行相关代码...
--------------------分隔符2-------------------
读取属性student, 执行相关代码...
设置属性student的值为[object Object], 执行相关代码...
--------------------分隔符3-------------------
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
读取属性student, 执行相关代码...
设置属性name的值为jack, 执行相关代码...
--------------------分隔符4-------------------
--------------------分隔符5-------------------
读取属性id, 执行相关代码...
2
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
jack
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
30
vue3.X
vue3.X的响应式是通过代理对象来实现的。
学过软件设计模式的话,应该对代理对象这个词非常熟悉。设计模式中有一种结构型模式叫做代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate,它是一种对象结构型模式。
假如我们要监测数据对象data,当我们想读取或改变data的属性时,我们不直接引用data对象,而是创建一个data的代理对象dataProxy,在dataProxy中定义get和set函数,从而访问data对象里数据并实现数据的劫持。
具体代码如下,执行observe(data)的返回值就是data的代理对象。
// 返回obj的代理对象
function observe(obj){
for(let key in obj){
if(typeof obj[key] === 'object'){
// 如果属性值是对象类型,则递归处理
obj[key] = observe(obj[key])
}
}
return new Proxy(obj, {
get(target, key){
console.log(`读取属性${key}, 执行相关代码...`)
return target[key]
},
set(target, key, value){
if(target[key] === value){
return
}
console.log(`设置属性${key}的值为${value}, 执行相关代码...`)
target[key] = (typeof value === 'object') ? observe(value) : value
return true
}
})
}
// 测试函数
function test(){
// 定义测试对象
let data = {
id: 1,
student:{
name: 'tom',
age: 10
}
}
// 获取data的代理对象
data = observe(data)
// 读取和更改data的属性
data.id
data.id = 2
console.log('--------------------分隔符1-------------------')
data.student.age
data.student.age = 20
console.log('--------------------分隔符2-------------------')
data.student
data.student = {
name: 'li',
age: 30,
}
console.log('--------------------分隔符3-------------------')
data.student.name
data.student.name = 'jack'
console.log('--------------------分隔符4-------------------')
data.newProp = 'new' // 可以劫持到新增属性
data.newProp
console.log('--------------------分隔符5-------------------')
console.log(data.id)
console.log(data.student.name)
console.log(data.student.age)
}
test()
相比于Object.defineProperty(),使用Proxy类来劫持数据,写法要简单的多。
程序输出如下,可以看到,Proxy可以劫持到新增属性的变化。
读取属性id, 执行相关代码...
设置属性id的值为2, 执行相关代码...
--------------------分隔符1-------------------
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
读取属性student, 执行相关代码...
设置属性age的值为20, 执行相关代码...
--------------------分隔符2-------------------
读取属性student, 执行相关代码...
设置属性student的值为[object Object], 执行相关代码...
--------------------分隔符3-------------------
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
读取属性student, 执行相关代码...
设置属性name的值为jack, 执行相关代码...
--------------------分隔符4-------------------
设置属性newProp的值为new, 执行相关代码...
读取属性newProp, 执行相关代码...
--------------------分隔符5-------------------
读取属性id, 执行相关代码...
2
读取属性student, 执行相关代码...
读取属性name, 执行相关代码...
jack
读取属性student, 执行相关代码...
读取属性age, 执行相关代码...
30