赞
踩
之前我就有一个很愚蠢的问题,对象中直接修改value值就可以改变对象的值,vue2中为什么还要使用Object.defindProperty呢,直接下面的代码就能实现了
- // 定义一个对象
- const obj = {
- name: "pan",
- age: 22
- }
-
- obj.name = "zhang"
- console.log(obj); //{ name: 'zhang', age: 22 }
后来发现直接在对象中修改属性值可以实现单向绑定,但无法实现双向绑定。
在 Vue 2 中,如果直接在对象中修改属性值,Vue 是无法自动检测到属性值的变化并更新相关的视图。这是因为 Vue 2 使用了 Object.defineProperty()
来创建 getter 和 setter,并在 getter 和 setter 中进行依赖追踪和更新通知。只有通过 Vue 提供的特定方法(如 $set
或通过数组的变异方法)去修改对象的属性值,Vue 才能正确地进行依赖追踪和视图更新。
正题开始
vue2中怎么实现对象属性监听?
下面是一个小demo
- const dog = {
- name: '小黄',
- age: 5
- }
-
- function listenAge(obj, key) {
- Object.defineProperty(obj, key, {
- get() {
- console.log(`属性${key}被访问了!`);
- return obj[key];
- },
- })
- }
- listenAge(dog, 'age')
- console.log(dog.age);
上面的代码就可以实现访问dog.age时会执行defineProperty进行对象的监听,但是这里直接return obj[key]会出现死循环,会导致栈溢出,因为为对象定义getter和setter方法时,每次访问或者设置属性值的时候都会再去调用getter和setter方法。因此直接访问或者修改属性值的时候就会导致栈溢出。下面设置属性的时候同理
- const dog = {
- name: '小黄',
- age: 5
- }
-
- function listenAge(obj, key) {
- Object.defineProperty(obj, key, {
- get() {
- console.log(`属性${key}被访问了!`);
- return obj[key];
- },
- set(newVal) {
- obj[key] = newVal
- }
- })
- }
- listenAge(dog, 'age')
- console.log(dog.age);
- dog.age = 6
那我们要怎么去修改呢?
我们可以不直接去访问和修改属性值,使用一个变量或者属性来存储实际的属性值,这样就可以避免栈溢出了。
- const dog = {
- name: '小黄',
- age: 5,
- _age: 5
- }
-
- function listenAge(obj, key) {
- Object.defineProperty(obj, key, {
- get() {
- console.log(`属性${key}被访问了!`);
- return obj['_' + key];
- },
- set(newVal) {
- console.log(`属性${key}被设置新值:${newVal}`);
- obj['_' + key] = newVal
- }
- })
- }
- listenAge(dog, 'age')
- console.log(dog.age); //5
- dog.age = 6
- console.log(dog.age); //6
-
如果第一次没有去设置新值直接去访问age的属性就会出现undefined,所以我们在初始化的时候多设置了一个_age:5,但是这种做法会使代码不优雅,因为时间关系,这里就没有去优化了,有好的解决办法也可以在评论区一起讨论。
这样我们就实现了Vue2中Object.defineProperty的简单demo
但是Object.defineProperty有一个致命的缺点,就是无法监听对象属性的新增和删除
可以使用this.$set和this.$delete解决,这个方法在项目中也经常使用,因为这里主要对比vue2和vue3的双向绑定,就不展开篇幅来讲,有兴趣的话我也会再出一篇文章详细讲讲。
es6新增proxy,Proxy
的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了。
var proxy = new Proxy(target, handler)
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))
handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p
的行为
下面一段代码实现proxy代理
- function reactive(obj) {
- return new Proxy(obj, {
- get(target, key) {
- console.log(`对象属性${key}被访问了`);
- return target[key];
- },
- set(target, key, newVal) {
- console.log(`对象属性${key}被修改成:${newVal}`);
- target[key] = newVal;
- return true;
- }
- });
- }
-
- let obj = reactive({
- name: 'zhangsan',
- age: 18
- });
- console.log(obj); //初始化 reactive 对象并打印它时,并没有实际访问对象的属性,不会触发 get 拦截器
- console.log(obj.name);//会触发get
- obj.age = 13
-
值得注意的是我们在proxy下直接return target[key]
在 Proxy 的 get 拦截器中,直接返回 target[key]
不会导致死循环,因为在这个语境下,target[key]
实际上是在访问原始对象的属性值,而不是再次触发 get 拦截器。当你通过 obj[key]
访问属性时,Proxy 的 get 拦截器会被触发,但在拦截器内部返回 target[key]
时,它实际上是在访问原始对象 obj
中的属性值,并不会再次触发 get 拦截器。因此,这样的写法并不会导致死循环。而如果你在 get 拦截器中返回的是 return obj[key]
,那么就会形成递归,导致死循环。因为在这种情况下,又会触发 get 拦截器,导致无限循环。
在这里我们设置obj.sex = "男";也是会触发get和set的;
新增deleteProperty可以监听删除对象属性
- function reactive(obj) {
- return new Proxy(obj, {
- get(target, key) {
- console.log(`对象属性${key}被访问了`);
- return target[key];
- },
- set(target, key, newVal) {
- console.log(`对象属性${key}被修改成:${newVal}`);
- target[key] = newVal;
- return true;
- },
- deleteProperty(target, proKey) {
- console.log(`删除对象属性${proKey}`);
- // 调用es6 Reflect操作对象的方法
- return Reflect.deleteProperty(target, proKey)
- }
- });
- }
-
- let obj = reactive({
- name: 'zhangsan',
- age: 18
- });
-
-
- delete obj.age
- console.log(obj); // 打印删除后的对象
Proxy可以监听增加和删除属性,还可以监听数组变化,defineProperty则不能,因此Vue2
在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外set
、delete
方法)。
vue中实现双向绑定考虑的因素非常多,肯定不止getter和setter这么简单有兴趣的可以看vue源码
vue2实现在src/core/observer下,链接https://github.com/vuejs/vue/tree/main/src/core/observer
vue3在core/package/reactivity/src下的reactives.ts和ref.ts,链接https://github.com/vuejs/core/tree/main/packages/reactivity
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。