单向绑定很简单:把 Model 绑到 View,JS 改数据,界面自动更新。双向绑定再走一步:用户改了 View(比如填表),Model 也跟着变。Vue 把这套封装在 MVVM 的 ViewModel 里。
本文从关系图讲到 Vue2 时代的 Observer / Compile / Watcher / Dep,并给出一份可运行的精简实现,方便面试时把链路说完整。
| 角色 |
职责 |
对应代码 |
| Model |
数据和业务 |
options.data |
| View |
UI / 模板 |
el 上的 DOM |
| Observer |
劫持属性 |
defineReactive |
| Compiler |
解析指令和插值 |
Compile |
| Watcher + Dep |
依赖收集与通知 |
Dep.target / notify |
一、什么是双向绑定
先看单向:Model 变,View 变。

用户填表时 View 状态更新;若能自动写回 Model,就是双向绑定。

二、双向绑定的原理
Vue 双向绑定由三层构成,合称 MVVM。控制层的核心就是数据双向绑定:
-
-
-
业务逻辑层(ViewModel):把数据和视图关联起来
理解 ViewModel
两件主责:数据变化后更新视图;视图变化后更新数据。内部两块:
-
-
解析器(Compiler):扫描指令,按模板替换数据,绑定更新函数
三、实现双向绑定
以 Vue 为例,流程是:
-
new Vue() 初始化,对 data 做响应化,发生在 Observe 中
-
同时编译模板,找到动态绑定,从 data 取值初始化视图,发生在 Compile 中
-
定义更新函数和 Watcher,数据变了由 Watcher 调更新函数
-
同一个 key 可能出现多次,所以每个 key 需要一个管家 Dep 管理多个 Watcher
-
data 变化先找对应 Dep,通知所有 Watcher 执行更新

响应化 data
构造函数:执行初始化,对 data 响应化、代理到 vm,再编译。
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
对 data 做响应化:
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
编译 Compile
扫描每个元素节点的指令,按模板替换数据,绑定更新函数。

class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值文本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依赖收集
视图用到 data 里某个 key 就叫依赖。同一 key 可能出现多次,每次用一个 Watcher 维护,多个 Watcher 交给 Dep,更新时统一通知。

-
defineReactive 时为每一个 key 创建一个 Dep 实例
-
初始化视图时读取某个 key(例如 name1),创建一个 watcher1
-
触发 name1 的 getter,把 watcher1 加到 name1 对应的 Dep
-
name1 更新时 setter 触发,通过 Dep 通知所有 Watcher
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
声明 Dep:
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
创建 watcher 时触发 getter(精简写法):
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创建 Dep 实例:
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
四、参考文献
-
-
-
原文: https://github.com/febobo/web-interview/issues/2
一句话总结:双向绑定 = Observer 劫持 data + Compile 解析模板 + Watcher 订阅 + Dep 通知;读属性收集依赖,写属性触发更新。