Vue 的双向绑定是其核心特性,它允许数据与视图自动同步:数据变化时视图更新,视图变化时数据也更新。下面我将基于 Vue 2 的实现原理(使用 Object.defineProperty
),手把手带你实现一个简易版双向绑定。整个过程基于数据劫持 和发布-订阅模式 ,核心包括 Observer
(监听数据)、Dep
(依赖管理)、Watcher
(订阅更新)和 Compile
(模板解析)四个部分。
🔧 核心原理概述
- 数据劫持 :通过
Object.defineProperty
拦截数据的读取(getter)和设置(setter),在 setter 中检测变化并通知更新。 - 发布-订阅模式 :每个响应式属性都有一个
Dep
实例来管理依赖它的Watcher
。数据变化时,Dep
通知所有Watcher
执行更新函数。 - 模板编译 :
Compile
解析模板中的指令(如v-model
),初始化视图并绑定事件监听器,将数据与 DOM 元素关联。
⚙️ 分步实现代码
以下是一个最小化实现,包含关键类和方法。代码基于 Vue 2 风格,但进行了简化以便理解。
1. 实现 Observer(数据劫持)
Observer
递归遍历数据对象,为每个属性添加 getter/setter:
javascript
// 数据劫持函数
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每个属性对应一个 Dep 实例
// 递归处理嵌套对象
if (typeof val === 'object' && val !== null) {
observe(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`访问属性 ${key}: ${val}`);
// 依赖收集:如果当前有 Watcher,则添加到 Dep
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) return;
console.log(`更新属性 ${key}: 从 ${val} 变为 ${newVal}`);
val = newVal;
// 通知所有订阅者更新
dep.notify();
}
});
}
// 遍历对象属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
2. 实现 Dep(依赖管理)
Dep
负责收集依赖(Watcher)并在数据变化时通知它们:
javascript
class Dep {
constructor() {
this.subs = []; // 存储 Watcher 实例
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null; // 全局变量,指向当前正在计算的 Watcher
3. 实现 Watcher(订阅者)
Watcher
是 Observer 和 Compile 之间的桥梁,在数据变化时触发更新:
kotlin
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb; // 更新回调函数
Dep.target = this; // 设置当前 Watcher
this.vm[this.key]; // 触发 getter,收集依赖
Dep.target = null; // 收集完成后重置
}
update() {
this.cb.call(this.vm, this.vm[this.key]);
}
}
4. 实现 Compile(模板编译)
Compile
解析 DOM 模板,处理指令并绑定事件:
ini
function compile(el, vm) {
const nodes = el.children;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// 递归处理子节点
if (node.children.length) {
compile(node, vm);
}
// 处理 v-model 指令(双向绑定)
if (node.hasAttribute('v-model')) {
const key = node.getAttribute('v-model');
node.value = vm[key]; // 初始化视图
// 数据 -> 视图:创建 Watcher,数据变化时更新 input 值
new Watcher(vm, key, (newVal) => {
node.value = newVal;
});
// 视图 -> 数据:监听 input 事件
node.addEventListener('input', (e) => {
vm[key] = e.target.value;
});
}
// 处理文本插值(如 {{ message }})
if (node.nodeType === 3 && /{{(.*)}}/.test(node.textContent)) {
const key = RegExp.$1.trim();
node.textContent = vm[key]; // 初始化
new Watcher(vm, key, (newVal) => {
node.textContent = newVal;
});
}
}
}
5. 整合为 MiniVue 类
将以上部分组合成一个简易 Vue 实例:
kotlin
class MiniVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 数据劫持
observe(this.$data);
// 代理数据:支持直接通过 vm.message 访问(而非 vm.$data.message)
this._proxyData();
// 编译模板
compile(document.querySelector(options.el), this);
}
_proxyData() {
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
get: () => this.$data[key],
set: (newVal) => { this.$data[key] = newVal; }
});
});
}
}
💻 完整使用示例
创建一个 HTML 文件测试上述代码:
xml
<div id="app">
<input type="text" v-model="message">
<p>{{ message }}</p>
</div>
<script>
// 此处插入以上所有 JavaScript 代码(Observer、Dep、Watcher、Compile、MiniVue)
const vm = new MiniVue({
el: '#app',
data: { message: 'Hello, Vue!' }
});
</script>
-
效果 :在输入框输入时,
<p>
标签内容会实时同步。 -
关键流程:
- 数据 -> 视图 :修改
vm.message
时,触发 setter → Dep.notify() → Watcher.update() → 更新 DOM。 - 视图 -> 数据 :输入框触发
input
事件 → 修改vm.message
→ 触发 setter → 循环上述流程。
- 数据 -> 视图 :修改
⚠️ 注意事项与局限性
- Vue 2 的局限 :
Object.defineProperty
无法检测新增属性(需用Vue.set
)或数组索引变化(需重写数组方法)。Vue 3 已改用Proxy
解决这些问题。 - 简化版缺陷:本例未实现虚拟 DOM、组件系统等,实际 Vue 更复杂(如 Diff 算法优化性能)。
- 扩展建议 :可添加事件指令(如
v-on
)和计算属性,原理类似(通过 Watcher 依赖追踪)。
通过这个手写实现,你能更深入理解 Vue 的响应式本质。实际开发中推荐直接使用 Vue 框架,但掌握原理有助于调试和优化。