前端MVC、MVVM的简单实现
MVC
MVC是一种设计模式,它将应用划分为3个部分:数据(模型)、展示层(视图)和用户交互层。结合一下下图,更能理解三者之间的关系。
换句话说,一个事件的发生是这样的过程
- 用户和应用交互
- 控制器的事件处理器被触发
- 控制器从模型中请求数据,并将其交给视图
- 视图将数据呈现给用户
模型:用来存放应用的所有数据对象。模型不必知晓视图和控制器的细节,模型只需包含数据及直接和这些数据相关的逻辑。任何事件处理代码、视图模版,以及那些和模型无关的逻辑都应当隔离在模型之外。
视图:视图层是呈现给用户的,用户与之产生交互。在javaScript应用中,视图大都是由html、css和JavaScript模版组成的。除了模版中简单的条件语句之外,视图不应当包含任何其他逻辑。事实上和模型类似,视图也应该从应用的其他部分中解耦出来
控制器:控制器是模型和视图的纽带。控制器从视图获得事件和输入,对它们进行处理,并相应地更新视图。当页面加载时,控制器会给视图添加事件监听,比如监听表单提交和按钮单击。然后当用户和应用产生交互时,控制器中的事件触发器就开始工作。
例如JavaScript框架早期框架backbone就是采用的MVC模式。
上面的例子似乎太过空洞,下面讲一个生活中的例子进行讲解:
1、用户提交一个新的聊天信息
2、控制器的事件处理器被触发
3、控制器创建了一个新的聊天模型
4、然后控制器更新视图
5、用户在聊天窗口看到新的聊天信息
讲了一个生活的例子,我们用代码的方式更加深入了解MVC。
Model
MVC中M表示model,与数据操作和行为相关的逻辑都应当放入模型中。例如我们创建一个Model对象,所有数据的操作都应该都放在这个命名空间中。下面是一些简化的代码,首先创建新模型和实例
var Model = {
create: function() {
this.records = {}
var object = Object.create(this)
object.prototype = Object.create(this.prototype)
return object
}
}
create用于创建一个以Model为原型的对象,然后就是一些包括数据操作的一些函数包括查找,存储
var Model = {
/*---代码片段--*/
find: function () {
return this.records[this.id]
},
save: function () {
this.records[this.id] = this
}
}
下面我们就可以使用这个Model了:
user = Model.create()
user.id = 1
user.save()
asset = Model.create()
asset.id = 2
asset.save()
Model.find(1)
=> {id:1}
可以看到我们就已经查找到了这个对象。模型也就是数据的部分我们也就完成了。
Control
下面来讲讲mvc中的控制器。当加载页面的时候,控制器将事件处理程序绑定在视图中,并适时地处理回调,以及和模型必要的对接。下面是控制器的简单例子:
var ToggleView = {
init: function (view) {
this.view = $(view)
this.view.mouseover(this.toggleClass, true)
this.view.mouseout(this.toggleClass, false)
},
this.toggleClass: function () {
this.view.toggleClass('over', e.data)
}
}
这样我们就实现了对一个视图的简单控制,鼠标移入元素添加over class,移除就移除over class。然后在添加一些简单的样式例如
ex:
.over {color: red}
p{color: black}
这样控制器就和视图建立起了连接。在MVC中有一个特性就是一个控制器控制一个视图,随着项目体积的增大,就需要一个状态机用于管理这些控制器。先来创建一个状态机
var StateMachine = function() {}
SateMachine.add = function (controller) {
this.bind('change', function (e, current) {
if (controller == current) {
controller.activate()
} else {
controller.deactivate()
}
})
controller.active = function () {
this.trigger('change', controller)
}
}
// 创建两个控制器
var con1 = {
activate: funtion() {
$('#con1').addClass('active')
},
deactivate: function () {
$('#con1').removeClass('active')
}
}
var con2 = {
activate: funtion() {
$('#con2').addClass('active')
},
deactivate: function () {
$('#con2').removeClass('active')
}
}
// 创建状态机,添加状态
var sm = new StateMachine
sm.add(con1)
sm.add(con2)
// 激活第一个状态
con1.active()
这样就实现了简单的控制器管理,最后我们在添加一些css样式。
#con1, #con2 { display: none }
#con2.active, #con2.active { display: block }
当con1激活的时候样式就发生了变化,也就是视图发生了变化。
控制器也就讲到了这里,下面来看看MVC中的View部分,也就是视图
View
视图是应用的接口,它为用户提供视觉呈现并与用户产生交互。在javaScript种,视图是无逻辑的HTML片段,又应用的控制器来管理,视图处理事件回调以及内嵌数据。简单来说就是在javaScript中写HTML代码,然后将HTML片段插入到HTML页面中,这里讲两种方法:
动态渲染视图
使用document.createElement创建DOM元素,设置他们的内容然后追加到页面中,例如
var views = documents.getElementById('views')
views.innerHTML = '' // 元素清空
var wapper = document.createElement('p')
wrapper.innerText = 'add to views'
views.appendChild(wrapper)
这样就完成了用createElement创建元素,然后添加到HTML页面中。
模板
如果以前有过后端开发经验,那么对模版应该比较熟悉。例如在nodejs中常用的就是ejs,下面是ejs的一个小例子,可以看到的是ejs将javascript直接渲染为HTML
str = '<h1><%= title %></h1>'
ejs.render(str, {
title: 'ejs'
});
那么这个渲染后的结果就是
<h1>ejs</h1>
当然实际中ejs的功能更强大,我们甚至可以在其中加入函数,模板语言是不是觉得跟vue,React的书写方式特别像,我也觉得像。那么view的作用就显而易见了,就是将HTML和javaScript连接起来。剩下一个问题就是在mvc原理图我们看到了视图和模型之间的关系,当模型更改的时候,视图也会跟着更新。那么视图和模型就需要进行绑定,它意味着当记录发生改变时,你的控制器不需要处理视图的更新,因为这些更新是在后台自动完成的。为了将javaScript对象和视图绑定在一起,我们需要设置一个回调函数,当对象的属性发生改变时发送一个更新视图的通知。下面是值发生变化的时候调用的回调函数,当然现在我们可以使用更简单的set,get进行数据的监听,这在我们后面的MVVM将会讲到。
var addChange = function (ob) {
ob.change = function (callback) {
if (callback) {
if (!this._change) this._change = {}
this._change.push(callback)
} else {
if (!this._change) return
for (var i = this._change.length - 1; i >= 0; i--) {
this._change[i].apply(this)
}
}
}
}
我们来看看一个实际的例子
var addChange = function (ob) {
ob.change = function (callback) {
if (callback) {
if (!this._change) this._change = {}
this._change.push(callback)
} else {
if (!this._change) return
for (var i = this._change.length - 1; i >= 0; i--) {
this._change[i].apply(this)
}
}
}
}
var object = {}
object.name = 'Foo'
addChange(object)
object.change(function () {
console.log('Changed!', this)
// 更新视图的代码
})
obejct.change()
object.name = 'Bar'
object.change()
这样就实现了执行和触发change事件了。
我相信大家对MVC有了比较深刻的理解,下面来学习MVVM模式。
MVVM
如今主流的web框架基本都采用的是MVVM模式,为什么放弃了MVC模式,转而投向了MVVM模式呢。在之前的MVC中我们提到一个控制器对应一个视图,控制器用状态机进行管理,这里就存在一个问题,如果项目足够大的时候,状态机的代码量就变得非常臃肿,难以维护。还有一个就是性能问题,在MVC中我们大量的操作了DOM,而大量操作DOM会让页面渲染性能降低,加载速度变慢,影响用户体验。最后就是当Model频繁变化的时候,开发者就主动更新View,那么数据的维护就变得困难。世界是懒人创造的,为了减小工作量,节约时间,一个更适合前端开发的架构模式就显得非常重要。这时候MVVM模式在前端中的应用就应运而生。
MVVM让用户界面和逻辑分离更加清晰。下面是MVVM的示意图,可以看到它由Model、ViewModel、View这三个部分组成。
下面分别来讲讲他们的作用
View
View是作为视图模板,用于定义结构、布局。它自己不处理数据,只是将ViewModel中的数据展现出来。此外为了和ViewModel产生关联,那么还需要做的就是数据绑定的声明,指令的声明,事件绑定的声明。这在当今流行的MVVM开发框架中体现的淋淋尽致。在示例图中,我们可以看到ViewModel和View之间是双向绑定,意思就是说ViewModel的变化能够反映到View中,View的变化也能够改变ViewModel的数据值。那如何实现双向绑定呢,例如有这个input元素:
<input type='text' yg-model='message'>
随着用户在Input中输入值的变化,在ViewModel中的message也会发生改变,这样就实现了View到ViewModel的单向数据绑定。下面是一些思路:
- 扫描看哪些节点有yg-xxx属性
- 自动给这些节点加上onchange这种事件
- 更新ViewModel中的数据,例如ViewModel.message = xx.innerText
那么ViewModel到View的绑定可以是下面例子:
<p yg-text='message'></p>
渲染后p中显示的值就是ViewModel中的message变量值。下面是一些思路:
- 首先注册ViewModel
- 扫描整个DOM Tree 看哪些节点有yg-xxx这中属性
- 记录这些被单向绑定的DOM节点和ViewModel之间的隐射关系
- 使用innerText,innerHTML = ViewModel.message进行赋值
ViewModel
ViewModel起着连接View和Model的作用,同时用于处理View中的逻辑。在MVC框架中,视图模型通过调用模型中的方法与模型进行交互,然而在MVVM中View和Model并没有直接的关系,在MVVM中,ViewModel从Model获取数据,然后应用到View中。相对MVC的众多的控制器,很明显这种模式更能够轻松管理数据,不至于这么混乱。还有的就是处理View中的事件,例如用户在点击某个按钮的时候,这个行动就会触发ViewModel的行为,进行相应的操作。行为就可能包括更改Model,重新渲染View。
Model
Model 层,对应数据层的域模型,它主要做域模型的同步。通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。在层间关系里,它主要用于抽象出 ViewModel 中视图的 Model。
MVVM简单实现
实现效果:
<div id="mvvm">
<input type="text" v-model="message">
<p>{{message}}</p>
<button v-click='changeMessage'></button>
</div>
<script type="">
const vm = new MVVM({
el: '#mvvm',
methods: {
changeMessage: function () {
this.message = 'message has change'
}
},
data: {
message: 'this is old message'
}
})
</script>
这里为了简单,借鉴了Vue的一些方法
Observer
MVVM为我们省去了手动更新视图的步骤,一旦值发生变化,视图就重新渲染,那么就需要对数据的改变就行检测。例如有这么一个例子:
hero = {
name: 'A'
}
这时候但我们访问hero.name 的时候,就会打印出一些信息:
hero.name
// I'm A
当我们对hero.name 进行更改的时候,也会打印出一些信息:
hero.name = 'B'
// the name has change
这样我们是不是就实现了数据的观测了呢。
在Angular中实现数据的观测使用的是脏检查,就是在用户进行可能改变ViewModel的操作的时候,对比以前老的ViewModel然后做出改变。
而在Vue中,采取的是数据劫持,就是当数据获取或者设置的时候,会触发Object.defineProperty()。
这里我们采取的是Vue数据观测的方法,简单一些。下面是具体的代码
function observer (obj) {
let keys = Object.keys(obj)
if (typeof obj === 'object' && !Array.isArray(obj)) {
keys.forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
function defineReactive (obj, key, val) {
observer(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('I am A')
return val
},
set: function (newval) {
console.log('the name has change')
observer(val)
val = newval
}
})
}
把hero带入observe方法中,结果正如先前预料的一样的结果。这样数据的检测也就实现了,然后在通知订阅者。如何通知订阅者呢,我们需要实现一个消息订阅器,维护一个数组用来收集订阅者,数据变动触发notify(),然后订阅者触发update()方法,改善后的代码长这样:
function defineReactive (obj) {
dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('I am A')
Dep.target || dep.depend()
return val
},
set: function (newval) {
console.log('the name has change')
dep.notify()
observer(val)
val = newval
}
})
}
var Dep = function Dep () {
this.subs = []
}
Dep.prototype.notify = function(){
var subs = this.subs.slice()
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Dep.prototype.addSub = function(sub){
this.subs.push(sub)
}
Dep.prototype.depend = function(){
if (Dep.target) {
Dep.target.addDep(this)
}
}
这跟Vue源码差不多,就完成了往订阅器里边添加订阅者,和通知订阅者。这里以前我看Vue源码的时候,困扰了很久的问题,就是在get方法中Dep是哪儿来的。这里说一下他是一个全局变量,添加target变量是用于向订阅器中添加订阅者。这里的订阅者是Wacther,Watcher就可以连接视图更新视图。下面是Watcher的一部分代码
Watcher.prototype.get = function(key){
Dep.target = this
this.value = obj[key] // 触发get从而向订阅器中添加订阅者
Dep.target = null // 重置
};
Compile
在讲MVVM概念的时候,在View -> ViewModel的过程中有一个步骤就是在DOM tree中寻找哪个具有yg-xx的元素。这一节就是讲解析模板,让View和ViewModel连接起来。遍历DOM tree是非常消耗性能的,所以会先把节点el转换为文档碎片fragment进行解析编译操作。操作完成后,在将fragment添加到原来的真实DOM节点中。下面是它的代码
function Compile (el) {
this.el = document.querySelector(el)
this.fragment = this.init()
this.compileElement()
}
Compile.prototype.init = function(){
var fragment = document.createDocumentFragment(), chid
while (child.el.firstChild) {
fragment.appendChild(child)
}
return fragment
};
Compile.prototype.compileElement = function(){
fragment = this.fragment
me = this
var childNodes = el.childNodes
[].slice.call(childNodes).forEach(function (node) {
var text = node.textContent
var reg = /\{\{(.*)\}\}/ // 获取{{}}中的值
if (reg.test(text)) {
me.compileText(node, RegExp.$1)
}
if (node.childNodes && node.childNodes.length) {
me.compileElement(node)
}
})
}
Compile.prototype.compileText = function (node, vm, exp) {
updateFn && updateFn(node, vm[exp])
new Watcher(vm, exp, function (value, oldValue) {
// 一旦属性值有变化,就会收到通知执行此更新函数,更新视图
updateFn() && updateFn(node, val)
})
}
// 更新视图
function updateFn (node, value) {
node.textContent = value
}
这样编译fragment就成功了,并且ViewModel中值的改变就能够引起View层的改变。接下来是Watcher的实现,get方法已经讲了,我们来看看其他的方法。
Watcher
Watcher是连接Observer和Compile之间的桥梁。可以看到在Observer中,往订阅器中添加了自己。dep.notice()发生的时候,调用了sub.update(),所以需要一个update()方法,值发生变化后,就能够触发Compile中的回调更新视图。下面是Watcher的具体实现
var Watcher = function Watcher (vm, exp, cb) {
this.vm = vm
this.cb = cb
this.exp = exp
// 触发getter,向订阅器中添加自己
this.value = this.get()
}
Watcher.prototype = {
update: function () {
this.run()
},
addDep: function (dep) {
dep.addSub(this)
},
run: function () {
var value = this.get()
var oldVal = this.value
if (value !== oldValue) {
this.value = value
this.cb.call(this.vm, value, oldValue) // 执行Compile中的回调
}
},
get: function () {
Dep.target = this
value = this.vm[exp] // 触发getter
Dep.target = null
return value
}
}
在上面的代码中Watcher就起到了连接Observer和Compile的作用,值发生改变的时候通知Watcher,然后Watcher调用update方法,因为在Compile中定义的Watcher,所以值发生改变的时候,就会调用Watcher()中的回调,从而更新视图。最重要的部分也就完成了。在加一个MVVM的构造器就ok了。推荐一篇文章自己实现MVVM,这里边讲的更加详细。
总结
ok,本篇文章就结束了,通过对比希望读者能够对前端当前框架能够更清晰的认识。谢谢大家
前端MVC、MVVM的简单实现的更多相关文章
- 前端mvc mvp mvvm 架构介绍(vue重构项目一)
首先 我们为什么重构这个项目 1:我们现有的技术是前后台不分离,页面上采用esayUI+jq构成的单页面,每个所谓的单页面都是从后台胜场的唯一Id 与前端绑定,即使你找到了那个页面元素,也找不到所在的 ...
- 前端MVC学习总结(一)——MVC概要与angular概要、模板与数据绑定
一.前端MVC概要 1.1.库与框架的区别 框架是一个软件的半成品,在全局范围内给了大的约束.库是工具,在单点上给我们提供功能.框架是依赖库的.AngularJS是框架而jQuery则是库. 1.2. ...
- 前端MVC框架、类库、UI框架选择
CSS预处理器sass(基于Ruby服务端版)less(客户端版:基于js; 服务端版:基于nodejs) 前端UI框架JqueryMiniUI: http://www.miniui.com/(适用于 ...
- 侃侃前端MVC设计模式
前言 前端的MVC,近几年一直很火,大家也都纷纷讨论着,于是乎,抽空总结一下这个知识点.看了些文章,结合实践略作总结并发表一下自己的看法. 最初接触MVC是后端Java的MVC架构,用一张图来表示之— ...
- 前端MVC学习笔记(一)——MVC概要与angular概要、模板与数据绑定
一.前端MVC概要 1.1.库与框架的区别 框架是一个软件的半成品,在全局范围内给了大的约束.库是工具,在单点上给我们提供功能.框架是依赖库的.AngularJS是框架而jQuery则是库. 1.2. ...
- 前端MVC Vue2学习总结(一)——MVC与vue2概要、模板、数据绑定与综合示例
一.前端MVC概要 1.1.库与框架的区别 框架是一个软件的半成品,在全局范围内给了大的约束.库是工具,在单点上给我们提供功能.框架是依赖库的.Vue是框架而jQuery则是库. 1.2.AMD与CM ...
- 前端框架MVVM是什么(整理)
前端框架MVVM是什么(整理) 一.总结 一句话总结:vm层(视图模型层)通过接口从后台m层(model层)请求数据,vm层继而和v(view层)实现数据的双向绑定. 1.我大前端应该不应该做复杂的数 ...
- 前后端分层架构MVC&MVVM
早期 特点 页面由 JSP.PHP 等工程师在服务端生成 JSP 里揉杂大量业务代码 浏览器负责展现,服务端给什么就展现什么,展现的控制在 Web Server 层 优点 简单明快,本地起一个 Tom ...
- 【blade的UI设计】理解前端MVC与分层思想
前言 最近校招要来了,很多大三的同学一定按捺不住心中的焦躁,其中有期待也有彷徨,或许更多的是些许担忧,最近在开始疯狂的复习了吧 这里小钗有几点建议给各位: ① 不要看得太重,关心则乱,太紧张反而表现不 ...
- 我的前端MVC之路
大约十几个月前,了解到时下前端MVC之火爆,同事推荐我了解一下angular.当时也不是特别在意,只是稍稍阅读了一遍官方文档,并尝试了文档上的例子.其实当时也颇有震惊之感的,原来代码还可以这么写!看完 ...
随机推荐
- 网站更换服务器出现加载不了js css文件的问题
原因是 里面加找不到.woff类型,后面把上面注释掉就可以了
- JS基础_强制类型转换-String
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- springboot页面模板thymeleaf的简单用法
thymeleaf基础语法: 变量输出与字符串操作: th:text 表示在页面输出值 th:value 表示将一个值放入input标签的value中 判断字符串是否为空: thymele ...
- 多线程编程-- part 6 共享锁和ReentrantReadWriteLock
介绍: ReadWriteLock,顾名思义,是读写锁.它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作.(1)“读取锁”用于只读操作,它是“共享锁”,能同时 ...
- python、第二篇:库相关操作
一 系统数据库 information_schema: 虚拟库,不占用磁盘空间,存储的是数据库启动后的一些参数,如用户表信息.列信息.权限信息.字符信息等performance_schema: MyS ...
- 31C3 CTF web关writeup
0x00 背景 31c3 CTF 还是很人性化的,比赛结束了之后还可以玩.看题解做出了当时不会做的题目,写了一个writeup. 英文的题解可以看这:https://github.com/ctfs/w ...
- 2019.9.25使用BP和Hydra爆破相关的服务
使用BP和Hydra爆破相关的服务. Hydra:九头蛇,开源的功能强大的爆破工具,支持的服务有很多,使用hydra爆破c/s架构的服务.使用bp爆破web登录端口. dvwa:web应用程序漏洞演练 ...
- Arch Linux 安装 ibus-rime
参考网站 default.custom.yaml 在方案選單中添加五筆.雙拼 rime-wubi 操作方式 # 删除原rime(可选) sudo pacman -Rs ibus-rime ibus-t ...
- 生成大量插入语句,并将语句写入txt文件中
import java.io.*; /** * Created by czz on 2019/9/23. */ public class TTest { /** * 生成大量插入语句,并将语句写入tx ...
- PHP的函数获取图片的宽高等信息
PHP的函数getimagesize可以得到图片的宽高等信息 array getimagesize ( string $filename [, array &$imageinfo ] ) ...