Vue 中的 MVVM、MVC 和 MVP:现代前端架构模式深度解析

Vue 中的 MVVM、MVC 和 MVP:现代前端架构模式深度解析

前言:架构模式的演变之旅

在 Vue 开发中,我们经常听到 MVVM、MVC 这些术语,但它们到底意味着什么?为什么 Vue 选择了 MVVM?这些模式如何影响我们的代码结构?今天,让我们抛开教科书式的定义,从实际 Vue 开发的角度,深入探讨这些架构模式的本质区别。

一、MVC:经典的王者(但已不再适合前端)

1.1 MVC 的核心三要素

javascript 复制代码
// 模拟一个传统的 MVC 结构(不是 Vue,但可以帮助理解)
class UserModel {
  constructor() {
    this.users = []
    this.currentUser = null
  }
  
  addUser(user) {
    this.users.push(user)
  }
  
  setCurrentUser(user) {
    this.currentUser = user
  }
}

class UserView {
  constructor(controller) {
    this.controller = controller
    this.userList = document.getElementById('user-list')
    this.userForm = document.getElementById('user-form')
    
    // 手动绑定事件
    this.userForm.addEventListener('submit', (e) => {
      e.preventDefault()
      const name = document.getElementById('name').value
      const email = document.getElementById('email').value
      this.controller.addUser({ name, email })
    })
  }
  
  renderUsers(users) {
    this.userList.innerHTML = users.map(user => 
      `<li>${user.name} (${user.email})</li>`
    ).join('')
  }
}

class UserController {
  constructor(model) {
    this.model = model
    this.view = new UserView(this)
  }
  
  addUser(userData) {
    this.model.addUser(userData)
    this.view.renderUsers(this.model.users)
  }
}

// 使用
const app = new UserController(new UserModel())

1.2 MVC 在 Vue 中的"遗迹"

虽然 Vue 不是 MVC,但我们能看到 MVC 的影子:

vue 复制代码
<!-- 这种写法有 MVC 的影子 -->
<template>
  <!-- View:负责展示 -->
  <div>
    <h1>{{ title }}</h1>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <button @click="loadUsers">加载用户</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // Model:数据状态
      title: '用户列表',
      users: []
    }
  },
  methods: {
    // Controller:业务逻辑
    async loadUsers() {
      try {
        const response = await fetch('/api/users')
        this.users = await response.json()
      } catch (error) {
        console.error('加载失败', error)
      }
    }
  }
}
</script>

MVC 的关键问题在前端

  • 视图和控制器紧密耦合:DOM 操作和业务逻辑混杂
  • 双向依赖:视图依赖控制器,控制器也依赖视图
  • 状态管理困难:随着应用复杂,状态散落在各处

二、MVP:试图改进的中间者

2.1 MVP 的核心改进

javascript 复制代码
// 一个 MVP 模式的示例
class UserModel {
  constructor() {
    this.users = []
  }
  
  fetchUsers() {
    return fetch('/api/users').then(r => r.json())
  }
}

class UserView {
  constructor() {
    this.userList = document.getElementById('user-list')
    this.loadButton = document.getElementById('load-btn')
  }
  
  bindLoadUsers(handler) {
    this.loadButton.addEventListener('click', handler)
  }
  
  displayUsers(users) {
    this.userList.innerHTML = users.map(user => 
      `<li>${user.name}</li>`
    ).join('')
  }
  
  showLoading() {
    this.userList.innerHTML = '<li>加载中...</li>'
  }
}

class UserPresenter {
  constructor(view, model) {
    this.view = view
    this.model = model
    
    // Presenter 初始化时绑定事件
    this.view.bindLoadUsers(() => this.onLoadUsers())
  }
  
  async onLoadUsers() {
    this.view.showLoading()
    try {
      const users = await this.model.fetchUsers()
      this.view.displayUsers(users)
    } catch (error) {
      console.error('加载失败', error)
    }
  }
}

// 使用
const view = new UserView()
const model = new UserModel()
new UserPresenter(view, model)

2.2 MVP 的特点

  1. Presenter 作为中间人:协调 View 和 Model
  2. View 被动:只负责显示,不包含业务逻辑
  3. 解耦更好:View 和 Model 不知道彼此存在
  4. 但仍有问题:Presenter 可能变得臃肿,测试仍复杂

三、MVVM:Vue 的选择与实现

3.1 MVVM 的核心:数据绑定

vue 复制代码
<!-- 这是典型的 MVVM,Vue 自动处理了绑定 -->
<template>
  <!-- View:声明式模板 -->
  <div class="user-management">
    <input 
      v-model="newUser.name" 
      placeholder="用户名"
      @keyup.enter="addUser"
    >
    <button @click="addUser">添加用户</button>
    
    <ul>
      <li v-for="user in filteredUsers" :key="user.id">
        {{ user.name }}
        <button @click="removeUser(user.id)">删除</button>
      </li>
    </ul>
    
    <input v-model="searchQuery" placeholder="搜索用户...">
  </div>
</template>

<script>
export default {
  data() {
    return {
      // Model/ViewModel:响应式数据
      users: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' },
        { id: 3, name: '王五' }
      ],
      newUser: { name: '' },
      searchQuery: ''
    }
  },
  
  computed: {
    // ViewModel:派生状态
    filteredUsers() {
      return this.users.filter(user =>
        user.name.toLowerCase().includes(this.searchQuery.toLowerCase())
      )
    }
  },
  
  methods: {
    // ViewModel:操作方法
    addUser() {
      if (this.newUser.name.trim()) {
        this.users.push({
          id: Date.now(),
          name: this.newUser.name.trim()
        })
        this.newUser.name = ''
      }
    },
    
    removeUser(id) {
      this.users = this.users.filter(user => user.id !== id)
    }
  }
}
</script>

3.2 Vue 如何实现 MVVM

让我们看看 Vue 的底层实现:

javascript 复制代码
// 简化的 Vue 响应式系统
class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data()
    
    // 1. 数据劫持(核心)
    this.observe(this._data)
    
    // 2. 编译模板
    this.compile(options.template)
  }
  
  observe(data) {
    Object.keys(data).forEach(key => {
      let value = data[key]
      const dep = new Dep() // 依赖收集
      
      Object.defineProperty(data, key, {
        get() {
          // 收集依赖
          if (Dep.target) {
            dep.addSub(Dep.target)
          }
          return value
        },
        set(newVal) {
          if (newVal !== value) {
            value = newVal
            // 通知更新
            dep.notify()
          }
        }
      })
    })
  }
  
  compile(template) {
    // 将模板转换为渲染函数
    // 建立 View 和 ViewModel 的绑定
  }
}

class Dep {
  constructor() {
    this.subs = []
  }
  
  addSub(sub) {
    this.subs.push(sub)
  }
  
  notify() {
    this.subs.forEach(sub => sub.update())
  }
}

// Watcher 观察数据变化
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    
    Dep.target = this
    this.value = vm._data[key] // 触发 getter,收集依赖
    Dep.target = null
  }
  
  update() {
    const newValue = this.vm._data[this.key]
    if (newValue !== this.value) {
      this.value = newValue
      this.cb(newValue)
    }
  }
}

四、三种模式的深度对比

4.1 通信流对比

graph TD subgraph "MVC" A[View] -->|用户输入| B[Controller] B -->|更新| C[Model] C -->|通知| B B -->|渲染| A end subgraph "MVP" D[View] -->|委托| E[Presenter] E -->|更新| F[Model] F -->|返回数据| E E -->|更新视图| D end subgraph "MVVM" G[View] <-->|双向绑定| H[ViewModel] H -->|操作| I[Model] I -->|响应数据| H end

4.2 代码结构对比

vue 复制代码
<!-- 同一个功能,三种模式的不同实现 -->

<!-- MVC 风格(不推荐) -->
<template>
  <div>
    <input id="username" type="text">
    <button id="save-btn">保存</button>
    <div id="output"></div>
  </div>
</template>

<script>
export default {
  mounted() {
    // Controller 逻辑散落在各处
    document.getElementById('save-btn').addEventListener('click', () => {
      const username = document.getElementById('username').value
      this.saveUser(username)
    })
  },
  methods: {
    saveUser(username) {
      // Model 操作
      this.$store.commit('SET_USERNAME', username)
      // View 更新
      document.getElementById('output').textContent = `用户: ${username}`
    }
  }
}
</script>

<!-- MVP 风格 -->
<template>
  <div>
    <input v-model="username" type="text">
    <button @click="presenter.save()">保存</button>
    <div>{{ displayText }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      displayText: ''
    }
  },
  created() {
    // Presenter 处理所有逻辑
    this.presenter = {
      save: () => {
        this.$store.commit('SET_USERNAME', this.username)
        this.displayText = `用户: ${this.username}`
      }
    }
  }
}
</script>

<!-- MVVM 风格(Vue 原生) -->
<template>
  <div>
    <!-- 双向绑定自动处理 -->
    <input v-model="username" type="text">
    <button @click="saveUser">保存</button>
    <!-- 自动响应式更新 -->
    <div>用户: {{ username }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: ''
    }
  },
  methods: {
    saveUser() {
      // 数据改变,视图自动更新
      this.$store.commit('SET_USERNAME', this.username)
    }
  }
}
</script>

4.3 实际项目中的体现

javascript 复制代码
// 一个真实的 Vuex + Vue 项目结构

// Model 层:Vuex Store
// store/modules/user.js
export default {
  state: {
    users: [],
    currentUser: null
  },
  mutations: {
    SET_USERS(state, users) {
      state.users = users
    },
    ADD_USER(state, user) {
      state.users.push(user)
    }
  },
  actions: {
    async fetchUsers({ commit }) {
      const users = await api.getUsers()
      commit('SET_USERS', users)
    }
  },
  getters: {
    activeUsers: state => state.users.filter(u => u.isActive)
  }
}

// ViewModel 层:Vue 组件
// UserList.vue
<template>
  <!-- View:声明式模板 -->
  <div>
    <UserFilter @filter-change="setFilter" />
    <UserTable :users="filteredUsers" />
    <UserPagination 
      :current-page="currentPage"
      @page-change="changePage"
    />
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  data() {
    return {
      // 组件本地状态
      currentPage: 1,
      filter: ''
    }
  },
  computed: {
    // 连接 Model (Vuex) 和 View
    ...mapState('user', ['users']),
    ...mapGetters('user', ['activeUsers']),
    
    // ViewModel:计算属性
    filteredUsers() {
      return this.activeUsers.filter(user => 
        user.name.includes(this.filter)
      )
    }
  },
  methods: {
    ...mapActions('user', ['fetchUsers']),
    
    // ViewModel:方法
    setFilter(filter) {
      this.filter = filter
      this.currentPage = 1 // 重置分页
    },
    
    changePage(page) {
      this.currentPage = page
      this.fetchUsers({ page, filter: this.filter })
    }
  },
  created() {
    this.fetchUsers()
  }
}
</script>

五、Vue 3 组合式 API:MVVM 的进化

5.1 传统 Options API 的问题

vue 复制代码
<!-- Options API:逻辑分散 -->
<script>
export default {
  data() {
    return {
      users: [],
      filter: '',
      page: 1
    }
  },
  computed: {
    filteredUsers() { /* ... */ }
  },
  watch: {
    filter() { /* 过滤逻辑 */ },
    page() { /* 分页逻辑 */ }
  },
  methods: {
    fetchUsers() { /* ... */ },
    handleFilter() { /* ... */ }
  },
  mounted() {
    this.fetchUsers()
  }
}
</script>

5.2 组合式 API:更好的逻辑组织

vue 复制代码
<!-- Composition API:逻辑聚合 -->
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'

// 用户搜索功能
const { 
  users, 
  searchUsers, 
  isLoading: usersLoading 
} = useUserSearch()

// 分页功能  
const {
  currentPage,
  pageSize,
  paginatedData,
  changePage
} = usePagination(users)

// 筛选功能
const {
  filter,
  filteredData,
  setFilter
} = useFilter(paginatedData)

// 生命周期
onMounted(() => {
  searchUsers()
})

// 响应式监听
watch(filter, () => {
  currentPage.value = 1
})
</script>

<template>
  <!-- View 保持不变 -->
  <div>
    <input v-model="filter" placeholder="搜索...">
    <UserTable :data="filteredData" />
    <Pagination 
      :current-page="currentPage"
      @change="changePage"
    />
  </div>
</template>

5.3 自定义组合函数

javascript 复制代码
// composables/useUserManagement.js
import { ref, computed } from 'vue'
import { useUserStore } from '@/stores/user'

export function useUserManagement() {
  const userStore = useUserStore()
  const localUsers = ref([])
  const filter = ref('')
  const currentPage = ref(1)
  const pageSize = 10

  // 计算属性:ViewModel
  const filteredUsers = computed(() => {
    return localUsers.value.filter(user =>
      user.name.toLowerCase().includes(filter.value.toLowerCase())
    )
  })

  const paginatedUsers = computed(() => {
    const start = (currentPage.value - 1) * pageSize
    return filteredUsers.value.slice(start, start + pageSize)
  })

  // 方法:ViewModel
  const addUser = (user) => {
    localUsers.value.push(user)
    userStore.addUser(user)
  }

  const removeUser = (id) => {
    localUsers.value = localUsers.value.filter(u => u.id !== id)
  }

  return {
    // 暴露给 View
    users: paginatedUsers,
    filter,
    currentPage,
    addUser,
    removeUser,
    setFilter: (value) => { filter.value = value }
  }
}

六、现代 Vue 生态中的架构模式

6.1 Pinia:更现代的"Model"层

javascript 复制代码
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    currentUser: null
  }),
  
  actions: {
    async fetchUsers() {
      const { data } = await api.get('/users')
      this.users = data
    },
    
    addUser(user) {
      this.users.push(user)
    }
  },
  
  getters: {
    activeUsers: (state) => state.users.filter(u => u.isActive),
    userCount: (state) => state.users.length
  }
})

// 组件中使用
<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const { users, activeUsers } = storeToRefs(userStore)

// MVVM 清晰分层:
// Model: userStore
// ViewModel: 组件中的 computed/methods
// View: template
</script>

6.2 基于特性的架构

bash 复制代码
src/
├── features/
│   ├── user/
│   │   ├── components/     # View
│   │   ├── composables/    # ViewModel
│   │   ├── stores/         # Model
│   │   └── types/          # 类型定义
│   └── product/
│       ├── components/
│       ├── composables/
│       └── stores/
├── shared/
│   ├── components/
│   ├── utils/
│   └── api/
└── App.vue

6.3 服务器状态管理(TanStack Query)

vue 复制代码
<script setup>
import { useQuery, useMutation } from '@tanstack/vue-query'

// Model:服务器状态
const { data: users, isLoading } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

// ViewModel:本地状态和逻辑
const filter = ref('')
const filteredUsers = computed(() => {
  return users.value?.filter(u => 
    u.name.includes(filter.value)
  ) || []
})

// Mutation:修改服务器状态
const { mutate: addUser } = useMutation({
  mutationFn: createUser,
  onSuccess: () => {
    // 自动重新获取 users
  }
})
</script>

<template>
  <!-- View -->
  <div>
    <input v-model="filter" placeholder="搜索用户">
    <UserList :users="filteredUsers" />
  </div>
</template>

七、如何选择合适的模式

7.1 决策矩阵

场景 推荐模式 理由 Vue 实现
小型项目 MVVM(Vue 原生) 简单直接,上手快 Options API
中型项目 MVVM + 状态管理 需要共享状态 Vuex/Pinia
大型项目 组合式 MVVM 逻辑复用,类型安全 Composition API + TypeScript
复杂业务逻辑 领域驱动设计 业务逻辑复杂 特性文件夹 + Clean Architecture
实时应用 MVVM + 响应式增强 需要复杂响应式 Vue + RxJS/Signals

7.2 架构演进示例

javascript 复制代码
// 阶段1:简单 MVVM(适合 todo 应用)
export default {
  data() {
    return { todos: [], newTodo: '' }
  },
  methods: {
    addTodo() {
      this.todos.push({ text: this.newTodo, done: false })
      this.newTodo = ''
    }
  }
}

// 阶段2:加入状态管理(适合电商网站)
// store/todos.js + TodoList.vue + TodoItem.vue

// 阶段3:组合式架构(适合 SaaS 平台)
// features/todo/
//   ├── useTodoList.js
//   ├── useTodoFilter.js
//   ├── TodoStore.js
//   └── components/

// 阶段4:微前端架构(适合大型企业应用)
// app-todo/ + app-user/ + app-order/ + 主应用

7.3 代码质量检查清单

javascript 复制代码
// 好的 MVVM 代码应该:
// 1. View(模板)保持简洁,只负责展示
<template>
  <!-- ✅ 好:声明式 -->
  <button @click="handleSubmit">提交</button>
  
  <!-- ❌ 不好:包含逻辑 -->
  <button @click="validate() && submit()">提交</button>
</template>

// 2. ViewModel(脚本)处理所有逻辑
<script>
export default {
  methods: {
    // ✅ 好:逻辑在 ViewModel
    handleSubmit() {
      if (this.validate()) {
        this.submit()
      }
    },
    
    // ❌ 不好:直接操作 DOM
    badMethod() {
      document.getElementById('btn').disabled = true
    }
  }
}
</script>

// 3. Model(数据)清晰分层
// ✅ 好:状态管理集中
state: {
  users: [], // 原始数据
  ui: {      // UI 状态
    loading: false,
    error: null
  }
}

// ❌ 不好:状态混杂
data() {
  return {
    apiData: [],     // API 数据
    isLoading: false, // UI 状态
    localData: {}     // 本地状态
  }
}

八、总结:Vue 架构模式的核心要义

8.1 三种模式的本质区别

模式 核心思想 Vue 中的体现 适用场景
MVC 关注点分离,但耦合度高 早期 jQuery 时代 传统后端渲染
MVP Presenter 中介,View 被动 某些 Vue 2 项目 需要严格测试
MVVM 数据绑定,自动同步 Vue 核心设计 现代前端应用

8.2 Vue 为什么选择 MVVM?

  1. 开发效率:数据绑定减少样板代码
  2. 维护性:响应式系统自动处理更新
  3. 可测试性:ViewModel 可以独立测试
  4. 渐进式:可以从简单开始,逐步复杂化

8.3 现代 Vue 开发的最佳实践

  1. 拥抱 MVVM:理解并善用响应式系统
  2. 合理分层
    • View:只负责展示,尽量简单
    • ViewModel:处理业务逻辑和状态
    • Model:管理数据和业务规则
  3. 组合优于继承:使用组合式 API 组织代码
  4. 状态管理:在需要时引入 Pinia/Vuex
  5. 关注点分离:按特性组织代码,而非技术

8.4 记住的关键点

  • Vue 不是严格的 MVVM,但受其启发
  • 架构模式是工具,不是教条,根据项目选择
  • 代码组织比模式名称更重要
  • 渐进式是 Vue 的核心优势,可以从简单开始

最后,无论你使用哪种模式,记住 Vue 的核心原则:让开发者专注于业务逻辑,而不是框架细节。这才是 Vue 成功的真正原因。


思考题:在你的 Vue 项目中,你是如何组织代码的?有没有遇到过架构选择上的困惑?或者有什么独特的架构实践想要分享?欢迎在评论区交流讨论!

相关推荐
北辰alk8 小时前
为什么 Vue 中的 data 必须是一个函数?深度解析与实战指南
vue.js
北辰alk8 小时前
Vue 的 <template> 标签:不仅仅是包裹容器
vue.js
北辰alk8 小时前
为什么不建议在 Vue 中同时使用 v-if 和 v-for?深度解析与最佳实践
vue.js
北辰alk8 小时前
Vue 模板中保留 HTML 注释的完整指南
vue.js
北辰alk8 小时前
Vue 组件 name 选项:不只是个名字那么简单
vue.js
北辰alk8 小时前
Vue 计算属性与 data 属性同名:优雅的冲突还是潜在的陷阱?
vue.js
北辰alk8 小时前
Vue 的 v-show 和 v-if:性能、场景与实战选择
vue.js
计算机毕设VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
心.c11 小时前
如何基于 RAG 技术,搭建一个专属的智能 Agent 平台
开发语言·前端·vue.js