MVC
MVC是一種設(shè)計模式,它將應(yīng)用劃分為3個部分:數(shù)據(jù)(模型)醇王、展示層(視圖)和用戶交互層呢燥。結(jié)合一下下圖,更能理解三者之間的關(guān)系寓娩。
換句話說叛氨,一個事件的發(fā)生是這樣的過程
- 用戶和應(yīng)用交互
- 控制器的事件處理器被觸發(fā)
- 控制器從模型中請求數(shù)據(jù),并將其交給視圖
- 視圖將數(shù)據(jù)呈現(xiàn)給用戶
模型:用來存放應(yīng)用的所有數(shù)據(jù)對象棘伴。模型不必知曉視圖和控制器的細(xì)節(jié)寞埠,模型只需包含數(shù)據(jù)及直接和這些數(shù)據(jù)相關(guān)的邏輯。任何事件處理代碼焊夸、視圖模版仁连,以及那些和模型無關(guān)的邏輯都應(yīng)當(dāng)隔離在模型之外。
視圖:視圖層是呈現(xiàn)給用戶的阱穗,用戶與之產(chǎn)生交互饭冬。在javaScript應(yīng)用中使鹅,視圖大都是由html、css和JavaScript模版組成的昌抠。除了模版中簡單的條件語句之外患朱,視圖不應(yīng)當(dāng)包含任何其他邏輯。事實上和模型類似扰魂,視圖也應(yīng)該從應(yīng)用的其他部分中解耦出來
控制器:控制器是模型和視圖的紐帶麦乞。控制器從視圖獲得事件和輸入劝评,對它們進(jìn)行處理姐直,并相應(yīng)地更新視圖。當(dāng)頁面加載時蒋畜,控制器會給視圖添加事件監(jiān)聽声畏,比如監(jiān)聽表單提交和按鈕單擊。然后當(dāng)用戶和應(yīng)用產(chǎn)生交互時姻成,控制器中的事件觸發(fā)器就開始工作插龄。
例如JavaScript框架早期框架backbone就是采用的MVC模式。
上面的例子似乎太過空洞科展,下面講一個生活中的例子進(jìn)行講解:
1均牢、用戶提交一個新的聊天信息
2、控制器的事件處理器被觸發(fā)
3才睹、控制器創(chuàng)建了一個新的聊天模型
4徘跪、然后控制器更新視圖
5、用戶在聊天窗口看到新的聊天信息
講了一個生活的例子琅攘,我們用代碼的方式更加深入了解MVC垮庐。
Model
MVC中M表示model,與數(shù)據(jù)操作和行為相關(guān)的邏輯都應(yīng)當(dāng)放入模型中。例如我們創(chuàng)建一個Model對象坞琴,所有數(shù)據(jù)的操作都應(yīng)該都放在這個命名空間中哨查。下面是一些簡化的代碼,首先創(chuàng)建新模型和實例
var Model = {
create: function() {
this.records = {}
var object = Object.create(this)
object.prototype = Object.create(this.prototype)
return object
}
}
create用于創(chuàng)建一個以Model為原型的對象,然后就是一些包括數(shù)據(jù)操作的一些函數(shù)包括查找剧辐,存儲
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}
可以看到我們就已經(jīng)查找到了這個對象寒亥。模型也就是數(shù)據(jù)的部分我們也就完成了。
Control
下面來講講mvc中的控制器荧关。當(dāng)加載頁面的時候护盈,控制器將事件處理程序綁定在視圖中,并適時地處理回調(diào)羞酗,以及和模型必要的對接。下面是控制器的簡單例子:
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)
}
}
這樣我們就實現(xiàn)了對一個視圖的簡單控制紊服,鼠標(biāo)移入元素添加over class檀轨,移除就移除over class胸竞。然后在添加一些簡單的樣式例如
ex:
.over {color: red}
p{color: black}
這樣控制器就和視圖建立起了連接。在MVC中有一個特性就是一個控制器控制一個視圖参萄,隨著項目體積的增大卫枝,就需要一個狀態(tài)機用于管理這些控制器。先來創(chuàng)建一個狀態(tài)機
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)
}
}
// 創(chuàng)建兩個控制器
var con1 = {
activate: funtion() {
$('#con1').addClass('active')
},
deactivate: function () {
$('#con1').removeClass('active')
}
}
var con2 = {
activate: funtion() {
$('#con2').addClass('active')
},
deactivate: function () {
$('#con2').removeClass('active')
}
}
// 創(chuàng)建狀態(tài)機讹挎,添加狀態(tài)
var sm = new StateMachine
sm.add(con1)
sm.add(con2)
// 激活第一個狀態(tài)
con1.active()
這樣就實現(xiàn)了簡單的控制器管理校赤,最后我們在添加一些css樣式。
#con1, #con2 { display: none }
#con2.active, #con2.active { display: block }
當(dāng)con1激活的時候樣式就發(fā)生了變化筒溃,也就是視圖發(fā)生了變化马篮。
控制器也就講到了這里,下面來看看MVC中的View部分怜奖,也就是視圖
View
視圖是應(yīng)用的接口浑测,它為用戶提供視覺呈現(xiàn)并與用戶產(chǎn)生交互。在javaScript種歪玲,視圖是無邏輯的HTML片段迁央,又應(yīng)用的控制器來管理,視圖處理事件回調(diào)以及內(nèi)嵌數(shù)據(jù)滥崩。簡單來說就是在javaScript中寫HTML代碼岖圈,然后將HTML片段插入到HTML頁面中,這里講兩種方法:
動態(tài)渲染視圖
使用document.createElement創(chuàng)建DOM元素钙皮,設(shè)置他們的內(nèi)容然后追加到頁面中蜂科,例如
var views = documents.getElementById('views')
views.innerHTML = '' // 元素清空
var wapper = document.createElement('p')
wrapper.innerText = 'add to views'
views.appendChild(wrapper)
這樣就完成了用createElement創(chuàng)建元素,然后添加到HTML頁面中株灸。
模板
如果以前有過后端開發(fā)經(jīng)驗崇摄,那么對模版應(yīng)該比較熟悉。例如在nodejs中常用的就是ejs,下面是ejs的一個小例子慌烧,可以看到的是ejs將javascript直接渲染為HTML
str = '<h1><%= title %></h1>'
ejs.render(str, {
title: 'ejs'
});
那么這個渲染后的結(jié)果就是
<h1>ejs</h1>
當(dāng)然實際中ejs的功能更強大逐抑,我們甚至可以在其中加入函數(shù),模板語言是不是覺得跟vue,React的書寫方式特別像屹蚊,我也覺得像厕氨。那么view的作用就顯而易見了,就是將HTML和javaScript連接起來汹粤。剩下一個問題就是在mvc原理圖我們看到了視圖和模型之間的關(guān)系命斧,當(dāng)模型更改的時候,視圖也會跟著更新嘱兼。那么視圖和模型就需要進(jìn)行綁定国葬,它意味著當(dāng)記錄發(fā)生改變時,你的控制器不需要處理視圖的更新,因為這些更新是在后臺自動完成的汇四。為了將javaScript對象和視圖綁定在一起接奈,我們需要設(shè)置一個回調(diào)函數(shù),當(dāng)對象的屬性發(fā)生改變時發(fā)送一個更新視圖的通知通孽。下面是值發(fā)生變化的時候調(diào)用的回調(diào)函數(shù)序宦,當(dāng)然現(xiàn)在我們可以使用更簡單的set,get進(jìn)行數(shù)據(jù)的監(jiān)聽,這在我們后面的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()
這樣就實現(xiàn)了執(zhí)行和觸發(fā)change事件了互捌。
我相信大家對MVC有了比較深刻的理解,下面來學(xué)習(xí)MVVM模式行剂。
MVVM
如今主流的web框架基本都采用的是MVVM模式秕噪,為什么放棄了MVC模式,轉(zhuǎn)而投向了MVVM模式呢硼讽。在之前的MVC中我們提到一個控制器對應(yīng)一個視圖巢价,控制器用狀態(tài)機進(jìn)行管理,這里就存在一個問題固阁,如果項目足夠大的時候壤躲,狀態(tài)機的代碼量就變得非常臃腫,難以維護(hù)备燃。還有一個就是性能問題碉克,在MVC中我們大量的操作了DOM,而大量操作DOM會讓頁面渲染性能降低并齐,加載速度變慢漏麦,影響用戶體驗。最后就是當(dāng)Model頻繁變化的時候况褪,開發(fā)者就主動更新View撕贞,那么數(shù)據(jù)的維護(hù)就變得困難。世界是懶人創(chuàng)造的测垛,為了減小工作量捏膨,節(jié)約時間,一個更適合前端開發(fā)的架構(gòu)模式就顯得非常重要食侮。這時候MVVM模式在前端中的應(yīng)用就應(yīng)運而生号涯。
MVVM讓用戶界面和邏輯分離更加清晰。下面是MVVM的示意圖锯七,可以看到它由Model链快、ViewModel、View這三個部分組成眉尸。
下面分別來講講他們的作用
View
View是作為視圖模板域蜗,用于定義結(jié)構(gòu)巨双、布局。它自己不處理數(shù)據(jù)地消,只是將ViewModel中的數(shù)據(jù)展現(xiàn)出來炉峰。此外為了和ViewModel產(chǎn)生關(guān)聯(lián),那么還需要做的就是數(shù)據(jù)綁定的聲明脉执,指令的聲明,事件綁定的聲明戒劫。這在當(dāng)今流行的MVVM開發(fā)框架中體現(xiàn)的淋淋盡致半夷。在示例圖中,我們可以看到ViewModel和View之間是雙向綁定迅细,意思就是說ViewModel的變化能夠反映到View中巫橄,View的變化也能夠改變ViewModel的數(shù)據(jù)值。那如何實現(xiàn)雙向綁定呢茵典,例如有這個input元素:
<input type='text' yg-model='message'>
隨著用戶在Input中輸入值的變化湘换,在ViewModel中的message也會發(fā)生改變,這樣就實現(xiàn)了View到ViewModel的單向數(shù)據(jù)綁定统阿。下面是一些思路:
- 掃描看哪些節(jié)點有yg-xxx屬性
- 自動給這些節(jié)點加上onchange這種事件
- 更新ViewModel中的數(shù)據(jù)彩倚,例如ViewModel.message = xx.innerText
那么ViewModel到View的綁定可以是下面例子:
<p yg-text='message'></p>
渲染后p中顯示的值就是ViewModel中的message變量值。下面是一些思路:
- 首先注冊ViewModel
- 掃描整個DOM Tree 看哪些節(jié)點有yg-xxx這中屬性
- 記錄這些被單向綁定的DOM節(jié)點和ViewModel之間的隱射關(guān)系
- 使用innerText,innerHTML = ViewModel.message進(jìn)行賦值
ViewModel
ViewModel起著連接View和Model的作用扶平,同時用于處理View中的邏輯帆离。在MVC框架中,視圖模型通過調(diào)用模型中的方法與模型進(jìn)行交互结澄,然而在MVVM中View和Model并沒有直接的關(guān)系哥谷,在MVVM中,ViewModel從Model獲取數(shù)據(jù)麻献,然后應(yīng)用到View中们妥。相對MVC的眾多的控制器,很明顯這種模式更能夠輕松管理數(shù)據(jù)勉吻,不至于這么混亂监婶。還有的就是處理View中的事件,例如用戶在點擊某個按鈕的時候餐曼,這個行動就會觸發(fā)ViewModel的行為压储,進(jìn)行相應(yīng)的操作。行為就可能包括更改Model,重新渲染View源譬。
Model
Model 層集惋,對應(yīng)數(shù)據(jù)層的域模型,它主要做域模型的同步踩娘。通過 Ajax/fetch 等 API 完成客戶端和服務(wù)端業(yè)務(wù) Model 的同步刮刑。在層間關(guān)系里喉祭,它主要用于抽象出 ViewModel 中視圖的 Model。
MVVM簡單實現(xiàn)
實現(xiàn)效果:
<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為我們省去了手動更新視圖的步驟泛烙,一旦值發(fā)生變化,視圖就重新渲染翘紊,那么就需要對數(shù)據(jù)的改變就行檢測蔽氨。例如有這么一個例子:
hero = {
name: 'A'
}
這時候但我們訪問hero.name 的時候,就會打印出一些信息:
hero.name
// I'm A
當(dāng)我們對hero.name 進(jìn)行更改的時候帆疟,也會打印出一些信息:
hero.name = 'B'
// the name has change
這樣我們是不是就實現(xiàn)了數(shù)據(jù)的觀測了呢鹉究。
在Angular中實現(xiàn)數(shù)據(jù)的觀測使用的是臟檢查,就是在用戶進(jìn)行可能改變ViewModel的操作的時候踪宠,對比以前老的ViewModel然后做出改變自赔。
而在Vue中,采取的是數(shù)據(jù)劫持柳琢,就是當(dāng)數(shù)據(jù)獲取或者設(shè)置的時候绍妨,會觸發(fā)Object.defineProperty()。
這里我們采取的是Vue數(shù)據(jù)觀測的方法柬脸,簡單一些他去。下面是具體的代碼
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方法中,結(jié)果正如先前預(yù)料的一樣的結(jié)果肖粮。這樣數(shù)據(jù)的檢測也就實現(xiàn)了孤页,然后在通知訂閱者。如何通知訂閱者呢涩馆,我們需要實現(xiàn)一個消息訂閱器行施,維護(hù)一個數(shù)組用來收集訂閱者,數(shù)據(jù)變動觸發(fā)notify(),然后訂閱者觸發(fā)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] // 觸發(fā)get從而向訂閱器中添加訂閱者
Dep.target = null // 重置
};
Compile
在講MVVM概念的時候怒允,在View -> ViewModel的過程中有一個步驟就是在DOM tree中尋找哪個具有yg-xx的元素闸拿。這一節(jié)就是講解析模板必尼,讓View和ViewModel連接起來愕撰。遍歷DOM tree是非常消耗性能的览露,所以會先把節(jié)點el轉(zhuǎn)換為文檔碎片fragment進(jìn)行解析編譯操作。操作完成后丽惶,在將fragment添加到原來的真實DOM節(jié)點中炫七。下面是它的代碼
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) {
// 一旦屬性值有變化,就會收到通知執(zhí)行此更新函數(shù)钾唬,更新視圖
updateFn() && updateFn(node, val)
})
}
// 更新視圖
function updateFn (node, value) {
node.textContent = value
}
這樣編譯fragment就成功了,并且ViewModel中值的改變就能夠引起View層的改變万哪。接下來是Watcher的實現(xiàn),get方法已經(jīng)講了,我們來看看其他的方法抡秆。
Watcher
Watcher是連接Observer和Compile之間的橋梁壤圃。可以看到在Observer中琅轧,往訂閱器中添加了自己。dep.notice()發(fā)生的時候踊挠,調(diào)用了sub.update()乍桂,所以需要一個update()方法,值發(fā)生變化后效床,就能夠觸發(fā)Compile中的回調(diào)更新視圖睹酌。下面是Watcher的具體實現(xiàn)
var Watcher = function Watcher (vm, exp, cb) {
this.vm = vm
this.cb = cb
this.exp = exp
// 觸發(fā)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) // 執(zhí)行Compile中的回調(diào)
}
},
get: function () {
Dep.target = this
value = this.vm[exp] // 觸發(fā)getter
Dep.target = null
return value
}
}
在上面的代碼中Watcher就起到了連接Observer和Compile的作用剩檀,值發(fā)生改變的時候通知Watcher憋沿,然后Watcher調(diào)用update方法,因為在Compile中定義的Watcher沪猴,所以值發(fā)生改變的時候辐啄,就會調(diào)用Watcher()中的回調(diào),從而更新視圖运嗜。最重要的部分也就完成了壶辜。在加一個MVVM的構(gòu)造器就ok了。推薦一篇文章自己實現(xiàn)MVVM,這里邊講的更加詳細(xì)担租。
總結(jié)
ok砸民,本篇文章就結(jié)束了,通過對比希望讀者能夠?qū)η岸水?dāng)前框架能夠更清晰的認(rèn)識奋救。謝謝大家