前言
本文是对设计模式总结的第四篇。 主要针对MVC、MVP、MVVM等设计模式 第一篇(创建型模式) 第二篇(结构型模式) 第三篇(行为型模式)
正文
MVC
概念
- MVC模式将程序分成三个部分:模型(Model),视图(View),控制器(Controller)
- Model模型层:业务数据的处理和存储,数据更新后更新
- View 视图层: 人机交互接口,一般为展示给用户的界面
- Controller 控制器层 : 负责连接 Model 层和 View 层,接受并处理 View 层触发的事件,并在 Model 层的数据状态变动时更新 View 层
- MVC 模式的目的是通过引入 Controller 层来将 Model 层和 View 层分离,分层的引入是原来大锅烩方式的改进,使得系统在可维护性和可读性上有了进步
典型流程
- MVC典型思路是View层通过事件通知到Controller层,Controller层经过对事件的处理完成相关业务逻辑,要求Model层改变数据状态,Model层再将数据更新到View层。
- 实际操作时,用户可以直接对View层的UI进行操作,以通过事件通知Controller层,通过处理后修改Model层的数据,Model层使用最新数据更新View
- 也可以通过直接触发Controller来更新Model层状态,再更新View层
- 某些场景下,View层直接采用观察者/发布订阅模式监听Model层的变化,这样View和Model层相互持有,互相操作,导致紧密耦合,在可维护性上有待提高,因此MVP模式出现了。
MVP(Model View Presenter)
概念
MVP模式将程序分为三个部分:模型(Model),视图(View),管理层(Presenter)
- Model模型层:只负责存储数据,与View呈现无关,也与UI处理逻辑无关,发生更新也不用主动通知View
- View视图层:人机交互接口,一般为UI界面
- Presenter:管理层:负责连接Model层和View层,处理View层的事件,负责获取数据并将获取的数据经过处理后更新View
- MVP模式将View层和Model层解耦,交互只能通过Presenter层,使得对View层的修改不会影响到Model层,而对Model层的数据改动也不会影响到View层
典型流程
View层触发的事件传递到Presenter层中处理,Presenter层去操作Model层,并且将数据返回给View层,这个过程中View层和Model层没有直接联系。而View层不部署业务逻辑,除了展示数据和触发事件之外,其它时间都在等着Presenter层来更新自己,被称为‘被动视图’ 实际操作时,用户可以直接对View层的UI进行操作,View层通知Presenter层,Presenter层操作Model层的数据,Presenter层获取到数据之后更新View 由于Presenter层负责了数据获取,数据处理,交互逻辑,UI效果等等功能,所以Presenter层就变得庞大起来。而View层只负责视图,Model和View层的责任纯粹而单一,如果我们需要添加或修改功能模块,只需要修改Presenter层即可。同时,Presenter层需要调用View层的方法来更新视图,使得Presenter层直接持有并依赖于View层
- 该模式的一个问题是,Presenter层需要调用View层的方法来更新视图,有时候会变得很复杂。由此引出来一个问题,为什么我们不实现一个机制来自动更新视图呢?MVVM模式就此诞生了。
MVVM(Model View ViewModel)
MVVM模式将程序分为三个部分:模型(Model),视图(View),视图模型(View-Model)
- 与MVP模式类似,Model层和View层也被隔离开来,彻底解耦,ViewModel层承担了Presenter层类似的功能,负责绑定Model层和View层,相比于MVP增加了双向绑定机制。 置,要定位原始出问题的地方也就不那么容易了
- 尽管双向绑定便捷了视图更新的操作,但是也给调试和错误定位带来困难,View层的异常可能是View的代码有问题,也有可能是Model层的问题。数据绑定使得一个位置的Bug被传递到别的位置
- 对于简单UI来说,实现MVVM模式的开销是不必要的,而对于大型应用来说,引入MVVM模式则会节约大量手动更新视图的复杂过程,是否使用还是看使用场景
Vue与MVVM模型
- 严格意义上来讲,Vue不能算是MVVM模式
- 这是因为MVVM模式要求Model层和View层完全解耦,而Vue框架提供了ref这样的API,使其Model有能力直接持有View
模块模式
- 模块是任何健壮的应用程序体系结构不可或缺的一部分,特点是有助于保持应用项目的代码单元既能清晰地分离又有组织
命名空间模式
- 命名空间模式是一个简单的模拟模块的方法,即创建一个全局对象,然后将变量和方法添加到这个全局对象
var MYNS = {}
MYNS.param1 = 'hello'
MYNS.param2 = 'world'
MYNS.param3 = { prop: 'name' }
MYNS.method1 = function() {
//...
}
- 这种方式可以隐藏系统中的变量冲突,但也有一些缺点
- 命名空间如果比较复杂,调用可能就会变成MYNS.param.prop.data...长长一溜,使用不便且代码量增加
- 变量嵌套关系越多,属性解析的性能消耗越多
- 安全性不佳,所有的成员都可以被访问到
模块模式
- 除了命名空间模式,也可以使用闭包的特性来模拟实现私有成员的功能来提升安全性,将要隐藏的变量和方法放在闭包中
var myModule = (function() {
var privateProp = '' // 私有变量
var privateMethod = function() { // 私有方法
console.log(privateProp)
}
return {
publicProp: 'foo', // 公有变量
publicMethod: function(prop) { // 共有方法
privateProp = prop
privateMethod()
}
}
})()
myModule.publicMethod('new prop') // 输出:new prop
myModule.privateProp // Uncaught TypeError: myModule.privateMethod is not a function
myModule.privateProp // undefined
- 这里的私有变量和私有方法,在闭包外面无法访问到,称为私有成员。而闭包返回的方法因为作用域的原因可以访问到私有成员,所以称为特权方法。
- 在模块创建时,可以将参数传递到闭包中,以更自由的创建模块,也可以方便地将全局变量传入模块中,导入全局变量有助于加速即时函数中的全局符号解析的速度,因为导入的变量成了该函数的局部变量
揭示模块模式
- 稍加改动上面的模块模式,就可以得到揭示模块模式,又叫暴露模块模式,在私有域中定义我们所有的函数和变量,并且返回一个匿名对象,把想要暴露出来的私有成员赋值给这个对象,使这些私有成员公开化
var myModule = (function() {
var privateProp = ''
var printProp = function() {
console.log(privateProp)
}
function setProp(prop) {
privateProp = prop
printProp()
}
return {
print: printProp,
set: setProp
}
})()
myModule.set('new prop') // 输出:new prop
myModule.setProp() // Uncaught TypeError: myModule.setProp is not a function
myModule.privateProp // undefined
- 揭示模块暴露出来的私有成员可以在被重命名后公开访问,也增强了可读性
ES6 module
- 继社区提出的 CommonJS 和 AMD 之类的方案之后,从 ES6 开始,JavaScript 就支持原生模块(module)了,下面我们一起来简单看一下 ES6 的 module
- ES6的module功能主要由两个命令组成export,import,export用于规定模块对外暴露的接口,import用于输入其他模块提供的接口,简单来说就是一个作为输出,一个作为输入 输出
// 写法一
export var a = 'a'
// 写法二
var b = 'b'
export { b }
// 写法三
var c = 'c'
export { c as e }
引入
import bar from './3.js' // 写法一
bar()
// 输出:foo
import { default as bar } from './3.js' // 写法二
bar()
// 输出:foo
- 注意export,import都必须写在模块顶层,如果处于块级作用域内,就会报错,因为处于条件代码块中,就没法做静态优化,违背了ES6模块的设计初衷。
链模式
- 通常情况下,通过对构造函数使用 new 会返回一个绑定到 this 上的新实例,所以我们可以在 new 出来的对象上直接用 . 访问其属性和方法。如果在普通函数中也返回当前实例,那么我们就可以使用 . 在单行代码中一次性连续调用多个方法,就好像它们被链接在一起一样,这就是链式调用,又称链模式。
代码实现
/* 四边形 */
var rectangle = {
length: null, // 长
width: null, // 宽
color: null, // 颜色
getSize: function() {
console.log(`length: ${ this.length }, width: ${ this.width }, color: ${ this.color }`)
},
/* 设置长度 */
setLength: function(length) {
this.length = length
return this
},
/* 设置宽度 */
setWidth: function(width) {
this.width = width
return this
},
/* 设置颜色 */
setColor: function(color) {
this.color = color
return this
}
}
var rect = rectangle
.setLength('100px')
.setWidth('80px')
.setColor('blue')
.getSize()
// 输出:length: 100px, width: 80px, color: blue
- 原理
- 所有对象都会继承其原型对象的属性和方法,所以我们可以让原型方法都返回该原型的实例对象,这样就可以对那些方法进行链式调用了