发现一个Vue2和Vue3使用差异
众所周知,在Vue做一些操作是会导致数据丢失响应式的。例如:在Vue3里,对reactive定义的对象进行直接赋值,会发现输出改变了,但是视图并没有随之更新。
js
<script setup>
import { reactive } from 'vue';
let testInfo = reactive({
name: '11',
data: [0, 1]
})
const testHandle = () => {
testInfo = {
test: '111'
}
console.log(testInfo);
}
</script>
<template>
<div class="page">
{{ testInfo }}
<button @click="testHandle">test</button>
</div>
</template>
但在Vue2中,则会发现进行类似操作的时候并不会影响视图更新。如下,点击按钮后,视图顺利的产生了变化。这就产生了两种不同的现象。
js
<script>
export default{
data(){
return {
testInfo: {
name: '11',
data: [0, 1]
},
}
},
methods: {
testHandle() {
this.testInfo = {
test: '111'
}
}
}
}
</script>
<template>
<div class="page">
{{ testInfo }}
<button @click="testHandle">测试</button>
</div>
</template>
向通义千问提问
于是,我带着疑惑向通义千问进行了首次提问🤔:
提问:为什么vue2给对象直接赋值不会失去响应式?
#答:在Vue 2.x 中,当你直接给data对象中的某个现有属性赋值时,如果该属性已经存在于Vue实例的data对象中,并且已经被Vue转化为了响应式属性,则即使直接赋值,视图依然会保持响应式更新。
但是,如果你给对象添加了一个原本不存在的新属性,然后直接对该新属性进行赋值,Vue 2.x 默认情况下是不会检测到这个新属性的添加,并且该新属性不会自动变为响应式。这意味着,如果你直接做如下操作,视图不会立即更新
很好,回答的很不错!但是完全驴头不对马嘴!答案不是我想要的。于是,我进行了自我反思,是不是我的提问方式有问题?于是我重新组织了一下语言,然后继续提问。
提问:为什么在vue2中,给整个响应式对象直接替换赋值 不会 使它失去响应式?
#答:在 Vue 2.x 中,给响应式对象直接替换赋值(即完全替换原有的对象)确实会导致响应性的丢失。这是因为 Vue 只能追踪对象的属性变化,而不是对象本身的引用变化。当你直接用一个新的对象替换掉原来的响应式对象时,Vue 实例内部的响应式系统失去了对原始对象的引用,所以对新对象的任何改动都不会触发视图的更新。例如:
js
// 通义千问给出的例子
data() {
return {
user: { name: 'Alice' }
};
},
methods: {
replaceUser(newUserData) {
// 直接替换整个对象不会丢失响应性(仅限于顶级响应式对象)
this.user = newUserData;
}
}
这回,不仅回答完全是与我的提问相反方向的错误回答,甚至和自己举的例子(见上方代码)都是冲突的。于是,我开始了我的指指点点🫵
提问:错了,直接替换整个对象 不会丢失响应式
#答:抱歉之前的回答有误,实际上在Vue 2.x中,如果你直接替换整个响应式对象(如this.someObject = newObj),的确不会丢失响应性,
目前为止还是对的,只是事情还没有那么简单,接下来还有但是,它接着说:
#答:但这仅限于顶级响应式对象。Vue 2.x 会对data对象进行响应式处理,当你直接替换data中的某个对象时,Vue会继续监测新的对象,并对其进行响应式转换,因此视图依旧能正常响应新对象的属性变化。
此时,对它已经有些许不信任的我,对它说的进行了一番实践(见下例),发现视图更新没有受到任何影响,正常的输出了更新后的数据。于是,我又双叒继续进行了提问。
js
<template>
<div class="page">
// 响应式输出测试
{{ testInfo }}
<button @click="replaceUser({testRef: {a: 1}})">1</button>
</div>
</template>
<script>
export default {
data() {
testInfo: {
name: 'testA',
cc: { a: 1, b: 2 }
}
},
methods: {
replaceUser(newData) {
this.testInfo.cc = newData;
},
},
}
</script>
提问:错了,不是顶级响应式对象也不会丢失响应式
#答:抱歉,我在之前的纠正中仍然不够准确。实际上,在 Vue 2.x 中,如果直接替换一个嵌套的响应式对象,即非顶级响应式对象,确实会导致原有的响应式连接失效,因为Vue没有对新替换的嵌套对象进行响应式处理。但是,如果在Vue实例的生命周期钩子函数created、mounted等执行完毕之后,你直接替换一个已经处于Vue响应式系统监控下的对象(无论是顶级响应式对象还是嵌套的响应式对象),只要新对象被赋值到已经声明过的响应式属性上,Vue会尝试为新对象的所有属性转换为响应式,前提是这些属性在Vue实例创建时就已经存在。
总结对话
经过以上四轮对话,可能看的有些晕,到底什么情况会丢失响应式?什么情况不会?这种表现的原因是什么呢?下面总结一下疑惑的解答。
会丢失响应式的情况(附带原理解读)
1、直接给响应式对象新增一个属性,那么新增的属性不会具有响应式。
原因: 在 Vue2 中,会使用 Object.defineProperty 对传入的data中的数据进行劫持,也就是监听获取和设置操作,而Object.defineProperty这个api需要监听到对象的具体属性,所以在Vue2中会对对象进行遍历,然后监听对象具体的属性。所以哪些属性是响应式的,在Vue数据劫持完成之后就已经确定的,而后面新增的属性不在其内,所以新增的属性不具有响应式。
2、使用下标形式对响应式数组进行修改,修改后的数据不会响应式更新。
原因: 原因同上,在我们使用 Object.defineProperty 对数据进行劫持的时候,可以发现:使用下标形式对监听的数组进行修改的时候,并不会触发 Object.defineProperty 中更新操作的监听函数。也就是在这种情况下, Object.defineProperty 监听不到数据修改了,而触发响应式依赖的操作恰恰是放在 Object.defineProperty 的更新函数下的。监听不到数据修改,自然就不能触发响应式了。
可以亲自敲一敲下方的例子,便于理解上面说的原理。
js
function observer (value) {
// 简单处理,主要是要对数据进行前置判断
if (!value || (typeof value !== 'object')) {
return;
}
// 遍历传入
Object.keys(value).forEach((key) => {
defineReactive(value, key, value[key]);
});
}
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
// 是否可枚举
enumerable: true,
// 是否可被修改、删除
configurable: true,
// 获取属性的方法
get: function () {
console.log('获取数据')
// 后续会在这里进行依赖收集
return val;
},
// 设置属性的方法
set: function (newVal) {
console.log('设置数据')
if (newVal === val) return;
// 后续会在这里进行依赖触发、更新依赖
}
});
}
let test = {
a: {
testa: {
c: 2
}
},
data: [1, 2]
}
observer(test); // 将test传入进行observer监听
// ===================== 丢失响应式
test.data[0] = 0; // 控制台并没有输出 '设置数据',也就是没有监听到数据更新
console.log(test);
// ===================== 不会丢失响应式
// 控制台正常输出 '设置数据' 字样,也就是监听到了数据更新
test.a = {
testRef: {
a: 1
}
}
不会丢失响应式的情况(附带原理解读)
在数据监听完毕后,对已经变成响应式的对象进行整个儿的替换并不会使数据失去响应式,不管新对象内的属性名是原有的还是新增的,都不会对响应式有影响。
原因: 在对传入的data数据进行劫持监听的时候,我们第一步做的是:监听整个data的数据。也就是说data下的每一项数据都是data的一个属性,在初始化的时候已经将data的每个属性,都转化为了响应式的,相当于对原响应式属性的更改。而我们从上文可以得知:只有在为对象添加属性的时候才会导致响应式的丢失,所以这里是不会丢失响应式的。可以尝试在上例中尝试将 test.a 的值整个替换并检查控制台有没有输出 设置数据 字样来验证。