第 51 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

发布于 2022-09-14 10:11:08 字数 193 浏览 198 评论 11

Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;

Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象。

Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

梦魇绽荼蘼 2022-05-04 13:56:39

这里其实提到了很多,简单来说,就是 length 尽量不能去改写。

  1. length 在规范中不允许改写,configurable = false
  2. a.length = 100,等于增加了 100 个属性,需要对每个属性进行监听,这样一来,性能上所有问题,使用 push 或者 pop 等重写方法更加简单
爱冒险 2022-05-04 13:56:39
  1. Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  2. Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
  3. Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

Object.defineProperty是通过遍历整个对象来劫持的,所以原本数组的监听也需要遍历才能劫持,但是数组劫持遇到类似let list = new Array(1000000)的情况,性能开销太大。所以vue源码才会重写数组的方法和监听length来进行劫持。
另外vue3 proxy用的是懒劫持,不是一上来就递归遍历整个对象,只是兼容性方面差点。

海螺姑娘 2022-05-04 13:56:28

前面补充的挺详细的了,这里就补充一下数组哪方面不能监控

  • 不能监听动态length的变化
    例如,arr = [1],直接更改arr[10] = 20,这样就监听不到了,因为length在规范不允许重写,而arr[0]直接更改是可以监听到的。
  • 数组方法使用不能监听到数组的变更,例如push
    这也是为什么vue重写这些方法的原因
九命猫 2022-05-04 13:56:10

Object.defineProperty可以监听到数组下标的变化的
只是常规的使用数组时,很少会用下标去操作,而使用api更符合我们的编码习惯

梦毁影碎の 2022-05-04 13:52:25

@julyL 确实,这块和 defineProperty 的表现是一致的,理解错误,感谢大佬

小…红帽 2022-05-04 13:35:45

@zhl1232 proxy并不是递归的,你的代码中执行proxy.b.c1只触发了 proxy对象的get,但并没有触发proxy.b的get

莫相离 2022-05-04 13:30:57

之前正好写过个类似的东西,大概就是 Object.defineProperty 的监听只会根据第二个参数的 prop 来判断,甚至内存指针指向同一数据都不会监听,而且不会往下递归。

let a = {
  b:{c1:1,c2:2},
}

Object.defineProperty(a, 'b', {
  get: function() {
    console.log(a.b);
  }
})
// Maximum call stack size exceeded
// 循环调用,肯定是爆栈了
let a = {
  b:{c1:1,c2:2},
}

let temp = a.b

Object.defineProperty(a, 'b', {
  get: function() {
    return temp
  }
})
temp.c1 = 99
console.log(a.b, temp);  // {c1: 99, c2: 2} {c1: 99, c2: 2}
// 两个对象指针还是指向同一个数据,但是存取描述符只会根据 obj 和 prop 来判断
let a = {
  b:{c1:1,c2:2},
}

let temp = a.b

Object.defineProperty(a, 'b', {
  get: function() {
    return temp
  },
  set: function(newVal) {
    console.log(newVal);
  }
})
temp.c1 = 99  // 并没有调用 setter

console.log(a.b, temp);  // {c1: 99, c2: 2} {c1: 99, c2: 2}
// 还是说明存取描述符只会根据 obj 和 prop 来判断

知道了存取描述符只会根据 obj 和 prop 来判断,那监听数据就可以这样写

void (function() {
  function watch(obj, name) {
    let value = obj[name]

    Object.defineProperty(obj, name, {
      get: function() {
        return value
      },
      set: function(newVal) {
        value = newVal
      }
    })
    value ? (obj[name] = value) : ''   // 拿到外面处理,不然会循环 setter
  }

  return (this.watch = watch)
})()

let a = { b: 11 }

watch(a, 'b')

console.log(a.b)   // 11
a.b = 12
console.log(a.b)   // 12

需要做什么处理,在 watch 里再加个回调函数就可以了。

在 es6 还提供了一种新的监控方式。
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,Proxy 可以理解成,在目标对象之前架设一层“拦截”。

let a = {
  b:{c1:1,c2:2},
}
let proxy = new Proxy(a, {
  get: function(obj, prop) {
    console.log(obj, prop);  // {b:{c1:1,c2:2}} , 'b'
    return obj[prop]
  }
})

console.log(proxy.b.c1);  // 1


let p2 = proxy.b
console.log(p2.c1);   // 1

可以发现,proxy 的监控方式是往下递归的,不只 prop,prop 下面的对象也可以触发。
而且因为是需要要 Proxy 对象来触发,所以拷贝 Proxy 对象也没影响。

2022-05-04 12:43:41

1, 数据的变化是通过getter/setter来追踪的。因为这种追踪方式,有些语法中,即便是数据发生了变化,vue也检查不到。比如 向Object添加属性/ 删除Object的属性。

2,检测数组的变化,因为只是拦截了unshift shift push pop splice sort reverse 这几个方法,所以像

list[0] = 4
list.length = 0

检测不到

凉世弥音 2022-05-04 10:11:35

感觉 Proxy 行为有些复杂,一个操作会引起很多中间操作出现,这部分有哪位盆友讲解一下或者指个路,介绍一下Vue是怎样处理这些干扰信息的吗?

let a = [1];

let newA = new Proxy(a, {
    get(target, p, receiver) {
        console.log('get', p);
        return Reflect.get(target, p, receiver);
    },
    set(target, p, value, receiver) {
        console.log('set', p);
        return Reflect.set(target, p, value, receiver);
    }
});

newA.push(1);
console.log('===');
newA[2] = 2;
newA.length;
console.log('===');
newA.length = 100;
console.log('===');
newA.shift();

输出

get push
get length
set 1
set length
===
set 2
get length
===
set length
===
get shift
get length
get 0
get 1
set 0
get 2
set 1
set length
多像笑话 2022-05-04 05:28:29

楼上讲的很细致啦!有一点我补充一下:

Object.defineProperty本身有一定的监控到数组下标变化的能力:
Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。具体我们可以参考 《记一次思否问答的问题思考:Vue为什么不能检测数组变动》这篇文章,文章底部配图中有尤大大的严肃回复截图; 下方的讨论区也很值得大家下去看一看,有对于 for / forEach / for .. in .. 几个循环方式的讨论。

关于 Vue 3.0 的其他信息我们可以参考 尤大大发布的 Vue 3.0 新特性预览PPT

另外补充一些其他资料给大家:

妳是的陽光 2022-05-03 19:10:29

Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

而要取代它的Proxy有以下两个优点;

可以劫持整个对象,并返回一个新对象
有13种劫持操作

摒弃Object.defineProperty,基于 Proxy 的观察者机制探索

~没有更多了~

关于作者

醉南桥

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

1CH1MKgiKxn9p

文章 0 评论 0

ゞ记忆︶ㄣ

文章 0 评论 0

JackDx

文章 0 评论 0

信远

文章 0 评论 0

yaoduoduo1995

文章 0 评论 0

霞映澄塘

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文