MV-02 MVVM思想
02 · MVVM 思想(MVVM Architecture)
📅 预计 70 分钟 | ⭐ = 高频考点 | 📌 中英术语见文末
✍️ 配套练习:网页练习shengxia.dev/quiz?module=mv02
1.0 先给直觉:从"手动发传单"到"自动订阅"
想象你经营一家餐厅。传统做法是:每次菜单价格变动,你都得手动跑遍每张桌子,撕掉旧传单、贴上新的,客人点了已下架的菜才发现。这就是最原始的前端——用 JS 手动操作 DOM,数据一变,你得亲自找到页面上的每一处并更新。
MVVM 的做法是:给菜单贴上"价格一改,桌上自动更新"的标签。你只管改数据,页面自己知道怎么变。这就叫数据驱动视图(data-driven view)——视图是数据的函数,数据变了,视图跟着变,不需要你指挥。
💡 记忆口诀:传统前端"人拉车",MVVM"自动驾驶";改数据,页面自己动。
1.1 前端演进:为什么需要框架
从静态页到交互页
早期网页是静态的——服务器返回写死的 HTML,浏览器显示,用户只能看不能改。后来 JS 普及,页面能响应用户操作,但所有逻辑都是"找元素 → 改内容"的命令式写法。
// 命令式:每一步都亲自指挥
const list = document.querySelector('#list');
const newLi = document.createElement('li');
newLi.textContent = '新项目';
list.appendChild(newLi);
这种写法在小页面没问题。但页面一大,问题就来了:数据和 DOM 之间的同步散落在各处。改了数据忘了改某处 DOM,页面就出现"数据和显示对不上"的 bug,而且很难排查。
框架解决的核心问题
前端框架(Vue、React)统一解决一件事:如何高效地把数据变成页面,并保持同步。它们提供一套"声明式"写法——你声明"页面上应该显示什么",框架负责"怎么去更新 DOM"。就像你告诉厨师"来一份番茄炒蛋",厨师自己完成买菜、切菜、下锅,你不必指挥每一步。
⚠️ 常见错误
- 以为框架能取代 HTML/CSS:框架最终生成的还是 DOM,基础三件套是地基,不能跳。
- 命令式思维硬套框架:拿到框架还是用
querySelector一顿猛改 DOM,等于开自动挡还忍不住踩离合。 - 忽视"数据是唯一事实来源":数据和 DOM 各存一份,改了数据却不去同步,bug 由此而生。
1.2 MVC:最初的"分工方案"
三个角色各管一段
MVC 把一个应用拆成三层:
- Model(模型):数据与业务规则,比如"用户列表""计算总分"。它不知道页面长什么样。
- View(视图):用户看到的界面。它只管展示。
- Controller(控制器):接收用户输入,指挥 Model 更新数据、再让 View 刷新。
流程是"用户操作 → Controller → 改 Model → 刷新 View"。Controller 是"中间人",View 和 Model 通常不直接对话。
用户点击 --> Controller --> Model 改数据 --> View 刷新
问题:Controller 越来越臃肿
MVC 适合后端(服务器),Controller 在服务端处理请求、组装数据很自然。但搬到前端后,View 也是 JS 写的,Controller 里要写大量"数据变了 → 手动更新哪块 DOM"的代码,同步逻辑依旧没人替你管,Controller 变成一个大杂烩。
⚠️ 常见错误
- 把逻辑全塞进 View:页面里堆满数据和业务判断,想改需求要翻遍模板。
- Controller 里直接写死 DOM 操作:Model 一改就要手动查 DOM,同步靠人肉维护,必然漏。
- 混淆职责:Model 里写 UI 逻辑("如果金额大于100就显示红色"),数据层不该知道"红色"。
1.3 MVP:让 View 变"哑巴"
Presenter 接管全部逻辑
MVP(Model-View-Presenter)把 Controller 换成 Presenter(主持人),并且做得更彻底:View 只负责被动展示,所有交互逻辑都进 Presenter。View 通过接口暴露方法(如 showName(name)),Presenter 调用这些方法更新界面。
View 点击 --> Presenter --> Model
^ |
+-------- 调 showName() -------+
好处与代价
好处是 View 极轻、逻辑集中,测试时能"只测 Presenter 不碰界面"。代价是接口方法写起来繁琐——每个界面变化都要在 View 上定义方法,代码量不小。而且 View 和 Presenter 的关联代码仍是手写的同步。
⚠️ 常见错误
- 把 View 接口写宽:为每个小变化都加一个方法,接口数量爆炸。
- Presenter 持有太多 View 引用:一个 Presenter 操作多个 View,耦合失控。
- 同步仍靠手动:Presenter 里
modelChanged()后挨个调 View 方法,本质还是命令式。
1.4 MVVM:ViewModel 登场 ⭐
从 Controller 到 ViewModel
MVVM 由三个角色构成:
- Model:数据与业务逻辑,和 MVC 中的一致。
- View:用户界面(模板)。
- ViewModel(VM):View 与 Model 之间的"桥梁",持有页面所需的状态(state),并提供"把状态转成视图"的能力。
关键区别在于:View 与 ViewModel 之间建立了数据绑定(data binding),ViewModel 状态一变,View 自动更新;View 上的用户输入,也会自动写回 ViewModel。你不再需要手写"同步"。
View <--数据绑定--> ViewModel <--> Model
一个最小 MVVM 例子
用 Vue 写一个计数器,直观感受"状态变了页面自动动":
<template>
<p>当前计数:{{ count }}</p> <!-- 插值:count 变化时这里自动更新 -->
<button @click="count++">加一</button> <!-- @click 绑定点击事件 -->
</template>
<script>
export default {
data() {
return { count: 0 }; // 状态 count 就是 ViewModel 的数据
},
};
</script>
用户点击按钮 → 触发 count++ → count 一变,插值处自动重渲染。全程没有一行 querySelector,这就是数据绑定带来的声明式体验。
⚠️ 常见错误
- 把 ViewModel 当 Model:在组件 data 里放大量业务规则,数据层和表现层混在一起。
- 以为 MVVM 是框架专属:MVVM 是思想/架构模式,Vue/React/Angular 是它的具体实现,别把"Vue"和"MVVM"画等号。
- 双向绑定滥用:不是所有状态都该双向绑定,后面 1.6 会讲何时用单向。
1.5 单向绑定与双向绑定 ⭐
单向绑定:数据 → 视图
v-bind 只把数据"灌"给视图,视图不会反向改数据:
<p v-bind:title="tooltip">悬停看提示</p>
tooltip 变了,title 属性跟着变;但用户没法通过 <p> 改 tooltip。数据流向是单向的:ViewModel → View。适合展示类内容,流向清晰、好调试。
双向绑定:数据 ⇄ 视图
v-model 在表单元素上实现双向:数据变,输入框变;用户输入,数据也变。
<input v-model="username" />
<p>你好,{{ username }}</p>
用户在输入框打字 → username 更新 → 下面的 {{ username }} 立刻回显。两条路都自动同步。
什么时候用哪个
- 展示/单向变化:用
v-bind单向绑定,数据流可控。 - 表单输入:用
v-model双向绑定,省掉手动监听input再赋值。
原则:双向绑定只用于"用户能输入"的地方。强行给展示元素也搞双向,会引入隐式的数据流,出 bug 难查。
⚠️ 常见错误
- 把
v-model用在非表单元素:如<p v-model="x">,没意义也不会同步,该用v-bind。 - 双向绑定用在只读展示:数据流变双向后,任何来源的改变都可能改到数据,逻辑不透明。
- 忘了
v-model本质是"语法糖":它等价于"value绑定 +input事件更新",理解底层才不会在v-model上犯晕。
1.6 数据驱动视图:视图是数据的函数
一句话核心
视图 = f(状态)。状态(state)是唯一的"事实来源"(single source of truth),视图只是状态的一种投影。只要保证状态正确,视图就正确。
// 视图就是"读状态 → 生成描述"
function view(state) {
return `<p>共 ${state.todos.length} 条待办</p>`;
}
框架在此基础上加一层"当状态变化时,重新计算视图并精准更新 DOM"。你写代码时只关心状态怎么变,不关心 DOM 怎么改。
为什么这样更好
- 逻辑集中:业务就是"改状态",不用分散在各处同步 DOM。
- 可预测:给定一份状态,必然得到同一份视图,测试方便。
- 易维护:新增功能 = 新增状态与模板片段,不碰其他部分的同步代码。
⚠️ 常见错误
- 视图之外另存一份"镜像数据":比如把列表内容同时在 data 和某个全局变量各存一份,两处不同步就出错。一切数据都进状态。
- 直接改 DOM 绕过框架:
document.querySelector(...).textContent = ...会破坏"状态是唯一事实"的约定,下次状态更新时框架可能把改动覆盖掉。 - 在模板里写复杂逻辑:状态到视图的"函数"应保持简单,复杂计算放进"计算属性"(后续讲义专讲),模板里塞一堆判断难以维护。
1.7 MVVM 的生态位置:Vue、React 与双向绑定
框架是 MVVM 思想的落地
MVVM 是架构思想,不是某个库的专利。三大主流框架各有侧重:
- Vue:官方文档明确采用 MVVM 风格的设计。
data+ 模板 + 指令直接体现 ViewModel 与数据绑定,对初学者最友好,本课程以它为例。 - React:更接近"单向数据流"的变体。它没有
v-model这种语法糖,而是要求显式地"用函数更新状态、再重新渲染",数据流向更直白。 - Angular:把 MVVM 写进核心概念,依赖注入、双向绑定(
[(ngModel)])都是框架级功能,结构更重。
学通 MVVM 思想后,换框架只是换"语法外壳"——思想不变。
双向绑定的代价:数据从哪来,变得难追
双向绑定省事,也带来一个隐性成本:任何用户输入都可能改数据。如果页面有几十个输入框,每个都双向绑定,出了 bug 时你很难判断"这个数据是被谁改的"。单向绑定(数据 → 视图)的调试路径更短,因为数据只能有一个来源。
工程上常见的取舍:
- 表单场景:双向绑定收益大,普遍使用
v-model。 - 展示与业务逻辑:坚持单向,让数据流清晰可读。
- 大型团队项目:往往约定"业务数据单向、表单局部双向",兼顾效率与可维护性。
再谈"声明式"与"命令式"
回到本讲开头的直觉:命令式像"手动发传单",声明式像"自动订阅"。MVVM 把**"怎么更新页面"这件重复劳动交给框架,开发者只描述"页面应该长什么样、数据是什么"**。这带来的实际改变是:代码里不再有一堆 querySelector + textContent = ...,取而代之的是清晰的数据与模板。
⚠️ 常见错误
- 以为 React 不是 MVVM 就"不好":架构模式没有绝对优劣,只有适不适合;理解各框架的取舍比站队更重要。
- 把"双向绑定"当万能解:非表单区域滥用双向绑定,数据流混乱,调试成本上升。
- 只记 API 不记思想:会写
v-model却说不清它解决了什么问题,换技术栈就归零;先想清楚"数据驱动视图"这个本质。
1.8 思维转变:从"操作页面"到"管理数据"
一个习惯的迁移
传统写法里,你的注意力在DOM:哪个元素该改、改什么。MVVM 写法里,注意力转移到数据:这份状态对不对、要变成什么样。代码的组织方式也随之变化。
对比同一件事——点击按钮把文字变红:
// 命令式(传统):直接操作 DOM
btn.addEventListener('click', () => {
textEl.style.color = 'red';
});
<!-- 声明式(MVVM):改状态,样式由数据驱动 -->
<template>
<p :style="{ color: isRed ? 'red' : 'black' }">文字</p>
</template>
<script>
export default {
data() { return { isRed: false }; },
methods: {
turnRed() { this.isRed = true; },
},
};
</script>
前者的核心动作是"改元素",后者的核心动作是"改状态"。渲染结果的正确性由"数据 + 模板"整体保证,而不是靠一条条 DOM 操作拼出来。
这对排错方式的影响
命令式代码出 bug,你顺着 DOM 操作逐条看"哪一步没执行";声明式代码出 bug,你查"状态是不是预期的、模板是否读对了状态"。两种思维方式不同,排查路径也不同。习惯后,你会自然地先看数据再看模板。
为什么值得掌握
不只是 Vue——这个"状态驱动界面"的思想贯穿现代前端(React 的 state、Flutter 的 Widget 重建都是同一套逻辑)。学一次,换任何框架都能快速迁移,这是比记住某个 API 更值钱的能力。
⚠️ 常见错误
- 习惯性去模板里找元素改样式:忘掉"状态是唯一事实",又退回命令式,等于放弃框架优势。
- 把状态存在 DOM 属性里:用
data-*属性存业务值,绕过了响应式系统,改了不更新。 - 只学 API 不体会思想:换框架就懵。思想是"数据驱动视图",API 只是表达方式。
📌 记忆口诀
- MVVM 三件套:Model 管数据,View 管展示,ViewModel 当桥梁。
- 数据绑定:状态一变,视图自动跟着变;用户一输入,状态自动更新。
- 单向 vs 双向:展示用单向
v-bind,表单用双向v-model。 - 视图 = f(状态):状态是唯一事实来源,视图只是投影。
📌 双语术语表(本讲)
| 中文 | English | 记忆点 |
|---|---|---|
| 模型 | Model | 数据与业务规则 |
| 视图 | View | 用户看到的界面 |
| 控制器 | Controller | MVC 里的中间人 |
| 主持人 | Presenter | MVP 里的逻辑层 |
| 视图模型 | ViewModel | MVVM 的桥梁 |
| 数据驱动 | data-driven | 数据决定视图 |
| 声明式渲染 | declarative rendering | 声明"是什么" |
| 命令式 | imperative | 指挥"怎么做" |
| 单向绑定 | one-way binding | 数据 → 视图 |
| 双向绑定 | two-way binding | 数据 ⇄ 视图 |
| 状态 | state | 视图的依据 |
⭐ 本讲考点清单
- 前端演进:命令式 DOM 操作的痛点 → 框架的声明式方案
- MVC 三角色职责与协作流程
- MVP 中 Presenter 与 MVC 中 Controller 的区别
- MVVM 三角色与"数据绑定"的含义
- 单向绑定
v-bind与双向绑定v-model的适用场景 - 视图是状态的函数、"单一事实来源"的含义
- 双向绑定为何只用于表单等可输入元素
- 框架与 MVVM 的关系(思想 vs 实现)