簡單談談js中的MVC
MVC是什麽?
MVC是一種架構模式,它將應用抽象為3個部分:模型(數據)、視圖、控制器(分發器)。
本文將用一個經典的例子todoList來展開(代碼在最後)。
一個事件發生的過程(通信單向流動):
1、用戶在視圖 V 上與應用程序交互
2、控制器 C 觸發相應的事件,要求模型 M 改變狀態(讀寫數據)
3、模型 M 將數據發送到視圖 V ,更新數據,展現給用戶
在js的傳統開發模式中,大多基於事件驅動的:
1、hash驅動
2、DOM事件,用來驅動視圖
3、模型事件(業務模型事件和數據模型事件),用來驅動模型和模型結合
所以js中的mvc的特點是:單向流動、事件驅動
一)模型
模型存放
數據是面向對象的,當控制器請求模型讀寫數據時,模型就將數據包裝成模型實例。任何定義在這個數據模型上的函數或邏輯都可以直接被調用。在本文的例子中采用localSrorage也是類似道理的。存儲的Todos可以隨時被調用
模型不關心,不包含視圖和控制器的邏輯。它們應該是互相解耦的。這裏提一點,模型與視圖的耦合,顯然是違反MVC架構原則,但往往我們有時候卻因為業務關系而無法完全解耦
模型表現了領域特定的數據,當一個模型有所改變的時候,它會通知它的觀察者(視圖)。
二)視圖
視圖是呈現給用戶的,是用戶交互的第一入口。它定義配置、管理著每個頁面相應的模板與組件,它表現為一個模型的當前狀態,視圖通過觀察者模式監視模型,以獲得最新的數據,來呈現最新的頁面。所以,頁面首次加載時,往往是從接收模型的數據開始。
三)控制器
控制器(分發器),是模型和視圖之間的橋梁,集中式地配置和管理事件分發、模型分發、視圖分發,還用來權限控制、異常處理等。我們的應用中往往是有多個控制器的
頁面加載完成後,控制器會監聽視圖的用戶交互(按鈕點擊或表單提交),一旦用戶發生交互時,控制器做出對視圖的選擇,觸發控制器的事件處理機制,去派發新的事件,通知模型更新數據(這樣就回到了第一步了)
Demo-todoList
最後這裏是一個用原生js寫的todoLIst,這個demo做的很簡陋,點擊輸入文字點擊確定就添加,刪除是直接點擊該行信息。
單獨分離開來舉例子不好講,所以在代碼中進行註釋。首先簡單理下下邊代碼的思路:
1、V層定義配置了一個顯示數據的字符串模板,同時定義一個訂閱者的回調函數render() 用於頁面更新數據。
2、C層監聽用戶的添加與刪除操作,添加是add() 函數 它執行了回調函數render,同時向M層寫入數據,通知M層改變。刪除操作同理。
3、M層是本地存儲localStorage,模擬一個存儲數據對象的後臺模型。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>todo</title> 6 </head> 7 <body> 8 <header> 9 <h3>待定事項</h3> 10 </header> 11 <main> 12 <ul id="todoList"></ul> 13 <input type="text" id="content"> 14 <button id="confirm">確認</button> 15 </main> 16 17 <script> 18 (function () { 19 const ADD_KEY = ‘__todoList__‘ 20 21 const Utils = { 22 // 模擬 Modal(實體模型) 23 store(key, data) { 24 if (arguments.length > 1) { 25 return localStorage.setItem(key, JSON.stringify(data)); 26 } else { 27 let storeData = localStorage.getItem(key); 28 return (storeData && JSON.parse(storeData)) || []; // 這裏一定要設置初始值為 [] 29 } 30 } 31 } 32 33 class Todo { 34 constructor(id, text = "") { 35 this.id = id 36 this.text = text 37 } 38 } 39 40 let App = { 41 init() { 42 // this.todos 為一個存儲json對象的數組, 是一個實例化的數據對象,可任意調用 43 this.todos = Utils.store(ADD_KEY) 44 this.findDom() 45 this.bindEvent() 46 this.render() // 初始化渲染 47 }, 48 49 50 findDom() { 51 this.contentBox = document.querySelector("#content") 52 this.confirm = document.querySelector("#confirm") 53 this.todoList = document.querySelector("#todoList") 54 this.todoListItem = document.getElementsByTagName("li") 55 }, 56 57 // 模擬 Controller (業務邏輯層) 58 bindEvent() { 59 this.confirm.addEventListener(‘click‘, () => { 60 // 要求模型 M 改變狀態,add()函數是寫入數據操作 61 this.add() 62 }, false) 63 64 this.todoList.addEventListener(‘click‘, (item) => { // 事件委托,優化性能 65 this.remove(item) 66 }, false) 67 }, 68 69 // 這裏勉強抽象成一個視圖吧!!! 70 view() { 71 let fragment = document.createDocumentFragment() // 減少回流次數 72 fragment = ‘‘ 73 74 for (let i = 0; i < this.todos.length; i++) { // 一次性DOM節點生成 75 // 這裏使用拼接字符串代替視圖的模板, 76 // *******註意模板並不是一個視圖,模板是由視圖定義配置出來的,並被其管理著******* 77 // 模板是用一種聲明的方式指定部分甚至所有的視圖對象 78 fragment += `<li>${this.todos[i].text}</li>` 79 } 80 this.todoList.innerHTML = fragment 81 }, 82 83 // render()函數作為一個訂閱者的回調函數,數據的變化會反饋到模型 store 84 // 換句話說:視圖通過觀察者模式,觀察模型 store,當模型發生改變,觸發視圖更新 85 render() { 86 this.view() 87 88 /** 89 * 這裏需要特別提一下,按照 MVC 原則這裏本不應該出現下面的代碼的 90 * 因為業務邏輯關系(我本地存儲使用的是同一個key值,再次寫入數據會覆蓋原來的數據,), 91 * 所以必須通知模型 M 保存數據, V 層處理了不該它處理的邏輯,導致 M 與 V 耦合 92 * 93 * 解決辦法是:將其抽象出來編寫一個 視圖助手 helper 94 */ 95 Utils.store(ADD_KEY, this.todos) 96 }, 97 98 getItemIndex(item) { 99 let itemIndex 100 if (item.target.tagName.toLowerCase() === ‘li‘) { 101 let arr = Array.prototype.slice.call(this.todoListItem) 102 let index = arr.indexOf(item.target) 103 return itemIndex = index 104 } 105 }, 106 107 add(e) { 108 let id = Number(new Date()) 109 let text = this.contentBox.value 110 let addTodo = new Todo(id, text) 111 this.todos.unshift(addTodo) // 模型發生改變 112 this.render() // 當模型發生改變,觸發視圖更新 113 }, 114 115 remove(item) { 116 let index = this.getItemIndex(item) 117 this.todos.splice(index, 1) 118 this.render() 119 } 120 } 121 122 App.init() 123 })() 124 </script> 125 </body> 126 </html>
隨著界面和邏輯的復雜,用js或者jq去控制DOM是不現實的。上邊例子只是用原生js模擬mvc的思想實現過程。真正地項目往往會依賴一些封裝好的優秀庫進行高效開發。
mvc模式的優點
mvc編程把所有精力放在數據處理,盡可能減少對網頁元素的處理。對於有一定數量功能的網頁,Mvc模式下強制規範代碼,簡化,減少重復代碼,使代碼易於擴充。
mvc模式的弊端
1、清晰的構架以代碼的復雜性為代價, 對小項目反而降低開發效率。 (如果本文的例子todoList用面條式代碼編寫,那得多簡單啊!!!)
2、控制層和視圖層耦合,導致沒有真正分離和重用
3、在同一業務邏輯下,如果存在多種視圖呈現,需要視圖定義配置多個模板引擎、數據解析,多次處理數據與頁面更新。代碼就充滿了各種選擇器與事件回調,隨著業務的膨脹,變得難以維護。
總結:其實,現在MVC在前端用得比較少了,因為它的局限性,催生了MVVM模式的流行與廣泛使用,在下篇文章我會談談我對MVVM的理解,以及為何我使用基於MVVM模式的vue框架來高效開發。
簡單談談js中的MVC