Vue2
首先,我们要知道 JavaScript 的对象由一些属性(property)组成,每个属性其实都是一个键值对,有一个 key 和一个对应的 value。
而 JavaScript 提供了一些内置的方法,可以帮助我们自定义这些属性的行为。
其中,Object.defineProperty() 就是这样的一个方法。
它让我们可以精确地添加新的属性或修改对象的现有属性,并对属性进行更细粒度的控制。
例如,我们定义了一个对象:
let obj = { a: 1 };
我们可以用 Object.defineProperty() 来为这个对象定义一个新的属性 b:
Object.defineProperty(obj, 'b', {
value: 2,
writable: true,
enumerable: true,
configurable: true
});
在 Object.defineProperty() 的第三个参数(也就是属性描述符)里,我们可以定义这个属性的一些特性,比如这个属性是否可写(writable)、是否可枚举(enumerable)、是否可配置(configurable)等。
而 getter 和 setter 就是属性描述符里的两个可选键。它们可以让我们自定义一个属性的读取(get)和赋值(set)行为。
例如,我们可以这样定义一个属性:
let obj = {
_a: 1,
get a() {
console.log('get a');
return this._a;
},
set a(value) {
console.log('set a');
this._a = value;
}
};
这里,我们定义了一个属性 a,并提供了 get 和 set 方法。当我们访问 obj.a 的时候,会执行 get 方法并打印 "get a";当我们给 obj.a 赋值的时候,会执行 set 方法并打印 "set a"。
当我们访问一个对象的属性时,如果该属性定义了 getter 函数,那么 JavaScript 就会自动调用这个 getter 函数。
同样,当我们试图改变一个对象的属性值时,如果该属性定义了 setter 函数,那么 JavaScript 就会自动调用这个 setter 函数。因此,通过在 getter 和 setter 函数中添加我们自定义的代码,我们就可以知道何时读取了数据和何时修改了数据。
在 Vue.js 的响应式系统中,getter 和 setter 扮演着非常关键的角色。
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 会遍历这个对象的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变化。这就是 Vue.js 的“响应式”的核心——模板和组件中的数据变化,会触发视图的重新渲染。
举个简单的例子:
let data = { a: 1 };
let vm = new Vue({
data: data
});
console.log(vm.a == data.a); // -> true
vm.a = 2;
console.log(data.a); // -> 2
data.a = 3;
console.log(vm.a); // -> 3
在这个例子中,我们创建了一个 Vue 实例并传递了一个普通的 JavaScript 对象 data。Vue 会将 data 的属性转化为 getter 和 setter,因此当我们访问 vm.a 或 data.a 时,Vue 就知道我们访问了 a 属性,而当我们修改 vm.a 或 data.a 时,Vue 也知道我们修改了 a 属性,并触发视图的更新。
不能检测到对象属性的添加或删除
对于对象属性的添加或删除,Object.defineProperty 并不能检测到。
Object.defineProperty 只能劫持到对象的属性,因此它需要预先知道属性的键名。
如果在初始化的时候属性还不存在,那么后来添加的属性就无法被监听到,因为没有为其设置 getter 和 setter。同样的,对于删除的属性,因为删除操作不涉及设置新的值,所以也无法被侦听到。
这也是为什么在 Vue 中,如果需要添加新的响应式属性,需要使用 Vue.set 或者 this.$set 方法。
不能很好地处理数组的变化
对于数组,Object.defineProperty 无法监听到数组内容的改变。
这是因为常见的改变数组内容的方法如 push, pop, splice 等并不会触发数组的 setter。而 Object.defineProperty 是通过 getter/setter 来实现数据响应的,无法监听到这些操作。
push, pop, splice 等方法导致的数组变化的。这是因为这些操作并不会修改数组的索引或者长度,所以并不会触发 setter。记住,我们是通过定义属性的 getter 和 setter 来进行侦听的。
举个例子 👇🏻
当你使用 push 方法添加元素时,你实际上是在数组的末尾添加了一个新的元素,而不是改变了数组的长度。因此,使用 push 方法不会触发任何已经设置的 setter。
为了解决这个问题,Vue.js 对数组的一些方法(例如 push, pop, shift, unshift, splice, sort, reverse)进行了“变异”处理,使得这些方法在完成其原有功能的同时,也能触发视图的更新。
所以在Vue项目中,你可以放心地使用这些方法修改数组,Vue会在底层捕捉到这些变化并自动更新视图。
为了解决这些问题,Vue 对数组的一些方法进行了“变异”处理(比如
push、pop等)。但实际上这是一种“变通”的方法,并不能从根本上解决问题,也无法处理所有的边界情况,比如通过索引直接修改数组元素的值等。
Vue3
因为在 Vue 2.x 中,我们通过 Object.defineProperty 来对对象的属性进行劫持,但是,Object.defineProperty 存在一些限制,比如无法检测到对象的属性的添加和删除,对数组的处理也不够优雅。
Vue 3 的响应式系统通过 Proxy 对象来进行数据劫持。Proxy 对象可以在对象级别上进行拦截,而不是像 Object.defineProperty 那样只能在属性级别上进行拦截。这就解决了 Vue 2.x 中的一些问题。
比如,Proxy 可以直接监听对象而非属性,而 Reflect 则用来完成默认操作。这使得我们能够检测到添加和删除属性的操作,对数组的处理也更加优雅。
实现思路:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log(`get ${key}`);
return Reflect.get(target, key);
},
set(target, key, value) {
console.log(`set ${key}`);
return Reflect.set(target, key, value);
},
deleteProperty(target, key) {
console.log(`delete ${key}`);
return Reflect.deleteProperty(target, key);
}
});
}
let obj = reactive({ name: "Vue3" });
obj.name; // get name
obj.name = "Vue3.0"; // set name
delete obj.name; // delete name
值得注意的是,
Proxy是 ES6 的特性,所以在不支持 ES6Proxy的环境下(如 IE),Vue 3 的响应式系统无法工作。因此,对于需要兼容 IE 的项目,仍需要使用 Vue 2.x。
这个时候你可能要问了,webpack 这种工具不是可以通过 Bable-loader 来对 ES6、ES7进行转换吗?
但是!!有一些 JavaScript 的新特性是无法通过 Babel 转换的,其中就包括 ES6 的 Proxy。
这是因为 Proxy 是 JavaScript 的一种新的内置对象,它修改了 JavaScript 对象的基本行为,这种基本行为是无法通过转换代码来实现的。也就是说,如果浏览器不支持 Proxy,那么即使我们使用了 Babel,也无法在这种浏览器中使用 Proxy。