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'
});
複製代碼
那麼這個渲染後的結果就是
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,本篇文章就結束了,經過對比但願讀者可以對前端當前框架可以更清晰的認識。謝謝你們