众所周知,在vue2开发面试中总会问到响应式的实现方式
我们都知道是 Object.defineProperty
,使用 getter/setter
对对象的进行 拦截后进行重写对象;而在Vue3.0中则重构为 Proxy
那两者区别在哪?为啥要重构?(还得学😏)
首先我们再来回顾一下 defineProperty
的定义
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
上面给两个词划了重点,对象上,属性,我们可以理解为是针对对象上的某一个属性做处理的
其实这句话已经告诉我们 Object.defineProperty
的局限性了,它只能对对象的某一个属性进行拦截,而不能对整个对象进行拦截
js
data () {
return {
obj: {
a: 1
}
}
}
methods: {
update () {
this.obj.b = 2
}
}
用过vue2的同学都遇到过,当我们给对象新增一个属性的时候,是无法监听到的,这就是它的局限性😒😒
这个其实很好理解,我们先要明白 vue 中 data init 的时机,data init 是在生命周期 created 之前的操作,会对 data 绑定一个观察者 Observer,之后 data 中的字段更新都会通知依赖收集器Dep触发视图更新
总结:
defineProperty
本身,是对对象上的属性做操作,而非对象本身!
顺便复习一下vue2数组是如何实现响应式的呢?
数组变异
由于 JavaScript 的限制,Vue 不能检测以下数组的变动: 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
虽然说索引变更不是 defineProperty
的锅,但新增索引的确是 defineProperty
做不到的,所以就有了数组的变异方法,vue2中对数组的变异方法进行了重写,重写后的方法会通知依赖收集器Dep触发视图更新
js
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
// 重写数组的方法,以触发响应式更新
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
// 原始数组方法
const original = arrayProto[method];
// 重写的方法
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
// 获取数组对象的 __ob__(Observer 实例)
const ob = this.__ob__;
// 对于某些方法,可能需要额外的操作,例如 splice
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
// 如果有新的元素插入,需要将其变成响应式
if (inserted) ob.observeArray(inserted);
// 通知依赖更新
ob.dep.notify();
return result;
});
});
大白话总结一下,就是对数组的变异方法进行了重写,还是跟$set一样,手动 observer,重写后的方法会通知依赖收集器Dep触发视图更新
Proxy 的出现,解决了 defineProperty
的局限性
proxy
译为代理,可以理解为在操作目标对象前架设一层代理,将所有本该我们手动编写的程序交由代理来处理,生活中也有许许多多的"proxy", 如代购,中介,因为他们所有的行为都不会直接触达到目标对象
proxy的参数
- target 目标对象
- handler 代理对象,可以拦截目标对象的各种操作,比如读取属性值、设置属性值、获取对象的长度、判断一个对象是否存在某个属性等等
怎么说呢?好像和 defineProperty
有点像,但是又不一样🤦♀️,我们来看看 proxy
的用法
js
const proxy = new Proxy(target, handler)
js
const target = {
a: 1,
b: 2
}
const handler = {
get (target, key) {
console.log('get', key)
return target[key]
},
set (target, key, value) {
console.log('set', key, value)
target[key] = value
// 严格模式下,set代理如果没有返回true,就会报错
return true
}
}
const proxy = new Proxy(target, handler)
proxy.a // get a
proxy.a = 2 // set a 2
// 直接新增属性试试
proxy.c = 3 // set c 3
// 牛逼吧,直接就可以监听到新增属性了,打印target也是有c属性的这里就不贴图了
// 再来个数组试试
const arr = [1, 2, 3]
const proxyArr = new Proxy(arr, handler)
proxyArr.push(4) // set 3 4
// 打印arr也是有4的,知道为什么用proxy了吧🤡
proxy的优势(GPT总结的,我懒得写(copy)文档了)🤺🤺
-
更全面的拦截操作 :Proxy 允许你拦截和自定义对象上的各种操作,包括属性的读取、设置、删除、枚举、函数调用等。这使得你可以更精确地控制对象的行为,而不仅仅是属性的 getter 和 setter。
-
更灵活的属性访问:你可以使用 Proxy 定义属性的 getter 和 setter,从而实现更灵活的属性访问和修改,包括计算属性、惰性加载等。
-
更易于创建动态属性:Proxy 允许你动态地创建和管理属性,而不需要提前定义它们。这对于处理动态数据结构非常有用。
-
更易于处理数组和集合:在处理数组和集合时,Proxy 更直观且强大。你可以拦截数组的 push、pop、shift、unshift、splice 等操作,而无需手动设置 setter。
-
更好的错误处理:Proxy 提供了严格的错误处理机制,你可以在陷阱函数中处理错误或阻止不符合条件的操作。
需要注意的是,尽管 Proxy 具有许多优势,但它也具有一些性能开销,因此在性能敏感的应用程序中可能不适用。另外,Proxy 在某些环境下可能不被支持,特别是在旧的浏览器版本中(你懂的🤐)。