Vue 的 双向数据绑定 是其最核心的特性之一。
你只需在 <input v-model="message">
中绑定一个数据,输入框的值变化时,message
自动更新;反之,message
变化时,输入框也自动刷新。
这背后,是 数据劫持 + 发布-订阅模式 的精妙结合。
本文将带你从零构建一个简易的 Vue
,深入理解 v-model
背后的秘密。
一、双向数据绑定的核心思想
"数据变化 → 视图自动更新;视图交互 → 数据自动同步。"
这正是 MVVM 模式 的精髓:
- M (Model):数据层(
data
) - V (View):视图层(DOM)
- VM (ViewModel):连接层(Vue 实例)
二、四大核心模块
Vue 的双向绑定由四个关键模块协作完成:
模块 | 职责 |
---|---|
Observer | 劫持数据,实现响应式 |
Compile | 编译模板,解析指令 |
Watcher | 订阅者,连接数据与视图 |
MVVM | 入口,整合三大模块 |
三、模块详解与代码实现
✅ 1. Observer:数据劫持,实现响应式
目标:遍历 data
,为每个属性添加 getter/setter
。
js
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(obj, key, val) {
// 递归处理嵌套对象
this.walk(val);
const dep = new Dep(); // 依赖收集器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 新值也可能是对象
this.walk(newVal);
// 派发更新
dep.notify();
}
});
}
}
✅ 2. Dep:依赖收集器(发布者)
每个响应式属性都有一个 Dep
,管理所有依赖它的 Watcher
。
js
class Dep {
constructor() {
this.subs = []; // 存储 Watcher
}
// 添加订阅者
depend() {
Dep.target && this.subs.push(Dep.target);
}
// 通知所有订阅者更新
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 全局唯一 Watcher 标识
Dep.target = null;
✅ 3. Watcher:订阅者,连接数据与视图
Watcher
是 Observer 和 Compile 之间的通信桥梁。
js
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr; // 如 'user.name'
this.cb = cb;
this.value = this.get(); // 首次获取,触发 getter
}
get() {
Dep.target = this; // 标记当前 Watcher
// 读取属性,触发 getter,完成依赖收集
const value = this.getVMVal(this.vm, this.expr);
Dep.target = null; // 清除
return value;
}
update() {
const oldVal = this.value;
const newVal = this.getVMVal(this.vm, this.expr);
if (newVal !== oldVal) {
this.cb(newVal); // 触发回调(如更新视图)
}
}
// 辅助方法:从 vm 中读取嵌套属性
getVMVal(vm, expr) {
return expr.split('.').reduce((obj, key) => obj[key], vm);
}
}
✅ 4. Compile:模板编译,解析指令
负责解析模板中的指令(如 {{}}
、v-model
),并绑定更新函数。
js
class Compile {
constructor(el, vm) {
this.el = document.querySelector(el);
this.vm = vm;
this.fragment = this.nodeToFragment(this.el);
this.compile(this.fragment);
this.el.appendChild(this.fragment);
}
nodeToFragment(el) {
const fragment = document.createDocumentFragment();
let child;
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
compile(node) {
const reg = /\{\{(.*)\}\}/;
if (node.nodeType === 1) { // 元素节点
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const exp = attr.value;
if (attrName === 'v-model') {
// 绑定 input 事件
this.modelUpdater(node, exp);
node.addEventListener('input', e => {
const value = e.target.value;
this.setVMVal(this.vm, exp, value); // 视图 → 数据
});
// 创建 Watcher,数据 → 视图
new Watcher(this.vm, exp, value => {
node.value = value;
});
}
});
} else if (node.nodeType === 3 && reg.test(node.nodeValue)) { // 文本节点
const exp = RegExp.$1.trim();
const value = this.getVMVal(this.vm, exp);
node.nodeValue = value;
// 创建 Watcher
new Watcher(this.vm, exp, value => {
node.nodeValue = value;
});
}
// 递归子节点
if (node.childNodes && node.childNodes.length) {
node.childNodes.forEach(child => this.compile(child));
}
}
// 辅助方法
getVMVal(vm, exp) {
return exp.split('.').reduce((obj, key) => obj[key], vm);
}
setVMVal(vm, exp, value) {
exp.split('.').reduce((obj, key, index, arr) => {
if (index === arr.length - 1) {
obj[key] = value;
}
return obj[key];
}, vm);
}
modelUpdater(node, exp) {
const value = this.getVMVal(this.vm, exp);
node.value = value;
}
}
✅ 5. MVVM:整合者
js
class MVVM {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
// 代理 this.$data 到 this
Object.keys(this.$data).forEach(key => {
this.proxyData(key);
});
// 响应式系统
new Observer(this.$data);
// 编译模板
new Compile(this.$el, this);
}
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
四、使用示例
html
<div id="app">
<input v-model="message" />
<p>{{ message }}</p>
</div>
js
new MVVM({
el: '#app',
data: {
message: 'Hello MVVM'
}
});
✅ 运行流程
-
初始化:
Observer
劫持message
;Compile
解析模板,发现v-model
和{{}}
;- 为
message
创建Watcher
,绑定更新函数。
-
数据变化:
jsvm.message = 'New Value';
- 触发
setter
→dep.notify()
→Watcher.update()
→ 视图更新。
- 触发
-
视图交互:
- 用户在输入框输入;
- 触发
input
事件 →setVMVal()
→vm.message
被修改 → 触发setter
→ 视图更新。
五、Vue 3 的改进
Vue 3 使用 Proxy
替代 Object.defineProperty
,解决了:
- 无法监听属性新增/删除;
- 数组索引修改;
- 初始化性能问题。
但核心思想 "数据劫持 + 发布-订阅" 依然不变。
💡 结语
"双向绑定不是魔法,而是设计模式的优雅组合。"
通过 Observer
、Compile
、Watcher
和 Dep
的协作,Vue 实现了:
- 数据驱动视图;
- 视图反馈数据;
- 开发者零手动 DOM 操作。
理解其原理,不仅能写出更高质量的 Vue 代码,更能提升你的架构思维。