MVC是一種設計模式,它將應用程式劃分為3個部分:資料(模型)、展示層(視圖)和使用者互動層。結合下圖,更能理解三者之間的關係。
換句話說,一個事件的發生是這樣的過程
使用者和應用程式互動
#控制器的事件處理器被觸發
控制器從模型中請求數據,並將其交給視圖
視圖將資料呈現給使用者
模型:用來存放應用程式的所有資料物件。模型不必知道視圖和控制器的細節,模型只需包含資料及直接和這些資料相關的邏輯。任何事件處理程式碼、視圖模版,以及那些和模型無關的邏輯都應隔離在模型之外。
視圖:視圖層是呈現給使用者的,使用者會與之產生互動。在javaScript應用程式中,視圖大都是由html、css和JavaScript模版組成的。除了模版中簡單的條件語句之外,視圖不應包含任何其他邏輯。事實上和模型類似,視圖也應該從應用的其他部分解耦出來
控制器:控制器是模型和視圖的紐帶。控制器從視圖獲得事件和輸入,對它們進行處理,並相應地更新視圖。當頁面載入時,控制器會為視圖新增事件監聽,例如監聽表單提交和按鈕點擊。然後當使用者和應用程式產生互動時,控制器中的事件觸發器就會開始工作。
例如JavaScript框架早期框架backbone就是採用的MVC模式。
上面的例子似乎太過空洞,下面講一個生活中的例子進行講解:
1、用戶提交一個新的聊天資訊
2、控制器的事件處理器被觸發
3、控制器創建了一個新的聊天模型
4、然後控制器更新視圖
5、用戶在聊天視窗看到新的聊天資訊
講了一個生活的例子,我們用程式碼的方式更加深入了解MVC。
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}
可以看到我們就已經查找到這個物件了。模型也就是資料的部分我們也就完成了。
下面來講關於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部分,也就是視圖
視圖是應用的接口,它為用戶提供視覺呈現並與使用者產生互動。在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 = '
title: 'ejs'
});
那么这个渲染后的结果就是
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的单向数据绑定。下面是一些思路:
扫描看哪些节点有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起着连接View和Model的作用,同时用于处理View中的逻辑。在MVC框架中,视图模型通过调用模型中的方法与模型进行交互,然而在MVVM中View和Model并没有直接的关系,在MVVM中,ViewModel从Model获取数据,然后应用到View中。相对MVC的众多的控制器,很明显这种模式更能够轻松管理数据,不至于这么混乱。还有的就是处理View中的事件,例如用户在点击某个按钮的时候,这个行动就会触发ViewModel的行为,进行相应的操作。行为就可能包括更改Model,重新渲染View。
Model 层,对应数据层的域模型,它主要做域模型的同步。通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。在层间关系里,它主要用于抽象出 ViewModel 中视图的 Model。
实现效果:
<p id="mvvm"> <input type="text" v-model="message"> <p>{{message}}</p> <button v-click='changeMessage'></button> </p> <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,这里边讲的更加详细。
相关推荐:
以上是MVC、MVVM簡單實作方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!