本文来讲解从 Vue2 到 Vue3 响应式底层的一些改变。
Vue 2.x 为什么不监听数组下标索引值的变化?
参考了很多博主的推文,自己也尝试了一下,Object.defineProperty 是可以做到监听数组的索引值的变化的,来做 getter 和 setter。
但 Vue2.x 为什么没有这么做呢?官方回答是因为性能的问题,所以不监听数组索引值的变化。
var vm = new Vue({
data: {
a: 1
}
});
// vm.a 是响应式的
vm.a = 1;
// vm.b 是非响应式的
vm.b = 2;
为什么会出现这种情况?vm 是一个 Vue 的实例,它带有了 data 对象中的所有的属性,如果我们访问 vm.a,代表我们访问的是 data 数据对象中的 a 属性,
数据对象中的属性都会被转化为 getter 和 setter,vm.a = 2 会触发对应的 setter。vm.b = 2 就是在 vm 对象上添加了一个属性,所以它不会有响应性。
这里就是 Vue2 对对象处理比较薄弱的一块,我们没办法对对象新增的属性进行实时的响应。
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
});
vm.items[1] = 'x'; // 不是响应性的
vm.items[1].length = 2; // 不是响应性的
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此,我们还是有办法来回避这些限制并保证它们的响应性。
这里来介绍一个术语:代理原型。
假设我们有一个数组,想要监听这个数组元素的变化,我们首先要找到能够改变数组成员的方法,比如 push、pop 等方法,这些方法都能够改变数组结构。这些方法都是定义在 Array 这个数组的原型对象上,Array 是 JS 内置的一个构造函数。Array 对象的这些方法的代码我们没法去修改,所以这时我们就要想一个办法,我们能不能在要监听的数组和 Array 原型对象间嫁接一层,这个嫁接的一层就是我们所说的代理原型对象(即上图中粉色框)。
原本我们使用 push 方法时,指向的是数组原型对象上的 push 方法。现在我们做了一层嫁接,被监听数组的 __proto__ 指向的是代理原型对象,在代理原型对象中扩展了能改变数组数据结构的方法。在代理原型对象的方法中,需要做2件事。第一件事就是方法的原有行为不变,第二件事为做一些拦截操作,将改变数组结构的操作加入到响应式机制中,去通知触碰了这个数组的所有观察者数组已发生变化。
回到前言中的问题,Vue 2.x 为什么不监听数组下标索引值的变化?原因就是在于 Vue2 的数组响应式是通过代理原型对象实现了,只有调用数组原型对象上的方法改变数组元素时才可以监听到变化。当然也可以为数组的每个元素设置 getter 和 setter,但这样会增加很多不必要的性能损耗。
因此,Vue 提供了一个方法 Vue.set,使用这个方法改变数组和对象的元素可以响应性的被监听到。
在 Vue3 中,使用 Proxy 来实现响应性。
Vue3 使用了 Proxy 可以解决 Vue2 中两个不能直接通过语法来解决的问题:
(1) 对新增的响应式对象属性的监听。
(2) 通过数组下标修改数组元素的响应性。
Proxy 可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截。因此提供了一种机制,可以对外界的访问进行过滤和改写。
再来看上篇文章的示例:
const data: DataProps = reactive({
count: 0,
double: computed(() => data.count * 2),
increase: () => {
data.count++;
},
});
修改示例代码:
const data: DataProps = reactive({
count: 0,
double: computed(() => data.count * 2),
nums: [1, 2, 3, 4, 5],
increase: () => {
data.count++;
for (let i = 0; i < data.nums.length; i++) {
data.nums[i]++;
}
},
});
在 data 中新增一个数组 nums,元素为 1、2、3、4、5。修改方法 increase,对数组中的所有元素进行自增。
修改模板部分,添加一个 ul 列表,展示 nums 中的元素值。
{{ item }}
如果使用 Vue2,显然 increase 方法是不会触发 li 元素值的变化。但在 Vue3 中,是可以响应式修改的。
进入预览页面,可以看到展示出了 nums 的元素:
点击 button 按钮,列表元素响应式的进行自增:
简单实现代码如下:
var source = [0, 1, 2];
var wrapped = new Proxy(source, {
get(target, p, receiver) {
console.log('我侦测到你想对数组下标' + p + '进行访问');
return Reflect.get(target, p, receiver);
},
set(target, p, value, receiver) {
console.log('我侦测到你想对数组下标' + p + '进行赋值');
return Reflect.set(target, p, receiver);
}
});
wrapped[0];
wrapped[2] = 5;
粘贴到浏览器控制台运行,运行结果如下:
可以看到,使用 Proxy 成功监测到了数组下标元素的变化。
1. Object.defineProperty 是对对象属性的劫持;Proxy 是对整个对象的劫持。
2. Object.defineProperty 需要手动绑定响应式对象新增属性的响应性;Proxy 能侦测对象新增属性并实时响应。
3. Object.defineProperty 无法侦测数组元素的变化,需要手动设置代理原型;Proxy 能监听数组。
4. Object.defineProperty 启动就递归,不管对象嵌套多深,都把对象上的 property 转化为响应式的;Proxy 只在 getter 时才进行对象下一层属性的劫持。
5. Object.defineProperty 无兼容性问题;Proxy 有兼容性问题(IE)。