Vue的双向绑定机制是其核心特性之一,它使得数据和视图能够自动保持同步。下面分别阐述Vue 2和Vue 3中双向绑定的原理,并给出手动实现的思路和示例。
一、Vue 2 双向绑定原理
Vue 2 的双向绑定基于 数据劫持 + 发布-订阅模式,具体由三个核心模块协作完成:
- Observer:通过
Object.defineProperty 递归遍历数据对象的所有属性,将它们转换为 getter/setter。当属性被读取时(getter)收集依赖(Dep),当属性被修改时(setter)通知所有依赖(Watcher)进行更新。
- Dep:依赖管理器,每个响应式属性都对应一个 Dep 实例,负责收集所有依赖于该属性的 Watcher,并在属性变化时通知它们。
- Watcher:观察者,负责将模板中的表达式或指令与对应的数据属性建立联系。当数据变化时,Watcher 收到通知并触发视图更新(如重新渲染组件或更新DOM节点)。
v-model 的实现:
v-model 是语法糖,在普通表单元素上,它本质上是绑定 value 属性并监听 input 事件。例如:
1 2 3
| <input v-model="message">
<input :value="message" @input="message = $event.target.value">
|
在组件上使用 v-model,默认会传递一个名为 value 的 prop,并监听名为 input 的事件。
Vue 2 的局限性:
- 无法检测对象属性的添加或删除(需要使用
Vue.set 或 vm.$set)。
- 无法直接监听数组索引和长度的变化,因此 Vue 重写了数组的 7 个变异方法(push、pop 等)来实现响应式。
二、Vue 3 双向绑定原理
Vue 3 引入了 Proxy 作为新的响应式基础,解决了 Vue 2 中的诸多限制,同时提升了性能。
- Proxy:直接代理整个对象,而不是对象的每个属性。它可以拦截对象的各种操作(get、set、deleteProperty、has 等),因此能够监听到属性的添加、删除,以及数组索引和长度的变化。
- Reactive 模块:Vue 3 中使用
reactive 函数将数据转换为响应式对象,内部基于 Proxy 实现。同时提供了 ref 来处理基本类型值的响应式。
- 依赖收集:在 Proxy 的 get 陷阱中进行依赖收集(使用
track 函数),在 set 陷阱中触发更新(使用 trigger 函数)。依赖追踪通过 effect 函数进行管理。
v-model 的变化:
- 默认 prop 名称由
value 改为 modelValue,默认事件由 input 改为 update:modelValue。
- 支持在同一个组件上绑定多个 v-model,例如
v-model:title="pageTitle",对应的 prop 为 title,事件为 update:title。
- 支持自定义修饰符,通过 prop 接收修饰符对象。
示例:
1 2 3
| <ChildComponent v-model:title="pageTitle" />
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
|
三、如何手动实现简单的双向绑定
1. 基于 Object.defineProperty 的实现(类似 Vue 2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| function observe(obj) { if (!obj || typeof obj !== 'object') return; Object.keys(obj).forEach(key => { let internalValue = obj[key]; const dep = new Dep();
Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend(); } return internalValue; }, set(newVal) { if (newVal === internalValue) return; internalValue = newVal; dep.notify(); } }); }); }
class Dep { constructor() { this.subscribers = new Set(); } depend() { if (Dep.target) { this.subscribers.add(Dep.target); } } notify() { this.subscribers.forEach(sub => sub.update()); } } Dep.target = null;
class Watcher { constructor(updateFn) { this.updateFn = updateFn; this.get(); } get() { Dep.target = this; this.updateFn(); Dep.target = null; } update() { this.updateFn(); } }
const data = { message: 'Hello' }; observe(data);
const input = document.getElementById('input'); const span = document.getElementById('span');
new Watcher(() => { span.textContent = data.message; });
input.addEventListener('input', e => { data.message = e.target.value; });
|
2. 基于 Proxy 的实现(类似 Vue 3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { const value = Reflect.get(target, key, receiver); return value; }, set(target, key, value, receiver) { const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (oldValue !== value) { triggerUpdate(); } return result; } }); }
const data = reactive({ message: 'Hello' }); const input = document.getElementById('input'); const span = document.getElementById('span');
span.textContent = data.message;
input.addEventListener('input', e => { data.message = e.target.value; });
function triggerUpdate() { span.textContent = data.message; }
|
说明:
上述代码省略了完善的依赖收集和调度,仅展示了核心的“数据变化 → 视图更新”和“视图输入 → 数据更新”的双向联动。在实际框架中,还需要处理模板编译、组件更新队列、异步批处理等复杂逻辑。
四、总结
- Vue 2 使用
Object.defineProperty 实现响应式,结合发布-订阅模式,但存在无法监听属性添加/删除、数组索引变化等限制。
- Vue 3 使用
Proxy 实现响应式,功能更强大,性能更好,且支持更多的数据操作拦截。
- 手动实现双向绑定的核心是:数据劫持(监听数据变化)和 事件监听(监听用户输入),并通过回调或通知机制保持数据与视图的同步。
理解这些原理有助于更好地使用 Vue 框架,并能帮助我们在脱离框架的情况下实现自定义的双向绑定逻辑。