Vue 的数据响应式原理

Vue 的数据响应式原理

我之前一直很好奇在Vue中是如何实现数据的响应式的,从我自己的角度这个问题其实很好理解,实现数据的响应式,无非就是监听数据的变化嘛,这边儿数据变了,那边儿更新对应的引用数据的位置就可以了。

但是仔细思考一下,又感觉没那么容易。数据监听应该如何实现?引用位置如何确定?

React 中我发现这个问题更加离谱,他在数据更新以后,把他的所有子组件全刷新了一遍,重新执行了一遍所有没带缓存的子组件函数的render ,那在Vue总这个问题是否也存在呢?

Object.defineProperty

在JavaScript中,为我们提供了类似Object.defineProperty 的方式,可以轻松的监听的数据变化,回顾了一下它的用法。

Object.defineProperty(obj, prop, descriptor)
Code language: JavaScript (javascript)
  • obj: 要定义属性的对象。
  • prop: 属性的名字。
  • descriptor: 属性的描述符对象,包含了一些可以控制属性行为的配置选项。

我们先来试试水

  1. 定义一个简单属性
    const obj = {}<br>Object.defineProperty(obj, 'name', {<br>  value: 'Nic'<br>})<br><br>console.log("Name =", obj.name);<br>// >>> Name = Nic<br><br>console.log(Object.keys(obj)) // 直接通过Object.defineProperty 定义不会进入枚举里被奥<br>// >>> {}<br>

也就是说,这个方法可以直接定义 Object 的属性,并且可以绕开一些默认的描述,比如枚举,那我们通过描述符是不是还可以干一些其他事情,先来看看它的描述符号有些什么

常见的描述符属性

  • value: 属性的值(可以是任何类型)。
  • writable: 如果为true,属性值可以被修改(默认为false)。
  • enumerable: 如果为true,该属性会出现在for...in循环以及Object.keys()等方法的结果中(默认为false)。
  • configurable: 如果为true,可以删除或修改属性的描述符(默认为false)。
  • get: 用于获取属性值的 getter 函数。
  • set: 用于设置属性值的 setter 函数。

那我们试试,实现一个不可被修改的属性值

const obj = {};
Object.defineProperty(obj, 'name', {
  value: 'John',
  writable: false,  // 不允许修改
});

console.log(obj.name); // "John"
obj.name = 'Doe'; // 修改失败,静默失败
console.log(obj.name); // "John"
Code language: JavaScript (javascript)

确实无法被修改了。

那我们定义一个属性的时候,定义它的getset 函数,是不是就可以监听到它的变化了?

定义一个getter 和一个setter

const obj = {
  firstName: 'John',
  lastName: 'Doe'
};

// 使用 getter 和 setter 定义计算属性
Object.defineProperty(obj, 'fullName', {
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  set(value) {
    const parts = value.split(' ');
    this.firstName = parts[0];
    this.lastName = parts[1];
  },
  enumerable: true,
  configurable: true
});

console.log(obj.fullName); // "John Doe"
obj.fullName = 'Jane Smith'; // 使用 setter 设置新值
console.log(obj.firstName); // "Jane"
console.log(obj.lastName);  // "Smith"
Code language: JavaScript (javascript)

嗯,很好,我们修改了呃一个属性的get和set 以后,确实能够监听到数据的改变,那如果我再配合上发布订阅模式,是不是就可以实现基础的数据监听了。

虽然,这个方案确实可以监听的数据变化,但是Vue在升级Vue3以后就抛弃了这种实现方案,改用全新的特性Proxy 说明这种方案应该有一些缺点

1.Proxy 能监听整个对象,而**Object.defineProperty**只能监听单个属性

  • Object.defineProperty只能对对象的每个属性单独进行劫持,不能直接监听对象本身。如果一个对象有很多属性,Vue 2 需要遍历每个属性并调用Object.defineProperty
  • Proxy直接作用于整个对象,不需要遍历每个属性,可以拦截对整个对象的操作,包括属性添加和删除。

Vue 2 示例(**Object.defineProperty**监听属性):

javascript

复制编辑

const data = {};
Object.defineProperty(data, 'msg', {
  get() {
    console.log('获取 msg');
    return 'Hello';
  },
  set(newVal) {
    console.log('设置 msg:', newVal);
  }
});

console.log(data.msg); // 获取 msg
data.msg = 'Hi';       // 设置 msg: Hi
Code language: JavaScript (javascript)

这个方法只能劫持msg,如果data还有别的属性,就需要再手动定义。

Vue 3 示例(**Proxy**监听整个对象):

const data = new Proxy({}, {
  get(target, key) {
    console.log(`获取 ${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`设置 ${key}${value}`);
    target[key] = value;
    return true;
  }
});

console.log(data.msg); // 获取 msg
data.msg = 'Hi';       // 设置 msg 为 Hi
Code language: JavaScript (javascript)

这个Proxy可以监听所有属性的访问和修改,不需要为每个属性单独定义 getter 和 setter。


2.Proxy 能检测属性的添加和删除

  • Vue 2 使用Object.defineProperty监听对象时,无法检测到新增删除属性,必须使用Vue.set()才能让 Vue 监听新加的属性。
  • Vue 3 使用Proxy可以直接监听对象的getsetdeleteProperty操作,不需要额外的方法。

Vue 2(无法监听新增属性)

const obj = {};
Object.defineProperty(obj, 'msg', {
  value: 'Hello',
  writable: true,
  configurable: true
});

// Vue 2 无法监听新增的属性
obj.newProp = 'Hi'; 
console.log(obj.newProp); // Hi,但 Vue 2 不会响应这个变化
Code language: JavaScript (javascript)

必须用Vue.set(obj, 'newProp', 'Hi')才能让 Vue 监听到新增的属性。

Vue 3(**Proxy**可以监听新增和删除)

const obj = new Proxy({}, {
  get(target, key) {
    return target[key];
  },
  set(target, key, value) {
    console.log(`设置 ${key}${value}`);
    target[key] = value;
    return true;
  },
  deleteProperty(target, key) {
    console.log(`删除属性 ${key}`);
    delete target[key];
    return true;
  }
});

obj.msg = 'Hello'; // 设置 msg 为 Hello
delete obj.msg;    // 删除属性 msg
Code language: JavaScript (javascript)

在 Vue 3 中,你可以直接删除属性,而 Vue 2 无法做到这一点。


3.Proxy 支持对数组的监听

  • Vue 2 无法监听数组索引的变化,比如arr[1] = 'newValue',需要使用Vue.set(arr, 1, 'newValue')才能让 Vue 监听到变化。
  • Vue 3 通过Proxy直接监听数组的所有操作,如pushpopsplice等。

Vue 2(无法监听数组索引变化)

const arr = [];
Object.defineProperty(arr, '0', {
  get() {
    console.log('获取索引 0');
    return 'Hello';
  },
  set(newVal) {
    console.log('设置索引 0:', newVal);
  }
});

arr[0] = 'Hi'; // Vue 2 无法监听
console.log(arr[0]); // 仍然是 'Hi',但 Vue 2 响应式系统不会检测到这个变化
Code language: JavaScript (javascript)

Vue 3(**Proxy**监听数组)

const arr = new Proxy([], {
  set(target, key, value) {
    console.log(`数组索引 ${key} 变为 ${value}`);
    target[key] = value;
    return true;
  }
});

arr[0] = 'Hi'; // 数组索引 0 变为 Hi
console.log(arr[0]); // Hi
Code language: JavaScript (javascript)

Vue 3 可以监听数组的索引修改和方法调用(pushpop等),不需要额外的Vue.set()


4.*Proxy 允许监听对象的原型 (***__proto__**修改)

Vue 2 由于使用Object.defineProperty,无法检测对象的__proto__变化。例如:

const obj = { a: 1 };
Object.setPrototypeOf(obj, { b: 2 });

console.log(obj.b); // 2,但 Vue 2 无法检测到 b 这个新属性
Code language: JavaScript (javascript)

Vue 3 由于Proxy拦截了get操作,可以监听__proto__的变化,从而提高响应式能力。


5.Proxy 性能更好

在 Vue 2 中,Object.defineProperty需要对每个属性进行遍历深度绑定,如果对象有很多属性,初始化开销很大。而Proxy直接作用于整个对象,不需要遍历所有属性,因此性能更好。


6.Proxy 未来可扩展性更强

Object.defineProperty只能拦截getset,而Proxy提供了更多的拦截方法,如:

  • getPrototypeOf/setPrototypeOf
  • has(用于in操作符)
  • ownKeys(用于Object.keys()
  • apply(拦截函数调用)
  • construct(拦截new操作)

这些特性让 Vue 3 的响应式系统更强大,更容易扩展。


总结:为什么 Vue 3 使用Proxy

特性`Object.defineProperty`(Vue 2)`Proxy`(Vue 3)
监听整个对象❌ 只能逐个属性监听✅ 直接监听整个对象
监听新增/删除属性❌ 需要`Vue.set()`✅ 直接支持
监听数组索引变化❌ 需要`Vue.set()`✅ 直接支持
监听`__proto__`变化❌ 无法监听✅ 可以监听
性能🚀 需要遍历所有属性,性能低⚡ 直接作用于整个对象,性能更好
未来可扩展性🚫 仅支持`get`、`set`✅ 拦截更多操作,如`has`、`ownKeys`

由于这些优势,Vue 3 选择了Proxy来实现响应式,而不再使用Object.defineProperty。但Proxy需要 ES6+,在 IE11 及更早版本中不受支持,因此 Vue 3不再兼容 IE11

依赖收集

数据更新以后,就需要通知到视图层去刷新页面的数据。一个数据变化,应该直接作用在使用数据的位置,也就是说谁动了谁更新,

相关推荐

暂无相关文章!

暂无评论