前端知库

MVVM 是什么?Vue 是怎样体现 MVVM 的?

MVVM 把界面分成 Model(数据)、View(界面)和 ViewModel(连接两者的那一层),ViewModel 通过数据绑定让数据变化自动反映到界面,界面上的操作再改回数据。Vue 2 官方文档说 Vue 并不严格对应 MVVM,只是设计上部分受它启发,并习惯用 vm 指代组件实例;Vue 3 文档不再用 MVVM 来介绍,而是讲声明式渲染和响应式。

参考答案

简答

写过 jQuery 的人都知道,数据变了要自己找到 DOM 去改,用户输入了还要自己去读 DOM 再写回数据。MVVM 想解决的就是这件事:把界面分成三层,Model 是数据,View 是界面,中间加一层 ViewModel,负责把数据绑定到界面上,并把界面上的操作转成对数据的修改。开发者只管改数据,界面跟着变。

在 Vue 里大致可以这样对应:组件的 data 或 ref、reactive 状态是 Model,模板是 View,组件实例就是 ViewModel。模板里的 {{ }} 和 v-bind 把数据显示出来,v-on 把用户操作变成方法调用,v-model 两个方向都做。

不过要准确地说。Vue 2 官方文档的原话是:Vue 虽然没有完全遵循 MVVM 模式,但设计上部分受到了它的启发,所以习惯用变量名 vm(ViewModel 的缩写)表示 Vue 实例。Vue 3 的文档已经不用 MVVM 来介绍了,它把 Vue 的核心概括为两点:声明式渲染和响应式。

所以面试时可以先讲 MVVM 的三层和数据绑定,再说明 Vue 是“受 MVVM 启发”,背后靠的是响应式系统追踪依赖、自动更新 DOM,而不是说“Vue 就是 MVVM 框架”。

MVVM 的三层各管什么

Model 是业务数据,比如购物车里的商品列表。它不知道界面长什么样。

View 是用户看到的界面,比如列表和按钮。它只描述“数据显示成什么样子”,不写“怎么去改 DOM”。

ViewModel 是两者之间的桥。它持有 View 需要的数据和方法,通过数据绑定把数据推给 View;View 上发生点击、输入时,调用 ViewModel 的方法去改数据。因为有这层绑定,View 和 Model 不用直接打交道。

用 Vue 的代码对一下

Vue 3 官方文档入门的第一个例子就能看出这三层:

JavaScript
import { createApp } from 'vue';

createApp({
  data() {
    return { count: 0 };
  },
}).mount('#app');
HTML
<div id="app">
  <button @click="count++">Count is: {{ count }}</button>
</div>

count 是数据,模板是界面,createApp 创建并挂载的组件实例把两者连起来。点按钮时,@click 改的是 count,没有任何代码去找按钮改文字,按钮上的数字却变了。

Vue 3 文档对此的说法是:声明式渲染,用基于 JavaScript 状态的模板语法描述 HTML 输出;响应式,自动追踪 JavaScript 状态的变化,在变化时高效地更新 DOM。这两点正是 MVVM 里“数据绑定”在 Vue 中的实现方式。

数据变了界面为什么会自己变

关键在 ViewModel 怎么知道“数据变了,哪块界面要更新”。下面用 Proxy 写一个极简版本:读数据时记下是谁在用,改数据时通知它们重新执行。

JavaScript
function reactive(target) {
  const subscribers = new Set();
  const proxy = new Proxy(target, {
    get(obj, key) {
      if (activeEffect) subscribers.add(activeEffect);
      return obj[key];
    },
    set(obj, key, value) {
      obj[key] = value;
      subscribers.forEach((effect) => effect());
      return true;
    },
  });
  return proxy;
}

let activeEffect = null;
function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

// Model
const state = reactive({ count: 0 });

// View:用一个对象代替 DOM 节点
const view = { text: '' };
effect(() => {
  view.text = `Count is: ${state.count}`;
});
console.log(view.text); // Count is: 0

// 用户点击按钮时,事件处理只改数据
function onClick() {
  state.count++;
}
onClick();
onClick();
console.log(view.text); // Count is: 2

effect 第一次执行时读了 state.count,get 就把这个“渲染函数”记了下来。后来 onClick 改了 count,set 通知它重新执行,view.text 就更新了。onClick 本身完全没碰 view。

这个例子做了很多简化:真实的 Vue 按对象和属性分别记录依赖,组件更新会被缓冲后批量执行,渲染函数生成的是虚拟 DOM 再比较差异。完整过程见《Vue 3 响应式依赖是怎样收集和触发的?》。

为什么说 Vue 不是严格的 MVVM

经典 MVVM 里,ViewModel 是一个单独的层,View 通过绑定只和它打交道。Vue 2 文档也只说“部分受到启发”,没有把自己归为 MVVM 框架。

实际写 Vue 时,边界没那么严格。组件实例同时承担了 ViewModel 和一部分 View 的职责,模板就写在组件里;组件也可以通过模板 ref 直接操作 DOM,这在纯粹的 MVVM 里是 View 和 ViewModel 的越界。Vue 3 的组合式 API 更是把状态、计算和副作用按功能组织,不再强调分层。

所以更贴切的说法是:Vue 借用了 MVVM 的数据绑定思想,核心实现是响应式系统加模板编译。

常见追问

MVVM 和 MVC 有什么区别?

MVC 里 Controller 接收用户输入,去改 Model,再决定更新哪个 View,更新界面通常是手动的。MVVM 用数据绑定取代了这部分手动工作:ViewModel 的数据一变,View 自动更新,View 上的输入也能自动写回。

v-model 是双向绑定,那 Vue 是双向数据流吗?

不是。v-model 只是语法糖,在原生输入框上相当于绑定 value 加监听 input 事件,数据仍然从父组件单向流向子组件,子组件通过事件通知父组件修改,见《组件 v-model 的本质是什么?》。