2023-04-13 15:23:50
Vue 的双向绑定通过数据劫持 + 发布者-订阅者模式实现,其核心是利用 Object.defineProperty 劫持数据的 getter/setter,在数据变动时通知订阅者更新视图。以下是具体实现原理的分步解析:
一、核心模块与分工Vue 的双向绑定由三个核心模块协同实现:
Observer(数据监听系统)
遍历数据对象的所有属性,通过 Object.defineProperty 将其转换为 getter/setter。
在 getter 中收集依赖(即订阅者 Watcher),在 setter 中通知依赖更新。
局限性:无法监听对象属性的增删或数组的索引修改(需通过 Vue.set/delete 或重写数组方法解决)。
Compile(指令解析系统)
扫描模板中的指令(如 v-model、{{}})和事件绑定。
根据指令替换数据并绑定更新函数(如将 {{message}} 替换为实际值,并绑定 Watcher 到 message 的更新逻辑)。
生成虚拟 DOM(VNode)供后续对比更新。
Dep + Watcher(发布订阅模型)
Dep(发布者):每个数据属性对应一个 Dep 实例,负责在 getter 中收集依赖(Watcher),在 setter 中触发 notify() 通知所有依赖更新。
Watcher(订阅者):代表一个依赖(如模板中的表达式、watch 选项或计算属性),在数据变化时执行回调函数(如更新视图)。
依赖收集时机:在 Watcher 初始化时,通过 pushTarget(this) 将自身设为全局目标,访问数据时触发 getter,从而被当前 Dep 收集。

数据初始化与劫持
Vue 实例化时,将 data 对象传入,Observer 递归遍历其属性,通过 Object.defineProperty 定义 getter/setter。
示例代码:
function defineReactive(obj, key, val) { const dep = new Dep(); Object.defineProperty(obj, key, { get() { if (Dep.target) dep.addSub(Dep.target); // 收集依赖 return val; }, set(newVal) { if (newVal === val) return; val = newVal; dep.notify(); // 通知更新 } });}依赖收集与触发
收集依赖:Watcher 初始化时调用 get() 方法,触发数据的 getter,此时 Dep.target 为当前 Watcher,被 Dep 实例收集。
触发更新:数据变化时调用 setter,通过 dep.notify() 遍历所有订阅的 Watcher 并执行其回调函数(如重新渲染视图)。
模板编译与虚拟 DOM
Compile 将模板转换为 render 函数,生成虚拟 DOM(VNode)。
首次渲染时,直接根据 VNode 创建真实 DOM;后续更新时,通过 diff 算法对比新旧 VNode 的差异,并应用 patch 到真实 DOM。
Watcher 仅在以下场景中收集依赖并触发更新:
问题排查:若数据变化但视图未更新,需检查:
性能优化
批量更新:Vue 通过异步队列(nextTick)合并多次数据变更,减少重复渲染。
虚拟 DOM:通过 diff 算法最小化真实 DOM 操作。
局限性
对象增删:需通过 Vue.set/delete 手动触发更新。
数组索引修改:需用 splice 或 Vue.set 替代直接赋值。
初始值未变化:即使值未变,setter 仍会触发更新(可通过对比新旧值优化)。

Vue 双向绑定的核心流程:
这一设计实现了数据与视图的自动同步,同时通过模块化架构兼顾了灵活性与性能。如需深入源码,可参考《Vue.js 技术揭秘》或官方文档。