Vue
是前端比较火的开发框架,Vue
修改数据后能够立马在页面上反应出最新的修改结果,这种就叫数据响应式,本文讲解一下Vue
响应式原理,理解Vue
背后的逻辑。
有一定开发经验的应该知道如果要修改数据时执行其他业务逻辑,通常需要通过拦截器来实现。
Vue2响应式
Vue2
响应式原理是通过Object.defineProperty
实现,这个方法可以给对象定义属性描述信息,里面有get
和set
的拦截
属性描述
每个JS
对象都有一组描述信息,定义JS
数据能执行一些什么操作。
const obj1 = {
name: "gary",
age: 18
};
const descriptors1 = Object.getOwnPropertyDescriptor(obj1, 'name');
console.log(descriptors1)
输出一些信息,以name
为例,如果自定义有get
和set
还有get
和set
信息:
{
configurable: true, // 是否可以重新定义属性描述符
enumerable: true, // 是否可以迭代,如果配置成false,遍历时没有该属性
value: "gary", // 数据值
writable: true // 是否可以写
}
尝试修改属性描述信息Object.defineProperty
const obj1 = {
name: "gary",
age: 18,
};
Object.defineProperty(obj1, "name", {
configurable: false,
enumerable: false,
value: "gary",
writable: false,
});
for (const key in obj1) { // 遍历的时候没有name了,实际可以读取到name
console.log(key);
}
有了基础之后,可以尝试解读Vue2
响应式原理了
响应式原理
Vue2
在data
函数返回数据中,修改对象定义,插入依赖搜集与触发依赖的代码
const obj = {name:'gary', age: 19}
const objCache = {...obj}
for(const key in obj) {
Object.defineProperty(obj, key, {
get(){
console.log(`拦截get ${key},搜集依赖`)
return objCache[key]
},
set(value) {
console.log(`拦截set ${key} = ${value},触发依赖`)
objCache[key] = value
}
})
}
obj.name
obj.age
obj.name = 'gary1'
obj.age = 18
缺点
- 由于是遍历对象,因此性能相对要差一点
- 对象新增的属性由于没有重新定义过属性,因此不能响应
Vue3响应式
Vue2
存在一些缺点,因此Vue3
诞生了,性能更优秀,但是因为使用Proxy
,部分很老的浏览器不支持,目前老浏览器基本都退出市场了。
Proxy介绍
Proxy
对象包装另一个对象并拦截诸如读取和写入属性和其他操作,和前面的defineProperty
似乎有点类似,不过Proxy
代理整个对象的操作,不用针对每个属性去做包装,但是如果属性是对象,在访问到该属性的时候还是要再次用Proxy
代理子对象的访问。
注意原始对象和Proxy
对象实际是两个对象,无论修改原始对象还是Proxy
对象属性,都会影响原始对象的值,但是如果Proxy
中定义有响应代码,这些响应代码将不会执行,这点和Object.defineProperty
是不一样的。
const obj = {}
const handler = {} // handler不做任何处理
const proxy = new Proxy(obj, handler)
proxy.name = 'gary'
proxy.age = 19
console.log(obj) // 修改proxy会自动反应到原始对象上,直接穿透过去
Proxy
的Handler
支持的方法以及触发时机如下:
内部方法 | Handler 方法 | 触发时机 |
---|---|---|
[[Get]] |
get |
读取属性:obj.name |
[[Set]] |
set |
写入属性: obj.name = '' |
[[HasProperty]] |
has |
in 操作符 |
[[Delete]] |
deleteProperty |
delete 操作符 |
[[Call]] |
apply |
函数调用(仅函数) |
[[Construct]] |
construct |
new 操作符(函数) |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor, for..in , Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in , Object.keys/values/entries |
响应式原理
我们用Proxy
来模拟对象访问过程,Vue3
中叫Effect
(副作用)
const obj = {name:'gary', age: 19}
const handler = {
get(target, key){
console.log(`拦截get ${key},搜集依赖`)
return target[key]
},
set(target, key, value) {
console.log(`拦截set ${key} = ${value},触发副作用`)
target[key] = value
}
}
const proxy = new Proxy(obj, handler)
obj.name
obj.name = 'gary1' // 修改生效不过不会触发副作用
proxy.name
proxy.name = 'gary2' // 修改生效触发副作用
至此理解了Vue
响应式原理,方便在使用Vue
的时候大体了解其运行机制。
关于Reflect
上面的写法在普通情况下似乎没有什么问题,不过如果有继承关系且自定义了getter
的时候运行就有点问题了。
const obj = {
name: "gary",
age: 19,
get info() {
return `${this.name}: ${this.age}`
}
};
const handler = {
get(target, key) {
console.log(`拦截get ${key},搜集依赖`);
return target[key];
},
set(target, key, value) {
console.log(`拦截set ${key} = ${value},触发副作用`);
target[key] = value;
},
};
const proxy = new Proxy(obj, handler);
const child = {
__proto__: proxy, // 原型继承
name: 'child'
}
child.info // 'gary: 19' 输出不正确
如果用普通函数没有问题,因为obj
使用了代理,代理了getter
方法引起的问题,需要修改:
const obj = {
name: "gary",
age: 19,
get info() {
return `${this.name}: ${this.age}`
}
};
const handler = {
get(target, key, receiver) {
console.log(`拦截get ${key},搜集依赖`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) { // target:原始对象, key:对象属性, value:对象值, receiver:实际对象,this指向
console.log(`拦截set ${key} = ${value},触发副作用`);
return Reflect.set(target, key, value, receiver)
},
};
const proxy = new Proxy(obj, handler);
const child = {
__proto__: proxy,
name: 'child'
}
child.info // 'child: 19' 输出正确
这就是比较完善的写法,用Reflect
来写入和读取原始对象数据。