翻译的文章,末尾你能找到原文地址~~~
最近,我在知乎上读到了几篇有趣的文章,它们在讨论 MVVM 相关的东西。伴随着 Angular 和 React 的崛起,MVVM 现在处在了风口上。
在这篇以及后续的文章中,我将说明 MVVM 是如何运作的,然后我们将写一个简易的 MVVM 框架。这第一篇文章内容涵盖了 MVVM 的简单介绍以及它尝试解决的问题。
如果你已经是一个老手,请忽略这篇文章。
MVVM 是啥?它又为何而与众不同?
MVVM 是 MVC 的一个变种。如果你知道 MVC,MVVM 将变得很容易理解。
你可以简单理解为:
MVVM 是在 adapter 模式中的 MVC。
所有的 M-V-* 模式都有同一个目标,那就是提供一种容易理解的方式来组织数据和界面的协同关系。
而不同点则在于它们如何划分各自的代码。
(注意:10 个开发者对 MVC 可能有 12 种解释,下面的内容纯粹是我个人的观点。)
模型(Model)-视图(View)-控制器(Controller)和它潜在的问题
MVC 模式试图将用户界面(View)和数据源(Model)独立开,同时由控制器(Controller)来处理用户输入。下面是它的工作原理:
于是,问题就出来了:
除非你在创建一个数据库可视化系统,否则你需要针对视图层和模型层做出适配。但其实这都不是合适的选择。模型负责处理业务逻辑(它是真实世界的一个抽象)。视图重点聚焦展示内容,所以它的数据结构将和它的展示方式高度耦合(而不是与它的展示内容相耦合,因为那会使得你的视图层难以复用)。
如果你对我上面说的不是很明白,那就看看下面的示例。
假如我们有一个用 JSON 存储的学生信息的数据集:
{
"first-name": "Tracy",
"last-name": "Kennedy",
"grade": 6,
"height": 150,
"weight": 40
}
我们将在一个列表视图中像这样展示:
-
Name:
Tracy Kennedy
-
Grade:
6
-
Height:
1.5m
-
Weight:
40kg
这就存在这几个问题:
- 原始数据中没有一个叫做
name
的字段,所以我们需要在哪里计算它? - 我们的
height
字段是以厘米为单位的,我们需要在哪里转换它? - 最烦人的一个,谁来负责将这一个对象形式的数据抽象成一个数组?
程序中的任何尝试解决上述问题的一个部分,其可复用性和可维护性都将极大地降低。我们很容易就倾向于通过继承来为这种情况创建专用的模型和视图。但是,假设你有在一个复杂应用中有数十个这种列表视图,你可能就要专门记录继承树。那将真是一场世界性的噩梦。
庞大的 视图-控制器
你可能注意到了,我故意忽略了控制器。事实上,在传统的 MVC 架构中,控制器并不算一个单独的层级。它总是伴随着视图而存在。视图和控制器塑造了用户交互的 视图-控制器 层。
但是控制器和视图又有一点不一样:它生来就是被污染了的。我是指,一个控制器是原本就是用来做适配和转换的(我们使用控制器来将用户输入转化为数据变动)。它原本就和视图以及模型高度耦合,并且难以复用。
是的,将那些繁杂的工作交给控制器处理看起来是一个不错的主意,所以我们稍微修改了一下 MVC 模式:
完美!现在我们将所有繁杂的工作都集中到了一个地方。
但是,你知道我要说“但是……”,对吧?
现在你又有了另一个问题。你最终可能在一个类之中包含了成千上万行的代码,使得它变得难以阅读和维护。最终这种变种被称为庞大的 视图-控制器 模式,因为一个控制器将变得非常巨大和复杂 :)
我并不是在批判这种尝试将适配的逻辑从模型和视图中独立出来的尝试。我是说,它是一个不错的尝试,但是它可以做得更好。
ViewModel 作为一个适配器
从某种程度上说,在软件行业中越小巧才越受欢迎。人们总是讨论更小的类、更小的函数和更小的组件……因为小通常等价于简单,而大则通常意味着复杂。
所以,避免过于庞大的控制器带给我们的复杂度,最简单的方式就是将它分为几个部分。一个庞大的控制器有以下三类主要的逻辑:
- 用户界面效果,比如翻页、滑动……;
- 用户界面更新——模型改变了;
- 指令——用户操作;
用户界面效果显然不同于另外两个,它应该跟视图层在一起。
用户界面更新呢?它应该属于处在模型层和视图层中间的某种存在。
好了,现在我们已经将控制器至少划分成了两个独立的部分。指令呢?它该去哪儿?
此时,这个有点难以下决定了。我们先将它放到一边。
我们先看一些示例代码:
先继续我们的学生信息页面。我们将借用 Vue.js 模板的语法。
-
{{item.description}}
{{item.content}}
上面的代码意味着我们将遍历名为 items
的列表。对于每一个其中的成员,我们将插入一个 标签并且填入一些这个成员的数据到
标签中。
所以,如果我们需要一个转换函数:
function items(student){
function item(description, content){
return {
description: description,
content: content
};
}
let result = []
result.push(item('Name', `${student['first-name']} ${student['last-name']}`));
result.push(item('Grade', student['grade']))
result.push(item('Height', `${student['height'] * 0.01}m`))
result.push(item('Weight', `${student['weight']}kg`));
}
上面的 items
函数是不能复用的,但是我们释放了我们的列表视图,它现在和业务逻辑相解耦了。
如果你能理解上面的示例,现在你就明白 M-V-VM 是如何运作的了。
我们示例中的 items
函数就是一个 View-Model。它将模型中返回的数据转化成视图可以接受的特殊结构。现在,我们将发现我们之前忽略了的问题:
谁将负责处理指令?
比如,我们将添加一个按钮来告诉系统学生信息是否是合法的:
-
{{item.description}}
{{item.content}}
谁来实现 confirm
和 reject
函数呢?
视图层调用了它们,但是视图层应该和业务无关。我认为我们应该毫不犹豫地将其加入到 View-Model 层,View-Model 扮演了适配器的角色,所以为啥不让它作为一个双向的适配器呢?
所以 M-V-VM 模式就像这样:
注意一些 M-V-VM 框架提供了双向数据绑定(WPF,knockout.js……),我认为你使用双向或单向绑定并不重要,重要的是,视图层能通过一个适配器层同模型层交互,而如何绑定视图层和 View-Model 层全凭你的个人喜好。于我而言,带指令的单向绑定是我的最爱。
下一篇 >
原文地址