likes
comments
collection
share

前端面试点:MVVM

作者站长头像
站长
· 阅读数 3

现代前端框架绕不开的一个话题:Model-View-ViewModel(MVVM) 架构。那么为什么前端框架会不约而同地使用MVVM呢?探讨一下~

从MVC开始

Model-View-Controller绝对是史上最成功的架构模式,他很好解决了图形界面程序中界面与模型交互带来的复杂度问题。

GUI程序的问题

图形界面应用程序的出现,让用户的体验性更好,但是同时也增加了应用程序的复杂度。在图形界面程序中,界面给用户提供了更丰富的信息(数据),同时用户的行为(点击、输入等)会触发特定的业务逻辑,业务逻辑会变更相应的数据,数据变更后需要反馈到界面上。

GUI程序看起来很美好,但是在开发的时候,就不是那么美好的,从程序整体视角来看,为了更好的管理程序的复杂度,依然需要遵循“责职分离”的原则。于是我们把应用程序分为 View(视图)和 Model(Domain Model领域模型,是一个针对业务抽象的不包含状态的模型),View 展示并响应用户的操作,Model 包含两部分:数据和业务操作的接口。

那么问题就来了:

  • View和Model的数据怎么实现交互?
  • Model只提供固定的业务操作,那么业务的逻辑调度操作集成在哪里?

前端面试点:MVVM

上图简单说明了这个问题,在View与Model中间存在的逻辑调度和数据同步的问题。注意这里的数据并不是模型里面的所有数据,而是页面呈现和实现逻辑调度的数据,例如在模型中,一个商品有id、名称、图片、售价、成本价、存量、优惠策略等信息,但是下单的逻辑操作可能只需要用到id,而页面呈现的内容只有名字、图片、售价和存量这些信息。

MVC

MVC的提出就是为了解决上面的问题,MVC在MV之上加上了一个Controller,通过Controller来实现View和Model的联动。在最开始的GUI程序中,MVC的模型如下:

前端面试点:MVVM

我们先定义两个概念:

  • 应用逻辑:指的是一个具体的业务执行流程,例如商品下单,需要检查商品库存、价格计算、订单生成等一系列操作,这个整体流程称为应用逻辑,由Controller提供
  • 业务逻辑:指的是单一的业务操作逻辑,例如上面说的价格计算是一个业务逻辑,这些业务逻辑会执行具体的业务操作,有时会执行数据的变更,由Model提供

MVC三者之间的交互如下:

  1. 用户操作 View 时,View 会捕获这个操作,然后把操作上报给 Controller,把处理权交给 Controller。
  2. Controller 会先来自 View 的数据做预处理,然后决定调用哪些Model的接口。
  3. 由 Model 执行相关的业务操作
  4. Model 的数据变更后,会通过观察者模式通知 View(观察者模式一般是通过发布/订阅(pub/sub) 或者是事件(Events) 来实现的)
  5. View 接收到变更消息后,向 Model 请求新的数据,然后更新界面

在这个模型中,View负责捕获用户行为,然后有Controller决定行为需要执行什么应用逻辑,然后组合调用Model的接口完成,而Model在数据更新后会广播消息,View接收到消息后获取数据更新界面内容。

这样做的好处是View只做纯粹的展示和交互,不涉及业务逻辑,Model与其他两方完全隔离,不受外部状态影响,也不知道View和Controller的存在,只提供标准的交互接口,Controller控制具体的应用逻辑。三部分各司其职,逻辑与界面可以做到完全拆分,高度模块化,高度内聚,应用逻辑修改的时候可以做到只切换Controller,不需要(或少量)变动View和Model就可以完成。

当然,问题也是存在的,因为数据更新是通过View监听Model实现的,会导致View过度依赖Model(要实现监听就需要View提前在Model上注册),导致View组件化成本变高。

为什么是观察者模式呢?

因为 GUI 应用程序是可以有多个窗口的,也就是有多个 View,同时也存在多个 Controller 和 Model,整个应用程序是由一个或多个 MVC 三角组成的,通过观察者模式,可以实现一个 Model 的变更,触发多个依赖它的 View 的更新,实现多界面的实时更新。

MVC2.0

前面的讲的MVC架构是针对本地化的客户端的,到了B/S时代就无法使用了,最直接的问题就是无法实现View对Model的监听。

B/S的结构中,B端和Server端是分离的,又因为HTTP是单工单向,服务端无法主动向B端推送消息,也就是View无法感知Model,所以需要一套新的MVC架构。

新的MVC架构的模式简图如下:

前端面试点:MVVM

这次View和Model完全是分隔开的,全部由Controller来完成,在前后端没有分离的架构中,View的路由其实也有Controller来控制,Http请求其实全都进入到Controller,再由Controller来判断是进行应用逻辑操作还是页面渲染:

前端面试点:MVVM

在开发者的视角来看,其实MVC三部分还是都在一起(还是在服务器中),但是View的展示和交互是由浏览器提供的,我们可以把浏览器看成一个执行的媒介,那么三者交互的流程还是如上面的简图一样。

MVC的好处就是把整个系统按照最基础的责职切割开,实现“高内聚低耦合”,最大程度减少混乱的情况(例如把很多应用应用逻辑放到View中,导致View和应用逻辑混一起,这样先不说View的组件化,光是View的修改就很麻烦,同时代码的复用率会减低)。灵活地支持业务变更(特别是在敏捷迭代的产品中),避免出现牵一发动全身的情况。

MVVM

MVVM(Model-View-ViewModel)是MVC的变种,由微软的工程师开发在 2005 年发表,起初是用于简化.NET用户界面的事件驱动程序设计

对比MVC,MVVM的主要变更点是View和ViewModel,并且添加一个数据绑定器Binder,用于数据的绑定和更新。

ViewModel

ViewModel 是指“Model of View”,视图的模型,包含了领域模型(Domain Model)和视图状态(state)的抽象。在 View 中我们不仅需要展示领域模型的数据,还要展示一些不在领域模型中的视图状态(比如:是否可点击的状态、排序的规则标识等)。ViewModel可以简单的理解为是一个展示数据的模型(页面展示数据的抽象),用于精准的描述View和响应View的行为。

Binder

与MVC不同的是,MVVM中的View并不会主动去监听数据变化(不监听Model的数据变化也不会从VM获取数据更新),而是通过一个数据绑定器Binder来实现。Binder负责接收Model的数据变更和更新 View 的工作。

模式

开发人员在编写View的时候,会根据特定的模版语法编写View的代码,保证View符合数据绑定的规范,然后在执行(编译)阶段会根据模版提取出View相关的数据,生成ViewModel实例,在将数据与Model的数据进行绑定,这样Binder就知道Model的数据与View展示的数据的关系了,通过监听Model数据的变化,实现View的响应式更新。

在这个基础之上,View的行为可以通过事件绑定或者命令绑定的方式交接给ViewModel。

这样就形成一个完整的链路,通过模版语法编写View,View会生成特定的ViewModel,View的行为交接给ViewModel,ViewModel执行应用逻辑修改Model的数据,Binder在数据变更后同步更新View。

这种方式将原本的事件驱动的模式变更为数据驱动模式。整个程序的行为和状态都由数据的变化来驱动,开发人员只需要关心数据的变更而不需要根据手动去更新视图。

MVVM的流程大致如下:

前端面试点:MVVM

优缺点

优点

  • 开发人员不需要关注View层的更新逻辑,提升研发效率,同时也减低了这方面的耦合度
  • 测试用例可以针对 ViewModel 来做,因为View 和 Model 是同步更新的,测试mock会简便很多

缺点

  • 对于简单的UI的项目,使用MVVM的话,容易出现过度设计的情况
  • 大型的应用使用MVVM,视图的状态会非常多,ViewModel 会变得庞大和复杂,而且大量的数据绑定会消耗大量的内存,可能会有性能上的问题
  • 另外Binder的实现方式也会影响性能,例如angular1.x的脏检查,vm的过大就会卡爆

前端框架&MVVM

其实从MVVM的架构逻辑就可以看出为什么angular、vue、react都会选择使用,主要的思路还是为了解决DOM操作带来的复杂度的问题。传统的前端开发模式(例如JQuery),都是事件驱动的模式,View层相关的逻辑都通过事件来触发,通过监听和响应事件来实现交互和状态控制。事件驱动本身没什么问题,但是如果涉及到大量的频繁的DOM操作的话,逻辑就会变得非常复杂,相当于MVC中的VC混合成一个,因为开发者在开发过程中需要将大部分精力(或者说实现的重点)放在操作更新DOM上,这样就会忽略很多其他内容,所以如果你维护够JQ的老项目就可能会遇到一个业务模块js文件有几千行代码,而里面充斥着大量DOM操作。这并不是什么好事,应用逻辑和视图操作耦合在一起,导致过度的混乱。

MVVM数据驱动的思路可以很好的解决这种问题,使用VM来自动完成View层的更新,方式转变,开发者从关注视图变化转为关注数据变化,那么就可以将实现的重点放在处理业务逻辑和数据上,减少心智负担。而且通过这种方式可以很好的将页面、DOM操作、逻辑和数据进行较好的解耦,将这些内容分离后可以更好的做项目管理。

Vue&MVVM

一般的说法都是说Vue是MVVM的前端框架,但是Vue的重点其实是在实现ViewModel上,或者说Vue本身其实就是实现了个巨大的ViewModel。

前端面试点:MVVM

上图是Vue官方给出的示图,很清晰的说明了Vue对于MVVM三部分的定义,其中View部分就是真实DOM,Model则是一堆纯粹的JS对象(数据对象)。而Vue主要提供的就是中间的ViewModel的能力,通过模版和指令来实现数据绑定,监听DOM事件来完成行为响应,通过虚拟DOM来实现View层的更新。

在Vue中,每一个Vue实例,都是一个VM,每个VM里面包含了View的实例(DOM对象)vm.$el和需要用到的Model中的数据vm.$data,这里面包含所有在.Vue文件被用作数据的数据。举个例子:我们基于Vue搭建一个待办列表的项目,Model提供了业务数据:待办列表,里面包含了多条待办的各种数据,但是在todo-list.vue文件中,展示只需要用到待办的title、到期时间,应用逻辑需要id,那么vm.$data就是

{ id, title, date }这些来自Model,但是页面展示还需要序号、到期状态展示这些,这些是应用逻辑自己产生的数据,也是需要监听的。所以说是被用作数据(小集合)的数据(大集合)。

至于Binder,Vue2通过Object.defineProperty()实现,Vue3则是通过Proxy实现。

个人的思考

从整个项目的角度来看,我个人更愿意把Vue看做MVVM的一部分,即View和ViewModel。不管是采用什么模式(web、混合应用),在架构层面,Vue只是提供了页面展示的能力和触发应用逻辑的入口,而实际的应用逻辑和业务逻辑都属于后面的层级,以一个web项目为例,Vue提供了View和ViewModel。应用逻辑会分为两部分,一部分服务端提供的应用层的接口,另一部分则是在前端写的用于ViewModel中处理View的事件和通过ajax请求应用层的接口来执行应用逻辑,大致的交互逻辑是是这样:

前端面试点:MVVM

在代码结构的设计上,如果是比较复杂的项目,建议件前端的应用逻辑也做一层抽离(按前端的业务抽离成多多个模块),看过一些项目会把这部分也写在.vue文件中,直接写在生命周期和事件回调函数中,这样会导致应用逻辑与View模版过度耦合,大幅度降低应用逻辑的复用,在需要修改到某个应用逻辑的时候很不好修改。另外按照原则,应用/业务逻辑不应该与框架相关的过度耦合,灰度后续的业务升级、框架升级等事情造成困扰。

总结

本文是对MVVM的一些解析,个人的理解比较多,属于在工作中的一些思考的总结,不一定对,也不一定错。如果有补充或者不同的看法,欢迎一起讨论。