vue2是如何追踪数据变化形成响应?
概括版:
通过遍历劫持对象的所有属性来实现响应式数据(Object.defineProperty)。
专业版:
1.初始化阶段:
- 在Vue实例化过程中,Vue会遍历数据对象(通常是data属性中定义的数据)的所有属性,通过
Object.defineProperty
方法为每个属性添加getter和setter函数。这样做的目的是为了在属性被访问或修改时能够进行依赖收集和派发更新。2.依赖收集:
- 当Vue实例渲染过程中的模板(Template)中读取数据时,对应的属性的getter函数会被触发。Vue会通过一个全局变量(通常称为"依赖收集器"或"Watcher")来追踪这些属性的依赖关系。
- 每个Watcher对象会记录当前模板中使用的所有响应式数据。
- 当模板中的数据变化时,Watcher对象会负责通知相关的视图进行更新。
3. 触发更新:
- 当数据发生变化时,数据对象的setter函数会被触发。这时,Vue会通知依赖收集器中的Watcher对象,告诉它们数据已经发生了变化。
- Watcher对象会执行相应的回调函数,更新视图中依赖这些数据的部分。
4.更新视图:
- 一旦Watcher对象收到数据变化的通知,它会执行相应的回调函数,比如重新渲染模板或执行一些其他的副>作用。
- Vue通过虚拟DOM(Virtual DOM)来最小化真实DOM的操作,提高性能。当数据变化时,Vue会重新计算虚>>拟DOM与上一次渲染结果的差异,并将这些差异应用到真实DOM上,实现局部更新而不是整体重新渲染。 5.批量更新:
- Vue在更新视图时会进行批量更新。如果多次修改了同一个响应式数据,Vue会将这些更新合并为一次,以提高性能和减少不必要的DOM操作。
响应式丢失
但是通过遍历劫持对象的所有属性来实现响应式数据的方式,并不完美,存在很多缺点。例如当我们通过数组下标去修改数据时或者修改数组的长度、给对象不存在的属性赋值、或者使用delete删除对象的属性,这几种方式都不会被vue监听到,从而导致响应式丢失,视图不更新的问题。
js
<template> <!-- 模板部分 -->
<div id="app"> <!-- Vue 应用的根元素 -->
<p>Array: {{ someArray }}</p> <!-- 显示数组的内容 -->
<p>Object: {{ someObject }}</p> <!-- 显示对象的内容 -->
<button @click="modifyArrayIndex">Modify Array Index</button> <!-- 点击按钮,调用 modifyArrayIndex 方法 -->
<button @click="modifyArrayLength">Modify Array Length</button> <!-- 点击按钮,调用 modifyArrayLength 方法 -->
<button @click="addObjectProperty">Add Object Property</button> <!-- 点击按钮,调用 addObjectProperty 方法 -->
<button @click="deleteObjectProperty">Delete Object Property</button> <!-- 点击按钮,调用 deleteObjectProperty 方法 -->
</div>
</template>
<script> <!-- 脚本部分 -->
import Vue from 'vue'; // 导入 Vue 模块
export default { // 导出 Vue 组件对象
data() { // 数据对象
return {
someArray: [1, 2, 3], // 初始化数组
someObject: { name: 'John', age: 30 } // 初始化对象
};
},
methods: { // 方法对象
modifyArrayIndex() { // 修改数组中的元素
// 通过数组下标修改数据
this.someArray[0] = 100;
},
modifyArrayLength() { // 修改数组的长度
// 修改数组长度
this.someArray.length = 0;
},
addObjectProperty() { // 给对象添加新属性
// 给对象不存在的属性赋值
this.$set(this.someObject, 'newProp', 'New Value');
},
deleteObjectProperty() { // 删除对象的属性
// 删除对象属性
this.$delete(this.someObject, 'age');
}
}
};
</script>
<style scoped>
#app {
font-family: Arial, sans-serif;
padding: 20px;
}
button {
margin-right: 10px;
}
</style>
如何解决响应式丢失
1. Vue.$set(官方推荐)
- 对象:
Vue.set(object, key, value)
或this.$set(object, key, value)
- 数组:
Vue.set(array, index, value)
或this.$set(array, index, value)
- this.$set(要操作的对象或数组, 要新增或者修改的数组或对象key, 对应的值)
js
addObjectProperty() { // 给对象添加新属性
// 给对象不存在的属性赋值
this.$set(this.someObject, 'newProp', 'New Value');
},
modifyArrayIndex() { // 修改数组中的元素
// 通过 Vue.set 修改数组
Vue.set(this.someArray, 0, 100);
},
2. 使用数组的原型方法,修改数组
直接修改数组索引不会触发视图更新,但使用数组的变异方法(如 splice()、push()、pop()、shift()、unshift()、splice()、sort()、reverse() 等)可以保持响应式。这是因为这些方法会被 Vue 包装成响应式的操作。
js
modifyArrayIndex() { // 修改数组中的元素
// 使用数组的变异方法修改元素
this.someArray.splice(0, 1, 100);
},
3. 重新赋值
- 对象:当需要添加新属性时,可以通过重新赋值整个对象来确保新属性是响应式的。
- 数组:当需要修改数组时,可以通过重新赋值整个数组来确保数组变化是响应式的。
js
对象:
addObjectProperty() { // 给对象添加新属性
// 重新赋值整个对象,确保新属性是响应式的
this.someObject = { ...this.someObject, newProp: 'New Value' };
},
数组:
modifyArrayIndex() { // 修改数组中的元素
// 重新赋值整个数组,确保数组变化是响应式的
this.someArray = [100, ...this.someArray.slice(1)];
},
4. Vue.$forceUpdate(手动强制更新视图)
如果无法避免直接修改数组索引或对象属性,可以手动触发视图更新。可以使用
this.$forceUpdate()
或者this.$set(this.someObject, '__ob__', this.someObject.__ob__)
来强制更新视图。
js
//数组
modifyArrayIndex() { // 修改数组中的元素
// 通过数组索引直接修改元素
this.someArray[0] = 100;
// 手动触发视图更新
this.$forceUpdate();
},
//对象
addObjectProperty() { // 给对象添加新属性
// 给对象添加新属性
this.someObject.newProp = 'New Value';
// 手动触发视图更新
this.$set(this.someObject, '__ob__', this.someObject.__ob__);
},
5.Object.assign(使用修改栈能触发视图更新的特性)
Object.assign(target, ...sources)
target
:目标对象,将源对象的属性复制到该对象。sources
:一个或多个源对象,从这些对象中复制属性到目标对象。可以传入多个源对象,后面的源对象属性会覆盖前面的源对象属性。Object.assign()
方法会返回目标对象。
js
addObjectProperty() { // 给对象添加新属性
// 使用 Object.assign() 将新属性添加到目标对象,并重新赋值整个对象
this.someObject = Object.assign({}, this.someObject, { newProp: 'New Value' });
},