简述数据绑定的原理
Object.defineProperty 可以检测到属性的变化。一旦数据发生访问或修改时候,通过数据劫持,我们就可以检测到,
到底层检测到数据变化的时候,就会通过发布订阅者模式通知更新dom
Vue 的响应式原理是 Vue.js 框架的核心机制,它使得数据变化能够自动更新视图。其核心是利用 JavaScript 的 Object.defineProperty(Vue 2.x)或 Proxy(Vue 3.x)来实现数据劫持,结合发布-订阅模式,在数据变化时通知依赖进行更新。 下面分别对 Vue 2 和 Vue 3 的响应式原理进行说明:
Vue 2.x 响应式原理
- 数据劫持(Data Observation):
- Vue 2 使用
Object.defineProperty
来劫持对象属性的 getter 和 setter。 - 遍历数据对象的所有属性,将其转换为 getter/setter 形式,从而在读取和设置时可以执行自定义操作。
- Vue 2 使用
- 依赖收集(Dependency Collection):
- 在 getter 中收集依赖(Watcher)。每个组件实例对应一个 Watcher,Watcher 会在组件渲染过程中“接触”用到的数据属性,从而触发 getter,将 Watcher 添加到当前属性的依赖列表中。
- 每个属性都有一个 Dep(依赖管理器)实例,用于存储所有依赖于该属性的 Watcher。
- 派发更新(Dispatching Updates):
- 当数据发生变化时,触发 setter,调用 Dep 的 notify 方法,通知所有依赖的 Watcher 进行更新。
- Watcher 更新会触发组件的重新渲染(重新执行 render 函数生成虚拟 DOM,然后进行 patch 更新真实 DOM)。
- 数组处理:
- 由于
Object.defineProperty
无法监听数组索引的变化,Vue 2 通过重写数组的 7 个变更方法(push, pop, shift, unshift, splice, sort, reverse)来实现响应式。这些方法被重写,在调用时会触发更新。
- 由于
- 缺点:
- 无法检测对象属性的添加或删除(需要使用
Vue.set
或Vue.delete
)。 - 数组的索引直接设置(如
arr[index]=newValue
)和修改数组长度无法检测。 - 深度监听需要递归遍历整个对象,性能负担较大。
- 无法检测对象属性的添加或删除(需要使用
Vue 3.x 响应式原理
Vue 3 使用 ES6 的 Proxy 代替了Object.defineProperty
,解决了 Vue 2 的一些限制。
- Proxy 代理:
- Proxy 可以拦截整个对象,包括属性的读取、设置、删除、方法调用等,不需要遍历每个属性。
- 对于数组,Proxy 可以监听索引变化和
length
变化。
- Reflect:
- 通常与 Proxy 配合使用,用于操作目标对象。
- 响应式 API:
reactive
:创建响应式对象(使用 Proxy)。ref
:创建基本类型的响应式数据(内部也是用 reactive 包装为对象)。effect
:副作用函数,类似于 Vue 2 的 Watcher,当依赖的响应式数据变化时,effect 会重新执行。
- 依赖收集与触发更新:
- 在 Proxy 的 get 拦截中收集依赖(track 函数)。
- 在 set、deleteProperty 等拦截中触发更新(trigger 函数)。
- 依赖管理采用 WeakMap(target -> Map(key -> Set(effects)))的数据结构,可以更精确地管理依赖。
- 优势:
- 可以检测对象属性的添加和删除,不需要特殊 API。
- 支持数组索引和长度的直接修改。
- 性能更好:惰性收集,不需要一开始就递归整个对象(按需收集)。
- 支持 Map、Set 等新的集合类型。
响应式原理的流程图(Vue 3 为例)
初始化响应式数据(reactive/ref) → 用Proxy代理对象
│
读取数据(触发get) → 依赖收集(track) → 将当前activeEffect(副作用)添加到依赖集合中
│
修改数据(触发set) → 依赖触发(trigger) → 从依赖集合中取出所有effect并执行
│
→ 组件更新/副作用函数重新执行
示例代码(Vue 3 响应式模拟)
// 简单的Vue3响应式模拟
let activeEffect = null;
const targetMap = new WeakMap();
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => effect());
}
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
});
}
function effect(fn) {
activeEffect = fn;
fn(); // 执行一次,触发依赖收集
activeEffect = null;
}
// 使用示例
const state = reactive({ count: 0 });
effect(() => {
console.log(`count is: ${state.count}`);
});
state.count++; // 触发更新,输出: count is: 1
总结
- Vue 2:通过
Object.defineProperty
劫持数据,需要递归遍历对象,对数组有特殊处理。使用 Dep 和 Watcher 进行依赖管理。 - Vue 3:使用 Proxy 和 Reflect,可以拦截更多操作,无需递归初始化,性能更好,支持更多数据类型。依赖管理采用 WeakMap、Map、Set 的数据结构。 Vue 的响应式系统是其数据驱动视图的核心,理解其原理有助于更好地使用 Vue 和进行性能优化。