0 到 1 掌握:Vue 核心之数据双向绑定
Category(分类): Vue Status: 已校订(Vue 2 原理 + Vue 3 更新)
前言
当被问到 Vue 数据双向绑定原理时,很多人会脱口而出:Vue 通过 Object.defineProperty 把 data 属性转换成 getter/setter,数据变化时通知视图更新。这句话描述的是 Vue 2 的响应式基础,而不是所有版本的 Vue,也还没有覆盖完整的“双向绑定”。
- 响应式更新(Data → View):读取状态时收集依赖,写入状态时通知渲染更新。
- 视图写回(View → Data):由 DOM 事件或组件事件完成,
v-model只是把属性绑定与事件监听组合起来。
本文保留原文 Observer、Dep、Watcher、Compile 的 Vue 2 教学思路,并补充 Vue 3 在 2026 年仍适用的机制。Vue 2 已于 2023-12-31 EOL,新项目应优先使用 Vue 3。

原作者项目汇总:github.com/fengshi123/blog。
一、什么是 MVVM 数据双向绑定
MVVM 中常说的双向绑定包含两条方向不同的链路:

- 输入框内容变化时,事件处理器把值写回状态,即 View → Data。
- 状态变化后,响应式系统触发组件重新渲染,即 Data → View。
以文本输入框为例,下面两种写法表达了同一个核心意图:
<input v-model="name">
<!-- 概念上的展开形式;不同表单控件使用的属性和事件并不完全相同 -->
<input :value="name" @input="name = $event.target.value">
Vue 3 组件上的 v-model 默认对应 modelValue prop 和 update:modelValue 事件;Vue 3.4+ 还可使用 defineModel()。因此,getter/setter 或 Proxy 主要解决 Data → View,事件才补上 View → Data。
原文的教学实现可拆成四个角色:
Observer:拦截数据的读写;Dep:管理依赖;Watcher:读取数据以建立依赖,并在变化后执行更新;Compile:解析模板、创建绑定,并为v-model等指令注册事件。

原始演示源码:mvvm_example。需要注意,该教学项目不是完整 Vue 源码。
二、Vue 2 的监听器 Observer
2.1 Object.defineProperty()
Object.defineProperty(obj, prop, descriptor) 可以定义或修改对象属性,并返回传入的对象。obj 是目标对象,prop 是属性名,descriptor 是属性描述符。
描述符分为两类,不能把 value / writable 与 get / set 混用:
- 两类共有:
configurable决定描述符能否再次修改以及属性能否删除,enumerable决定属性能否出现在枚举中;二者默认都是false。 - 数据描述符:
value是属性值,默认undefined;writable决定能否用赋值改变值,默认false。 - 存取描述符:
get在读取时执行,默认undefined;set在赋值时接收新值,默认undefined。访问器中的this取决于实际接收者,并不总是最初定义属性的对象。
教学代码若遗漏描述符选项,可能意外改变原字段的枚举、配置行为。一个最小示例:
let value = 'tom'
const person = {}
Object.defineProperty(person, 'name', {
enumerable: true,
configurable: true,
get() {
console.log('name 属性被读取了')
return value
},
set(newValue) {
console.log('name 属性被修改了')
value = newValue
},
})

2.2 浅层教学实现
下面的代码仅演示“遍历已有属性并拦截读写”,不是 Vue 2 的等价实现:
function observable(obj) {
if (!obj || typeof obj !== 'object') return obj
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj
}
function defineReactive(obj, key, initialValue) {
let value = initialValue
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`${key} 属性被读取了`)
return value
},
set(newValue) {
if (Object.is(newValue, value)) return
value = newValue
console.log(`${key} 属性被修改了`)
},
})
}
const person = observable({ name: 'tom', age: 15 })
真实 Vue 2 还会递归观测初始化时存在的对象、增强数组变异方法、处理原 accessor,并在赋入新对象时继续观测。它也有明确限制:任意已经观测的普通对象,直接新增或删除属性都不会被现有属性的 getter/setter 捕获。边界要分开看:
- 对嵌套响应式对象,可用
Vue.set(object, key, value)/vm.$set新增响应式属性,用Vue.delete(object, key)删除并通知依赖; - 对 Vue 实例本身或根
$data,运行时不能用Vue.set/Vue.delete动态增删根级响应式属性,应在初始化时声明;若需要动态字段,预先放进一个嵌套对象; - 数组索引赋值和直接修改
length也有检测限制,使用Vue.set(items, index, value)或splice()。
这些都是 Vue 2 历史限制;Vue 3 的 Proxy 机制不同。
三、Dep:依赖管理
可把 Vue 2 的依赖系统类比成观察者/发布订阅模式:属性在 getter 中登记当前 watcher,在 setter 中通知依赖它的 watcher。严格说,Vue 2 的 Dep 更接近直接维护观察者依赖,并不等于带事件中心的通用发布订阅系统;这个类比仍有助于理解一对多通知。
原作者用售楼处说明这一点:若每位购房者每天打电话询问尾盘,双方高度耦合;改为购房者留下号码(订阅),楼盘推出时售楼处遍历名单发消息(通知),发布者无需逐个硬编码调用购买者。它的教学优点是:
- 发布者只负责在状态变化时通知约定的订阅者,减少对象之间的硬编码调用;
- 新增订阅者通常不要求修改发布者,双方只依赖约定的通知机制;
- 在异步编程里,调用者可只关注成功、失败等事件,而不必持续查询对象内部状态。
类比不能替代源码事实:Vue 的 getter 依赖收集会记录“本次求值实际读取了什么”,并在后续求值中清理失效依赖。
class Dep {
constructor() {
this.subs = new Set()
}
depend() {
if (Dep.target) this.subs.add(Dep.target)
}
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
Dep.target = null
真实源码不是简单地把一个 watcher 永久放在全局变量中。Dep.target 表示“当前正在求值的 watcher”,嵌套求值时通过 pushTarget()/popTarget() 栈恢复;Watcher 还会去重并清理已失效依赖。
把 Dep 放进简化版属性拦截:
function defineReactive(obj, key, initialValue) {
const dep = new Dep()
let value = initialValue
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.depend()
return value
},
set(newValue) {
if (Object.is(newValue, value)) return
value = newValue
dep.notify()
},
})
}
四、Watcher:收集依赖并更新
Watcher 初始化时读取目标属性,从而触发 getter 和 dep.depend():
const targetStack = []
function pushTarget(watcher) {
targetStack.push(watcher)
Dep.target = watcher
}
function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1] ?? null
}
class Watcher {
constructor(data, key, callback) {
this.data = data
this.key = key
this.callback = callback
this.value = this.get()
}
get() {
pushTarget(this)
try {
return this.data[this.key]
} finally {
popTarget()
}
}
update() {
const value = this.data[this.key]
if (!Object.is(value, this.value)) {
const oldValue = this.value
this.value = value
this.callback(value, oldValue)
}
}
}
这仍是教学简化版:只支持顶层单字段,没有依赖清理、computed、deep/lazy/user watcher,也没有调度队列。真实 Vue 2 的普通更新会进入 queueWatcher 批处理,而不是在 setter 中同步完成 DOM 更新。
五、Compile 与真正的 v-model
Compile 会初始化文本节点并创建 Watcher:
function bindText(node, data, key) {
node.textContent = String(data[key] ?? '')
new Watcher(data, key, (value) => {
node.textContent = String(value ?? '')
})
}
textContent 与 Vue 插值的纯文本语义一致;不要把不可信数据直接交给 innerHTML。如果要实现 v-model,还必须增加 View → Data 的事件回路:
function bindTextInput(input, data, key) {
input.value = data[key] ?? ''
input.addEventListener('input', (event) => {
data[key] = event.target.value
})
new Watcher(data, key, (value) => {
input.value = value ?? ''
})
}
因此,仅有 Observer、Dep、Watcher 和插值替换只能称为“响应式渲染”;加入事件监听后才构成这个简化文本输入场景的双向绑定。
六、Vue 2 源码主链路
以 Vue 2.6.14 为例:
initState()调用initData();initData()把vm._data.xxx代理为vm.xxx,随后调用observe(data);Observer遍历对象 key 并调用defineReactive(),数组则增强变异方法并观测数组元素;- 渲染 watcher 执行 getter,本次 render 实际读取到的字段触发依赖收集;
- setter 调用
dep.notify(),watcher 通常进入异步更新队列; - 下一轮 flush 执行 render 和 patch,更新 DOM。
真实链路是 getter → dep.depend() → Dep.target.addDep(dep)。Watcher.addDep() 使用集合去重,并会在求值后清理不再使用的依赖。不能把简化版的 dep.addSub(Dep.target) 当作完整源码。

七、原文 Vue 2 历史源码走读
版本提示: 本节固定在 Vue 2.6.14 的历史实现。为恢复原文教学链路,保留 Flow 类型源码的关键完整函数,并修正原抓取中的
xport、粘连注释等问题。Vue 2 已 EOL;这些内部 API 也不是业务代码可依赖的公共接口。
7.1 从 initState 到 initData
组件初始化时,initState() 按 props、methods、data、computed、watch 的顺序初始化状态:
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initData() 先取得 data 对象,检查与 methods/props 的重名,再把 vm._data.xxx 代理为 vm.xxx,最后观测整个 data:
function initData(vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm,
)
}
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
'Use prop default value instead.',
vm,
)
} else if (!isReserved(key)) {
proxy(vm, '_data', key)
}
}
observe(data, true /* asRootData */)
}
7.2 observe、Observer 与 defineReactive
observe() 会复用已有 __ob__,只在满足条件时创建 Observer:
export function observe(value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) return
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) ob.vmCount++
return ob
}
Observer 为对象遍历已有 key;数组则增强变异方法并递归观测数组项:
export class Observer {
value: any
dep: Dep
vmCount: number
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk(obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
每个属性各自持有一个 Dep。getter 保留原 accessor、收集当前 watcher,并为子对象/数组补充依赖;setter 比较值、调用原 setter 或保存新值、观测新对象后通知:
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean,
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) return
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) val = obj[key]
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) dependArray(value)
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) return
if (process.env.NODE_ENV !== 'production' && customSetter) customSetter()
if (getter && !setter) return
if (setter) setter.call(obj, newVal)
else val = newVal
childOb = !shallow && observe(newVal)
dep.notify()
},
})
}
7.3 Dep 与 Watcher
原文 Dep.target “全局唯一”的说法要结合 target 栈理解:任一时刻只有当前求值 watcher 是 target,但嵌套求值会通过 pushTarget / popTarget 保存和恢复,而不是求值后永远简单置空。
export default class Dep {
static target: ?Watcher
id: number
subs: Array<Watcher>
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: Watcher) { this.subs.push(sub) }
removeSub(sub: Watcher) { remove(this.subs, sub) }
depend() { if (Dep.target) Dep.target.addDep(this) }
notify() {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) subs[i].update()
}
}
Dep.target = null
Watcher 构造函数会把表达式转换成 getter,并在非 lazy 时立即 get();真实类还有 teardown、deep traverse、依赖清理等代码:
export default class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) vm._watcher = this
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid
this.active = true
this.dirty = this.lazy
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn)
if (!this.getter) this.getter = noop
this.value = this.lazy ? undefined : this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
if (this.deep) traverse(value)
popTarget()
this.cleanupDeps()
}
return value
}
addDep(dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) dep.addSub(this)
}
}
update() {
if (this.lazy) this.dirty = true
else if (this.sync) this.run()
else queueWatcher(this)
}
}
因此完整路径是:render watcher 求值 → 实际读取字段 getter → dep.depend() → watcher.addDep(dep);赋值触发 setter → dep.notify() → watcher 进入队列 → 下一轮 flush 重新 render/patch。
上文 01-05 原理图概括了这条 Vue 2 数据响应主链路。
八、原文 Compile 到完整演示(Vue 2 风格教学实现)
下例恢复原文四角色闭环,但仍是历史教学代码,不是 Vue 2 源码。它只支持简单
{{ key }}、v-model和v-on:click,不处理嵌套路径、组件、虚拟 DOM、指令修饰符和销毁清理。
function Compile(el, vm) {
this.vm = vm
this.el = document.querySelector(el)
this.compile(this.el)
}
Compile.prototype.compile = function (node) {
Array.from(node.childNodes).forEach((child) => {
if (child.nodeType === 3) {
const match = child.textContent.match(/\{\{\s*([\w$]+)\s*\}\}/)
if (match) this.compileText(child, match[1])
} else if (child.nodeType === 1) {
Array.from(child.attributes).forEach((attr) => {
if (attr.name === 'v-model') {
const key = attr.value
child.value = this.vm[key]
child.addEventListener('input', (event) => {
this.vm[key] = event.target.value
})
new Watcher(this.vm, key, (value) => { child.value = value })
}
if (attr.name === 'v-on:click') {
child.addEventListener('click', this.vm.$methods[attr.value].bind(this.vm))
}
})
this.compile(child)
}
})
}
Compile.prototype.compileText = function (node, key) {
node.textContent = this.vm[key]
new Watcher(this.vm, key, (value) => { node.textContent = value })
}
完整页面把 Observer、Dep、Watcher、Compile 串联起来:
<!doctype html>
<html lang="zh-CN">
<body>
<div id="mvvm-app">
<input v-model="title">
<h2>{{ title }}</h2>
<button v-on:click="clickBtn">数据初始化</button>
</div>
<!-- 原作者项目构建出的 bundle 包含 Observer、Dep、Watcher、Compile 与 MVVM -->
<script src="../dist/bundle.js"></script>
<script>
const vm = new MVVM({
el: '#mvvm-app',
data: { title: 'hello world' },
methods: {
clickBtn() { this.title = 'hello world' },
},
})
</script>
</body>
</html>
输入事件完成 View → Data,setter/Watcher 完成 Data → View,按钮则演示方法再次写状态。文首的原文效果图 01-01 展示的正是这一简化双向绑定演示结果。
九、Vue 3:Proxy、ref 与 effect
Vue 3 不再以 Vue 2 的遍历 accessor 作为主要对象响应式机制:
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 对象拦截 | 遍历已有 key,使用 Object.defineProperty | reactive() 返回 Proxy |
| 基本值容器 | 非核心对象模型 | ref() 通过 .value getter/setter 跟踪和触发 |
| 依赖主体 | Dep / Watcher | track / trigger / ReactiveEffect |
| 新增、删除 | 有检测限制,常需 Vue.set/Vue.delete | 通过 Proxy 的 set/delete 等 trap 处理 |
| 数组、集合 | 数组方法增强;索引与 length 有限制 | 支持数组,并为 Map/Set 等集合提供 handler |
官方概念模型可以简化为:
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
})
}
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(this, 'value')
},
}
}
这也是概念伪代码。真实实现会处理缓存、只读、浅层、数组和集合、相同值判断、effect 调度等大量边界。reactive(raw) !== raw,应持续使用返回的 Proxy;普通 ref 包裹对象时,内部对象会转为深层 reactive。
十、总结
- 响应式系统负责 Data → View;
v-model用事件补齐 View → Data。 Object.defineProperty是 Vue 2 的核心实现,Vue 3 的reactive使用 Proxy,ref的.value使用 getter/setter。- Observer、Dep、Watcher、Compile 适合解释 Vue 2 的主链路,但教学代码不等于 Vue 源码。
- Vue 2 已 EOL;维护旧项目时要牢记新增属性、数组操作和异步更新队列等限制,新项目应以 Vue 3 官方文档为准。
官方参考
- Vue 3:深入响应式系统
- Vue 3:响应式基础
- Vue 3:表单输入绑定
- Vue 3:组件
v-model - Vue 2:深入响应式原理
- Vue 2.6.14:Observer /
defineReactive - Vue 2.6.14:Dep
- Vue 2.6.14:Watcher
- Vue 2 EOL
- MDN:Object.defineProperty
作者:随风而逝_风逝 原文链接:https://juejin.cn/post/6844903903822086151 来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。