MVC是一种设计模式,它将应用划分为3个部分:数据(模型)、展现层(视图)和用户交互层。结合一下下图,更能理解三者之间的关系。
换句话说,一个事件的发生是这样的过程javascript
模型:用来存放应用的全部数据对象。模型没必要知晓视图和控制器的细节,模型只需包含数据及直接和这些数据相关的逻辑。任何事件处理代码、视图模版,以及那些和模型无关的逻辑都应当隔离在模型以外。
视图:视图层是呈现给用户的,用户与之产生交互。在javaScript应用中,视图大都是由html、css和JavaScript模版组成的。除了模版中简单的条件语句以外,视图不该当包含任何其余逻辑。事实上和模型相似,视图也应该从应用的其余部分中解耦出来
控制器:控制器是模型和视图的纽带。控制器从视图得到事件和输入,对它们进行处理,并相应地更新视图。当页面加载时,控制器会给视图添加事件监听,好比监听表单提交和按钮单击。而后当用户和应用产生交互时,控制器中的事件触发器就开始工做。
例如JavaScript框架早期框架backbone就是采用的MVC模式。css
上面的例子彷佛太过空洞,下面讲一个生活中的例子进行讲解:
一、用户提交一个新的聊天信息
二、控制器的事件处理器被触发
三、控制器建立了一个新的聊天模型
四、而后控制器更新视图
五、用户在聊天窗口看到新的聊天信息
讲了一个生活的例子,咱们用代码的方式更加深刻了解MVC。html
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为原型的对象,而后就是一些包括数据操做的一些函数包括查找,存储vue
var Model = { /*---代码片断--*/ find: function () { return this.records[this.id] }, save: function () { this.records[this.id] = this } }
下面咱们就可使用这个Model了:java
user = Model.create() user.id = 1 user.save() asset = Model.create() asset.id = 2 asset.save() Model.find(1) => {id:1}
能够看到咱们就已经查找到了这个对象。模型也就是数据的部分咱们也就完成了。node
下面来说讲mvc中的控制器。当加载页面的时候,控制器将事件处理程序绑定在视图中,并适时地处理回调,以及和模型必要的对接。下面是控制器的简单例子:git
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。而后在添加一些简单的样式例如github
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样式。web
#con1, #con2 { display: none } #con2.active, #con2.active { display: block }
当con1激活的时候样式就发生了变化,也就是视图发生了变化。
控制器也就讲到了这里,下面来看看MVC中的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模式。
现在主流的web框架基本都采用的是MVVM模式,为何放弃了MVC模式,转而投向了MVVM模式呢。在以前的MVC中咱们提到一个控制器对应一个视图,控制器用状态机进行管理,这里就存在一个问题,若是项目足够大的时候,状态机的代码量就变得很是臃肿,难以维护。还有一个就是性能问题,在MVC中咱们大量的操做了DOM,而大量操做DOM会让页面渲染性能下降,加载速度变慢,影响用户体验。最后就是当Model频繁变化的时候,开发者就主动更新View,那么数据的维护就变得困难。世界是懒人创造的,为了减少工做量,节约时间,一个更适合前端开发的架构模式就显得很是重要。这时候MVVM模式在前端中的应用就应运而生。
MVVM让用户界面和逻辑分离更加清晰。下面是MVVM的示意图,能够看到它由Model、ViewModel、View这三个部分组成。
下面分别来说讲他们的做用
View是做为视图模板,用于定义结构、布局。它本身不处理数据,只是将ViewModel中的数据展示出来。此外为了和ViewModel产生关联,那么还须要作的就是数据绑定的声明,指令的声明,事件绑定的声明。这在当今流行的MVVM开发框架中体现的淋淋尽致。在示例图中,咱们能够看到ViewModel和View之间是双向绑定,意思就是说ViewModel的变化可以反映到View中,View的变化也可以改变ViewModel的数据值。那如何实现双向绑定呢,例若有这个input元素:
<input type='text' yg-model='message'>
随着用户在Input中输入值的变化,在ViewModel中的message也会发生改变,这样就实现了View到ViewModel的单向数据绑定。下面是一些思路:
那么ViewModel到View的绑定能够是下面例子:
<p yg-text='message'></p>
渲染后p中显示的值就是ViewModel中的message变量值。下面是一些思路:
ViewModel起着链接View和Model的做用,同时用于处理View中的逻辑。在MVC框架中,视图模型经过调用模型中的方法与模型进行交互,然而在MVVM中View和Model并无直接的关系,在MVVM中,ViewModel从Model获取数据,而后应用到View中。相对MVC的众多的控制器,很明显这种模式更可以轻松管理数据,不至于这么混乱。还有的就是处理View中的事件,例如用户在点击某个按钮的时候,这个行动就会触发ViewModel的行为,进行相应的操做。行为就可能包括更改Model,从新渲染View。
Model 层,对应数据层的域模型,它主要作域模型的同步。经过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。在层间关系里,它主要用于抽象出 ViewModel 中视图的 Model。
实现效果:
<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的一些方法
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 // 重置 };
在讲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是链接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,本篇文章就结束了,经过对比但愿读者可以对前端当前框架可以更清晰的认识。谢谢你们