Vue 是如何实现 MVVM 的?
Vue 实现 MVVM(Model-View-ViewModel)的核心原理可以概括为:数据劫持(Data Hijacking)结合发布-订阅模式(Publisher-Subscriber Pattern)。
简单来说,Vue 通过劫持数据对象的属性读写,在数据发生变化时通知订阅者,触发视图更新。
以下是详细的实现机制,主要包含三个核心模块:Observer(监听器)、Compiler(编译器) 和 Watcher(订阅者)。
1. 核心模块解析
A. Observer(数据监听器)
它的作用是把一个普通的 JavaScript 对象转换成响应式对象。
- Vue 2.x 实现: 使用
Object.defineProperty()。Vue 会遍历data选项中的所有属性,为每个属性添加getter和setter。- Getter: 用于依赖收集。当视图读取某个属性时,Getter 被触发,Vue 记下“谁在这个时候用了我”。
- Setter: 用于派发更新。当属性值被修改时,Setter 被触发,Vue 通知之前记下的所有依赖“我变了,你们该更新了”。
- Vue 3.x 实现: 使用 ES6 的
Proxy。它可以直接代理整个对象,而不是遍历属性,性能更好且能监听数组索引变化和对象属性的添加/删除。
B. Compiler(指令解析器/编译器)
它的作用是解析模板指令,将模板中的变量替换成数据。
- 它扫描 DOM 节点,解析
{{}}插值表达式和v-开头的指令(如v-model,v-on)。 - 将模板中的变量与数据关联起来。
- 为每个包含动态数据的节点创建 Watcher,一旦数据变化,Watcher 就会收到通知并更新视图。
C. Watcher(订阅者/观察者)
它是 Observer 和 Compiler 之间的桥梁。
- 连接点: 在 Compiler 解析模板时,会为每个数据绑定生成一个 Watcher。
- 触发 Getter: Watcher 在初始化时会读取一次数据,从而触发 Observer 的 Getter,将自己添加到该数据的依赖容器(Dep)中。
- 接收通知: 当数据变化触发 Setter 时,Dep 会通知 Watcher。
- 执行更新: Watcher 收到通知后,调用自身的
update()方法,触发回调函数(通常是 Virtual DOM 的 patch 过程)来更新视图。
D. Dep(依赖管理器)
这是一个专门用来管理 Watcher 的容器。
- 每个响应式属性都有一个对应的 Dep 实例。
- Dep 内部维护一个数组,用来存放所有依赖该属性的 Watcher。
- 当数据变化时,Dep 负责遍历数组,通知所有 Watcher。
2. 整体工作流程
- 初始化(Init): Vue 实例启动,遍历
data数据。 - 劫持(Observe): 利用
Object.defineProperty(Vue 2) 或Proxy(Vue 3) 将数据转化为响应式,定义 Getter/Setter。 - 编译(Compile): 解析模板指令,将模板中的变量替换成数据,并初始化渲染页面。
- 依赖收集(Dependency Collection):
- 在编译过程中,实例化
Watcher。 Watcher读取数据 -> 触发Getter-> 将Watcher添加到该数据的Dep中。
- 在编译过程中,实例化
- 派发更新(Notify):
- 用户修改数据 -> 触发
Setter。 Dep收到信号,通知所有订阅的Watcher。Watcher调用update方法,更新视图(DOM)。
- 用户修改数据 -> 触发
3. 双向绑定(v-model)是如何实现的?
MVVM 的显著特征是双向绑定。在 Vue 中,v-model 实际上是一个语法糖。
它结合了两个操作:
- 数据绑定(Model -> View): 通过上述的 Observer/Watcher 机制,将数据绑定到 input 元素的
value属性上。 - 事件监听(View -> Model): 在 input 元素上添加
input(或change)事件监听器。当用户输入内容时,触发事件,在回调函数中将 input 的新值赋值给 data 中的对应属性。
代码示例(简化版):
html
<input v-model="message">
<!-- 等同于 -->
<input :value="message" @input="message = $event.target.value">
4. Vue 2 与 Vue 3 实现的区别
这是一个常见的面试追问点:
| 特性 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 实现方式 | 递归遍历对象属性,逐个定义 getter/setter | 代理整个对象,拦截对象的所有操作 |
| 对象新增属性 | 无法监听(需用 Vue.set) |
可以监听 |
| 对象删除属性 | 无法监听(需用 Vue.delete) |
可以监听 |
| 数组监听 | 无法监听索引和长度变化(重写了数组 7 个方法) | 可以完美监听数组变化 |
| 性能 | 初始化时递归遍历,大对象性能较差 | 懒代理(访问时才代理),性能更好 |
总结
Vue 实现 MVVM 的核心在于:通过 Observer 劫持数据,通过 Compiler 解析模板,利用 Dep 和 Watcher 进行依赖收集和派发更新,从而实现数据驱动视图(Data Driven View)。而双向绑定则是通过在表单元素上自动添加事件监听来实现的。
右滑查看面试常问