和一般文章不同,本文不依賴於任何現有的框架,也不試圖陷入冗長的發展歷史,而是完全從頭開始,以一個儘可能小但是可以說明問題的案例,以此講清楚MVC這個歷史悠久、變型極多的技術理念。MVC是一種非常普及的,基礎的設計套路,在不同的語言社群內都有著大量的應用。理解了MVC,學習接下來的MVVM、MVP等才能成為可能。
MVC把一個系統的類分為三種:Model,View和Controller。它們也遵循著職責分離原則:
- 由Controller來處理訊息
- 由Model掌管資料來源
- 由View負責資料顯示
儘管MVC看起來複雜,其實用程式碼表達最簡單的MVC是可能的:
/** 模擬 Model, View, Controller */
var M = {}, V = {}, C = {};
/** Model 負責資料 */
M.data = "hello world";
/** View 負責輸出 */
V.render = (M) => { alert(M.data); }
/** Controller 搭橋*/
C.handleOnload = () => { V.render(M); }
window.onload = C.handleOnload;
複製程式碼
只要是和使用者互動的都是View,所以使用alert,或者直接輸出到console,都是一種View。
接下來,我希望用一個完整但是極簡的程式,來驗證MVC如何從一個日常的程式進化而來的。這個一個小型程式只不過是如此:
1
<div id="app">
<p><span id="count">0</span>
<button id="inc">+</button>
<button id="dec">-</button>
</p>
</div>
<script>
var counter = document.getElementById('count');
var btn1 = document.getElementById('inc');
var btn2 = document.getElementById('dec');
var count = 0;
btn1.addEventListener('click',function (){
counter.innerHTML = ++count;
}
)
btn2.addEventListener('click',function (){
counter.innerHTML = --count;
}
)
</script>
複製程式碼
當前的小型程式,所有的程式碼,無論資料還是邏輯還是UI程式碼,都是混合在一起的,並沒有所謂的任何的職責分離。因為還小,問題不大。但是產品程式碼都是從這一的基礎上逐步長大的。比如說資料就不太可能只有一個count,程式碼逐步增大,一個物件的資料屬性會越來越多,隨著來的是運算元據的函式也會越來越多。同理包括使用者介面和業務邏輯。
如果使用MVC的眼光來看,在此微觀模式下,其實可以使用MVC的模式做程式碼的職責分離。其中所有的UI元素物件,都應該放置到View型別內,其中的事件處理都是應該放到Controller內,而資料,也就是這裡的count變數和對它的操作(減一加一),應該放置到Model類內,組裝Model和View則是Controller的職責。這樣的思路下,程式碼可以改成:
<div id="app">
<p><span id="count">1</span>
<button id="inc">+</button>
<button id="dec">-</button>
</p>
</div>
<script>
class Model{
constructor(){
this.count = 1
}
inc(){
return this.count++
}
dec(){
return this.count--
}
}
class View{
constructor(){
this.counter = document.getElementById('count')
this.btn1 = document.getElementById('inc')
this.btn2 = document.getElementById('dec')
}
setCount(c){
this.counter.innerHTML = c
}
attachInc(cb){
this.btn1.addEventListener('click',cb)
}
attachDec(cb){
this.btn2.addEventListener('click',cb)
}
}
class Controller {
constructor(){
this.m = new Model()
this.v = new View()
this.v.attachInc(this.onInc.bind(this))
this.v.attachDec(this.onDec.bind(this))
}
onInc(){
this.v.setCount(this.m.inc())
}
onDec(){
this.v.setCount(this.m.dec())
}
}
var c = new Controller()
</script>
複製程式碼
將應用程式劃分為三種元件,模型 - 檢視 - 控制器(MVC)設計定義它們之間的相互作用。
Model用於封裝資料以及對資料的處理方法。Model不依賴“View”和“Controller”,也就是說, Model 不關心它會被如何顯示或是如何被操作。Model 中資料的變化一般會通過一種重新整理機制被公佈。為此,Model需要提供被監聽的機制。
View能夠實現顯示。在 View 中一般沒有程式上的邏輯。為了實現Model變化後的響應View 上的重新整理功能,View需要監聽Model的變化。
控制器(Controller)起到不同層面間的組織作用,用於控制應用程式的流程。它處理事件並作出響應。“事件”包括使用者的行為和資料 Model 上的改變。
實際上,通過觀察者模式,可以把Model的修改重新整理到多個檢視中,只要檢視做一個Model變化的訂閱即可。可以先做一個簡單的觀察者程式碼:
var Event = function (sender) {
this._sender = sender;
this._listeners = [];
}
Event.prototype = {
attach: function (listener) {
this._listeners.push(listener);
},
notify: function (args) {
for (var i = 0; i < this._listeners.length; i += 1) {
this._listeners[i](this._sender, args);
}
}
};
複製程式碼
然後把應用稍作修改,在加上一個span,其中為第一個span的值乘以2。介面看起來是這樣:
1|2
<div id="app">
<p><span id="count">1</span>|<span id="count2">2</span>
<button id="inc">+</button>
<button id="dec">-</button>
</p>
</div>
<script>
class Model{
constructor(e){
this.e = e
this.count = 1
}
inc(){
this.count++
this.e.notify(this.count)
return this.count
}
dec(){
this.count--
this.e.notify(this.count)
return this.count
}
}
class View{
constructor(e){
this.e = e
this.e.attach(this.f.bind(this))
this.counter = document.getElementById('count')
this.counter2 = document.getElementById('count2')
this.btn1 = document.getElementById('inc')
this.btn2 = document.getElementById('dec')
}
f(sender,c){
this.counter2.innerHTML = c * 2
}
setCount(c){
this.counter.innerHTML = c
}
attachInc(cb){
this.btn1.addEventListener('click',cb)
}
attachDec(cb){
this.btn2.addEventListener('click',cb)
}
}
var Event = function (sender) {
this._sender = sender;
this._listeners = [];
}
Event.prototype = {
attach: function (listener) {
this._listeners.push(listener);
},
notify: function (args) {
for (var i = 0; i < this._listeners.length; i += 1) {
this._listeners[i](this._sender, args);
}
}
};
class Controller {
constructor(){
this.e = new Event()
this.m = new Model(this.e)
this.v = new View(this.e)
this.v.attachInc(this.onInc.bind(this))
this.v.attachDec(this.onDec.bind(this))
}
onInc(){
this.v.setCount(this.m.inc())
}
onDec(){
this.v.setCount(this.m.dec())
}
}
var c = new Controller()
</script>
複製程式碼
此應用的最後一個實現,看起來更加具備了一個MVC的多個方面:
- 類職責分類為模型檢視控制器
- 有了事件的釋出和訂閱的機制,可以更好的釋出一個模型的變化到多個檢視去
- Model並不依賴於View,而是通過事件釋出訂閱的方式通知檢視變化