MVC 与 MVVM 的区别,以及在 Unity 项目中的实践

区别 & 概念回顾

区别

说实话,思路上没啥区别。代码上,MVC需要手动触发,而 MVVM 通过事件监听,把函数触发写在了 ViewModel 中

MVC

什么是MVC?Model、View、Controller。 一图胜千言:(如果图挂了请访问知乎原贴:什么是MVVM框架?

MVC图示

附言,我今天看了很多帖子,不同的帖子对 MVC 的定义都不太统一,上图据说是 iOS 的开发框架的定义。

比如下面我做的 Unity 计数器例子,用户点击按钮,这时候 View 调用 Controller 的方法,更新 Model,在 Model 的更新函数中,又会调用 Controller 的方法来更新 View,这就形成了上图的关系。

MVVM

什么是 MVVM?Model、View、ViewModel。

MVVM图示

听起来是不是和 MVC 一样?确实,MVVM 就是改进版的 MVC,ViewModel “算是” 充当了 Controller 的位置,但又不太一样:

ViewModel 和 View 双向绑定,如胶似漆,还是刚才那个计数器的例子。当用户点击按钮,View 的按钮直接监听绑定 ViewModel 的具体方法,比如 Increment() 增加计数,而 ViewModel 则在函数中对 Model 更新数值然后读取 Model 的数值并应用在自己的属性中,当自己数值更改,通知订阅了此事件的函数,也就是 View 自己更新页面。

这样就完成了 View 和 Model 的解耦, Model 和 View 都减少了调用 Controller/VM 这种中间层的代码来直接操控对方(Model/View),从而解耦。

Unity 实践

演示 Demo

用两种框架做了一个计数器,点击按钮数字+1,可以切换场景(MVC/MVVM),但表现层是一模一样的… 使用的 Unity WebGL 直接发布,网址如下: https://play.unity.com/en/games/76ee5d18-3e00-429e-9a21-05fbcf604663/webgl-builds

代码分析:MVC

概述: 主要设计了三个脚本:

  • MVCCounterController.cs
  • MVCCounterModel.cs
  • MVCCounterView.cs 以及一个设置脚本:
  • MVCSetup.cs,用来引用 View 并初始化 Controller

应用到场景中,需要给某个物体挂载 MVCSetup.cs 和 MVCCounterView.cs,然后把按钮、Text那些引用上去。

上述脚本的流程关系如下:MVCSetup 只负责 new 一个 Controller 出来,并把引用的 View 传给Controller 的构造函数,Controller 负责创建 Model,并把自己传递给 Model 的构造函数,同时有一些函数用来调用 View 和 Model 相应方法,比如 Increment、Reset、UpdateView、ChangeScene,Model 负责自己的数据管理,并且在数据操作完成后,调用 Controller 的方法来更新 View。

UML 类图如下:

classDiagram  
    class MVCSetup {  
        +MVCCounterView view  
        +MVCCounterController _mvcCounterController  
        +Awake()  
    }  
  
    class MVCCounterController {  
        -MVCCounterModel _model  
        -MVCCounterView _view  
        +MVCCounterController(view)  
        +Increment()  
        +Reset()  
        +UpdateView()  
        +ChangeScene()  
    }  
  
    class MVCCounterModel {  
        -int _count  
        -MVCCounterController _controller  
        +MVCCounterModel(controller)  
        +Count  
        +Increment()  
        +Reset()  
    }  
  
    class MVCCounterView {  
        -Button incrementButton  
        -Button resetButton  
        -Button changeSceneButton  
        -TextMeshProUGUI counterText  
        -MVCCounterController _controller  
        +Initialize(controller)  
        +UpdateCountText(count)  
        +OnIncrementButtonClicked()  
        +OnResetButtonClicked()  
        +OnChangeSceneButtonClicked()  
    }  
  
    MVCSetup --> MVCCounterView  
    MVCSetup --> MVCCounterController  
    MVCCounterController --> MVCCounterModel  
    MVCCounterController --> MVCCounterView  
    MVCCounterModel --> MVCCounterController  
    MVCCounterView --> MVCCounterController

代码分析:MVVM

概述: 主要设计了三个脚本:

  • MVVMCounterModel.cs
  • MVVMCounterView.cs
  • MVVMCounterViewModel.cs 以及两个辅助事件监听的脚本:
  • MVVMObservableObject.cs,响应事件监听,继承自 INotifyPropertyChanged
  • MVVMRelayCommand.cs,在 ViewModel 中绑定 UI 操作,继承自 ICommand

应用到场景中,需要给某个物体挂载 MVVMCounterView.cs 并把 UI 元素引用上去。

上述脚本的流程关系如下:View 负责引用页面元素,并给页面按钮添加事件监听(AddListener),有一个响应 ViewModel 属性改变的回调函数用来更新数字,ViewModel 中会声明 View 的各种按钮的具体方法(命令),在这些命令中对 Model 进行操控,操控后在 ViewModel 中更新自己的属性。而由于 ViewModel 继承了 MVVMObservableObject,MVVMObservableObject 继承了 INotifyPropertyChanged,在 SetField 过的属性的值被更改的时候,会触发事件,从而通知到 View 中的更新函数。

UML 类图如下:

classDiagram  
    class MVVMCounterViewModel {  
        -MVVMCounterModel _model  
        -string _countDisplay  
        +string CountDisplay  
        +ICommand IncrementCommand  
        +ICommand ResetCommand  
        +ICommand ChangeSceneCommand  
        +MVVMCounterViewModel()  
        -void IncrementCount()  
        -void ResetCount()  
        -void ChangeScene()  
        -void UpdateCountDisplay()  
    }  
  
    class MVVMCounterModel {  
        -int _count  
        +int Count  
        +void Increment()  
        +void Reset()  
    }  
  
    class MVVMCounterView {  
        -Button incrementButton  
        -Button resetButton  
        -Button changeSceneButton  
        -TextMeshProUGUI countText  
        -MVVMCounterViewModel _viewModel  
        +void Awake()  
        -void OnViewModelPropertyChanged(object, PropertyChangedEventArgs)  
    }  
  
    class MVVMObservableObject {  
        +event PropertyChangedEventHandler PropertyChanged  
        +bool SetField<T>(ref T, T, string)  
        +void OnPropertyChanged(string)  
    }  
  
    class MVVMRelayCommand {  
        -Action _execute  
        -Func<bool> _canExecute  
        +event EventHandler CanExecuteChanged  
        +MVVMRelayCommand(Action, Func<bool>)  
        +bool CanExecute(object)  
        +void Execute(object)  
        +void RaiseCanExecuteChanged()  
    }  
  
    MVVMCounterViewModel --> MVVMCounterModel
    MVVMCounterViewModel --|> MVVMObservableObject  
    MVVMCounterView --> MVVMCounterViewModel  
    MVVMCounterViewModel --> MVVMRelayCommand  
    MVVMRelayCommand ..> ICommand

差异比较

如文首所述,主要差异在于 MVVM 中的 ViewModel 与 View 通过事件绑定了,所以解耦了 View 和 Model,便于项目的维护。

比如新增了一个需求:计数器要增加一个按钮,点击按钮后,计数器 +100。

MVC架构下要这样改:

  • View 中注册按钮,给按钮新增一个函数,这个函数来调用 Controller 的函数 A
  • Controller 中新增两个函数 A 和函数 B,A 函数用来调用 Model 中的 +100 方法,B 函数用来更新 View 的显示(可能不需要,之前已经实现)
  • Model 中新增一个函数,这个函数用来 +100,+100之后,调用 Controller 的函数 B 来更新 View 的显示

MVVM 架构下这样改:

  • View 中注册按钮,给按钮添加一个监听,这个监听指向 ViewModel 的相关方法。相比于 MVC 少声明了一个函数。
  • ViewModel 新增一个方法,用来操作 Model,根据需要等待 Model 返回,Model 返回后,更新自己的值(比如Count),View 会响应更改事件刷新页面。
  • Model 中新增一个函数,用来+100,相比于 MVC,Model 完全不用管 View。

这只是个小例子,在大项目中,MVVM 相比于 MVC 还是剩了不少代码,并且代码之间的交互更简洁优雅。

具体代码实现

MVC

MVCSetup.cs

using System;  
using UnityEngine;  
  
namespace Test.MVC  
{  
    public class MVCSetup : MonoBehaviour  
    {  
        [SerializeField] private MVCCounterView view;  
        private MVCCounterController _mvcCounterController;  
  
        private void Awake()  
        {            
	        _mvcCounterController = new MVCCounterController(view); // Simplified constructor call  
        }  
    }
}

MVCCounterModel.cs

namespace Test.MVC
{
    // Model - 负责数据和业务逻辑
    public class MVCCounterModel
    {
        private int _count;
        private MVCCounterController _controller;
 
        public MVCCounterModel(MVCCounterController controller)
        {
            _controller = controller;
        }
 
        public int Count
        {
            get => _count;
            private set => _count = value;
        }
        
        public void Increment()
        {
            _count++;
            _controller.UpdateView();
        }
 
        public void Reset()
        {
            _count = 0;
            _controller.UpdateView();
        }
    }
}

MVCCounterController.cs

using Test.MVC;  
using UnityEngine.PlayerLoop;  
using UnityEngine.SceneManagement;  
  
public class MVCCounterController  
{  
    private readonly MVCCounterModel _model;  
    private readonly MVCCounterView _view;  
    public MVCCounterController(MVCCounterView view)  
    {        _view = view;  
        _model = new MVCCounterModel(this); // Create the model and pass the controller  
  
        _view.Initialize(this);  
        UpdateView();  
    }    public void Increment() => _model.Increment();  
    public void Reset() => _model.Reset();  
    public void UpdateView() => _view.UpdateCountText(_model.Count);  
    public void ChangeScene() => SceneManager.LoadScene("MVVMSampleScene", LoadSceneMode.Single);  
}

MVCCounterView.cs

using UnityEngine;  
using UnityEngine.UI;  
using TMPro;  
  
namespace Test.MVC  
{  
    public class MVCCounterView : MonoBehaviour  
    {  
        [SerializeField] private Button incrementButton;  
        [SerializeField] private Button resetButton;  
        [SerializeField] private Button changeSceneButton;  
        [SerializeField] private TextMeshProUGUI counterText;  
        private MVCCounterController _controller;  
  
        public void Initialize(MVCCounterController controller)  
        {            _controller = controller;  
            incrementButton.onClick.AddListener(OnIncrementButtonClicked);  
            resetButton.onClick.AddListener(OnResetButtonClicked);  
            changeSceneButton.onClick.AddListener(OnChangeSceneButtonClicked);  
        }        // 更新显示的计数  
        public void UpdateCountText(int count)  
        {            counterText.text = $"{count}";  
        }  
        private void OnIncrementButtonClicked() => _controller.Increment();  
  
        private void OnResetButtonClicked() => _controller.Reset();  
        private void OnChangeSceneButtonClicked() => _controller.ChangeScene();  
    }
}

MVVM

MVVMCounterView.cs

using UnityEngine;  
using UnityEngine.UI;  
using TMPro;  
namespace Test.MVVM  
{  
    public class MVVMCounterView : MonoBehaviour  
    {  
        [SerializeField] private Button incrementButton;  
        [SerializeField] private Button resetButton;  
        [SerializeField] private Button changeSceneButton;  
        [SerializeField] private TextMeshProUGUI countText;  
  
        private MVVMCounterViewModel _viewModel;  
  
        private void Awake()  
        {            // 创建视图模型  
            _viewModel = new MVVMCounterViewModel();  
        // 绑定视图模型属性到UI元素  
            countText.text = _viewModel.CountDisplay;  
            _viewModel.PropertyChanged += OnViewModelPropertyChanged;  
        // 绑定命令到按钮  
            incrementButton.onClick.AddListener(() => _viewModel.IncrementCommand.Execute(null));  
            resetButton.onClick.AddListener(() => _viewModel.ResetCommand.Execute(null));  
            changeSceneButton.onClick.AddListener(()=> _viewModel.ChangeSceneCommand.Execute(null));  
        }  
        // 当视图模型属性变化时更新UI  
        private void OnViewModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)  
        {            if (e.PropertyName == nameof(_viewModel.CountDisplay))  
            {                countText.text = _viewModel.CountDisplay;  
            }        }    }}

MVVMCounterViewModel.cs

namespace Test.MVVM  
{  
    using System.Windows.Input;  
    using UnityEngine.SceneManagement;  
    // 视图模型 - 连接模型和视图,处理视图逻辑  
    public class MVVMCounterViewModel : MVVMObservableObject  
    {  
        private readonly MVVMCounterModel _model;  
    // 绑定到视图的属性  
        private string _countDisplay;  
        public string CountDisplay   
{   
get => _countDisplay;   
private set => SetField(ref _countDisplay, value);   
        }  
  
        // 绑定到按钮的命令  
        public ICommand IncrementCommand { get; }  
        public ICommand ResetCommand { get; }  
        public ICommand ChangeSceneCommand { get; }  
  
        public MVVMCounterViewModel()  
        {            _model = new MVVMCounterModel();  
        // 初始化命令  
            IncrementCommand = new MVVMRelayCommand(IncrementCount);  
            ResetCommand = new MVVMRelayCommand(ResetCount);  
            ChangeSceneCommand = new MVVMRelayCommand(ChangeScene);  
        // 初始更新显示  
            UpdateCountDisplay();  
        }  
        private void IncrementCount()  
        {            _model.Increment();  
            UpdateCountDisplay();  
        }  
        private void ResetCount()  
        {            _model.Reset();  
            UpdateCountDisplay();  
        }  
        private void ChangeScene() => SceneManager.LoadScene("MVCSampleScene", LoadSceneMode.Single);  
  
        private void UpdateCountDisplay()  
        {            CountDisplay = $"{_model.Count}";  
        }    }  
}

MVVMCounterModel.cs

namespace Test.MVVM  
{  
    public class MVVMCounterModel  
    {  
        private int _count;  
  
        public int Count   
{   
get => _count;   
private set   
            {  
                _count = value;  
            }   
}  
  
        public void Increment()  
        {            _count++;  
        }  
        public void Reset()  
        {            _count = 0;  
        }    }}

MVVMObservableObject.cs

using System.Collections.Generic;  
  
namespace Test.MVVM  
{  
// 可观察对象基类,实现属性变化通知  
    using System.ComponentModel;  
    using System.Runtime.CompilerServices;  
  
    public class MVVMObservableObject : INotifyPropertyChanged  
    {  
        public event PropertyChangedEventHandler PropertyChanged;  
  
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)  
        {            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
        }  
        protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)  
        {            if (EqualityComparer<T>.Default.Equals(field, value)) return false;  
            field = value;            OnPropertyChanged(propertyName);  
            return true;  
        }    }}

MVVMRelayCommand.cs

namespace Test.MVVM  
{  
    // 命令实现类,用于绑定UI操作  
    using System;  
    using System.Windows.Input;  
    public class MVVMRelayCommand : ICommand  
    {  
        private readonly Action _execute;  
        private readonly Func<bool> _canExecute;  
  
        public event EventHandler CanExecuteChanged;  
  
        public MVVMRelayCommand(Action execute, Func<bool> canExecute = null)  
        {            _execute = execute ?? throw new ArgumentNullException(nameof(execute));  
            _canExecute = canExecute;  
        }  
        public bool CanExecute(object parameter)  
        {            return _canExecute == null || _canExecute();  
        }  
        public void Execute(object parameter)  
        {            _execute();  
        }  
        public void RaiseCanExecuteChanged()  
        {            CanExecuteChanged?.Invoke(this, EventArgs.Empty);  
        }    }}

标题:MVC 与 MVVM 的区别,以及在 Unity 项目中的实践
作者:LeonYew
日期:2025-09-29