个人博客:haichenyi.com。感谢关注
一. 目录
二. 架构模式的演变背景
在软件开发中,架构模式是解决代码组织,职责分离和可维护行的核心方案。一个"好"的架构可以少很多不必要的麻烦。这个"好"就很关键,虽然架构模式经历了从MVC——>MVP——>MVVM的演变,但是,不一定后者比前者好。比方说:你一个小项目,MVC就够用了,非要去使用MVP,MVVM,就会多写很多无用代码。要结合多方面去考虑。软件开发的终极目标是高类聚,低耦合 。但是,还是需要结合实际项目去选择。18年MVVM刚兴起的时候,就写过一篇三者的区别的文章
MVC、MVP、MVVM比较。回顾了一下,感觉还是适用的。
三. MVC:经典的分层起点
- 核心思想
- Model:管理数据和业务逻辑
- View:呈现给用户看的界面
- Controller:接收用户输入,协调Model和View
- 典型流程
2.1 用户点击按钮触发点击事件,传递到Controller
2.2 Controller触发Model的事件去拿数据
2.2 Model数据更新之后,通知Controller更新view
//伪代码如下
let tvContent = document.getElementById("tv_content")
document.getElementById("btn").onclick = function () {
axios.post().then(res=>{
let data = res.data
tvContent.textContent = data
})
}
//当前页面获取view的某个组件的引用
//用户点击按钮,触发网络请求,拿到数据
//拿到数据之后,更新tvContent的内容
//当前页面就是Controller,网络请求包装的类就是Model,组件就是view
//Controller控制Model,model拿到数据之后控制view刷新界面
//代码少,逻辑简单,看不出来啥问题。挺好用的。但是,代码多,逻辑复杂呢?
//(前面就说了架构要根据实际项目情况来看)
- 局限性
- View和Model直接交互,耦合度高
- Controller臃肿:业务代码全都写在Controller中
四. MVP:面向接口的解耦尝试
- 核心改进
- Presenter(协调者):取代Controller,通过接口与view通信
- View被动化,只负责页面更新,逻辑由Presenter处理
- 典型流程
2.1 View接收用户操作,调用Presenter接口
2.2 Presenter操作Model处理数据
2.3 Model返回结果后,Presenter通过View接口更新页面
//伪代码如下,H5没有接口的概念,我都不知道怎么举例子
//这么理解,接口就是定义了一个一个的功能,需要有实现类去实现。
//还是上面的例子:用户点击按钮更新页面
interface IHome {
update(content)
}
class HomeImpl implements IHome {
tvContent;
constructor() {
this.tvContent = document.getElementById("tv_content")
}
update(content: string) {
this.tvContent.textContent = content
}
}
class HomePresenter {
iHome: IHome
constructor(iHome: IHome) {
this.iHome = iHome
}
getData() {
Axions.post().then(res=>{
this.iHome.update(res.data)
})
}
}
let homePresenter = new HomePresenter(new HomeImpl())
document.getElementById("btn")?.onclick = function () {
homePresenter.getData()
}
//上面的IHome接口,就是页面更新的定义,HomeImpl就是这个接口的实现
//如果,页面有很多种显示需要处理,就需要定义多个方法,需要多个实现,以便于后面P层调用
//HomePresenter就是P层,负责数据处理,和使用接口的引用更新页面
//按钮点击,触发P层逻辑。
//这只是最简单的写法,有很多优化点。Android里面用的比较多
- 优势与不足
- 优势:View和Model完全解耦
- 不足:需要手动维护大量的接口,代码冗余
五. MVVM:数据驱动的终极形态
- 核心思想
- ViewModel:作为view和model的桥梁,通过数据绑定实现自动同步
- 双向绑定:view的输入自动更新model,model数据变化自动刷新view
- 典型流程
2.1 view通过模板语法(如{{data}}),绑定到ViewModel
2.2 ViewModel监听到Model变化,并转换为View所需要的数据格式
2.3 用户操作触发ViewModel的方法,更新Model
//这么举例子呢,其实,万变不离其中,你不做的事情,都是框架帮你做的。
//比方说,Vue的响应式编程,你只用改变数据,页面自动更新。啥都不干,页面怎么可能知道数据遍了,从而自动更新呢?
//实际上就是发布订阅者模式触发更新,那么,是谁发布,谁订阅呢?放到后面第六部分讲
- 优势
- 开发高效:减少手动 DOM 操作,聚焦数据逻辑
- 动态更新:适合实时数据展示
- 缺点:学习成本高,上手慢。原理复杂,遇到问题,不容易定位
六. Vue:MVVM 的现代化实践
先说概念:Vue.js是mvvm模式集大成者,通过响应式系统和申明试模板,简化了数据绑定。并引入组件化解决复杂场景的问题。
6.1 Vue中MVVM的映射关系
MVVM层 | Vue实现 |
---|---|
Model | data属性 |
View | 模板(templtate)和样式(style) |
ViewModel | Vue组件实例(暴露:data,computed,methods) |
6.2 双向绑定的原理:Vue 的响应式系统通过以下步骤实现:
- 数据劫持
- vue2是使用 Object.defineProperty 监听对象属性。
- vue3是使用Proxy代理对象,支持深层次监听
// Vue 2 数据劫持示例
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
dep.depend(); // 收集依赖
return val;
},
set(newVal) {
val = newVal;
dep.notify(); // 触发更新
}
});
}
//这里插一句题外话,这个响应式的设计模式,就是观察者模式
//数据本身就是被观察者,get方法里面收集依赖,就是收集观察者,set方法里面触发更新,就是通知观察者数据发生了变化
//vuex使用的是发布订阅者模式。
//下一篇文章。就讲讲这两种设计模式吧。
- 依赖收集
- 每个组件对应一个 Watcher,在渲染时访问数据属性,触发 getter 收集依赖。
- 分发更新
- 数据修改时触发 setter,通知所有关联的 Watcher 重新渲染(通过虚拟 DOM 对比更新真实 DOM,Diff算法)。
6.3 示例:数据变化驱动视图更新
<template>
<div>
<!-- 双向绑定:v-model 语法糖 -->
<input v-model="message" />
<p>{{ reversedMessage }}</p>
</div>
</template>
<script>
export default {
data() {
return { message: "Hello Vue!" }; // Model
},
computed: {
reversedMessage() { // ViewModel 逻辑
return this.message.split('').reverse().join('');
}
}
}
</script>
Vue 的双向绑定,解析如下:
HTML 模板
<div id="app">
<input v-model="message" />
<p>{{ reversedMessage }}</p>
</div>
JavaScript 逻辑
// 模拟 Vue 实例
const data = { message: "Hello" };
// 1. 数据劫持
defineReactive(data, "message", data.message);
// 2. 计算属性(依赖 message)
const computed = {
reversedMessage: () => data.message.split("").reverse().join("")
};
// 3. 模板渲染 Watcher
new Watcher(() => {
//归根结底,页面更新的方法,还是这个。我们前面说的mvc,mvp页面更新也是这个方法
//所以,你以为它很神奇,其实,底层原理都是一样的。主要还是看每个人的思路。
document.querySelector("p").textContent = computed.reversedMessage();
});
// 4. 双向绑定(输入框 → 数据)
document.querySelector("input").addEventListener("input", (e) => {
data.message = e.target.value; // 修改数据 → 触发 setter → 更新视图
});
效果:
- 输入框内容变化时,data.message 更新。
- reversedMessage 自动重新计算,p标签内容实时更新。