本文寫于 2020 年 7 月 27 日 首先有個(gè)問題:Vue 是 MVC 還是 MVVM 框架? 維基百科告訴我們:MVVM 是 PM 的變種,而 PM 又是 MVC 的變種。 所以一定程度上來說,不管 Vue 是 MVC 還是 MVVM 或者都不是,它的思想方向與這些設(shè)計(jì)模式的方向是大體相同的。 并且 Vue 的官網(wǎng)中也說道:“雖然沒有完全遵循 MVVM 模型,但是 Vue 的設(shè)計(jì)也受到了它的啟發(fā)?!?/p> 這個(gè)問題網(wǎng)上吵得比較多,本文并不是來討論這個(gè)問題的,而是面是向初學(xué)者淺淺的分析一下老大哥 MVC 的思想在 Vue 中的體現(xiàn)。 0 新手的困惑大學(xué)時(shí)候?qū)I(yè)里前后開了幾門網(wǎng)頁課,先是教授 HTML、CCC;后來一門課教了 JS;最后有一門教授 Vue 的課。 由于我大學(xué)讀的并不是計(jì)算機(jī)專業(yè),而是藝術(shù)類的數(shù)字媒體藝術(shù)專業(yè)。所以大家對(duì)于編程的熱情度幾乎是負(fù)的。 上學(xué)期的 JS 都沒學(xué)好,一聽說要學(xué) Vue,大家的內(nèi)心自然是崩潰的。課程上來就是一段代碼: let app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }); 大家一開始的心聲就是這樣的:什么?!這是什么?誰看得懂! 并且不光是初學(xué)者,一些寫了一段時(shí)間 Vue 的人,懂得 el 是是什么、data 是什么,但可能也不清楚為什么 Vue 要這么來組織代碼——除非他學(xué)過 MVC。 1 一個(gè) MVC 計(jì)數(shù)器一個(gè) MVC 模塊是三個(gè)對(duì)象的合體:M, V, C。
嚴(yán)格來說……MCV 沒有嚴(yán)格來說,MVC 的定義并不明確,所以我以為 MVC 其實(shí)是一種思想方向,代表著視圖和業(yè)務(wù)邏輯互不干擾。 還是那句話,放碼過來。我們先實(shí)現(xiàn)一個(gè)非常常見的例子:加按鈕與減按鈕。 普通版本 JS 計(jì)數(shù)器<div id="app"> <span>0</span> <button id="add">+</button> <button id="minus">-</button> </div> 我們希望的結(jié)果是,當(dāng)我們點(diǎn)擊 + 號(hào)時(shí), 我相信這種 JS 代碼應(yīng)該是信手拈來的對(duì)吧。 const numberWrapper = document.querySelector('#app span'); const addBtn = document.querySelector('#add'); const minusBtn = document.querySelector('#minus'); addBtn.addEventListener('click', () => { const newNumber = parseInt(numberWrapper.innerText) + 1; numberWrapper.innerText = newNumber.toString(); }); minusBtn.addEventListener('click', () => { const newNumber = parseInt(numberWrapper.innerText) - 1; numberWrapper.innerText = newNumber.toString(); }); 但這只是普通版,接下來讓我們用 MVC 的方式來一步步的重構(gòu)這個(gè)代碼。 MVC 版本 JS 計(jì)數(shù)器首先我們想,這樣寫的一個(gè)計(jì)數(shù)器,如果需要修改,那我一方面要改 HTML 文件、一方面還要修改 JS 文件,何其麻煩! 寫到一起來吧: const app = document.querySelector('#app'); const html = ` <span>0</span> <button id="add">+</button> <button id="minus">-</button> `; const counter = document.createElement('div'); counter.innerHTML = html; app.appendChild(counter); 那么我們來梳理一下現(xiàn)在的代碼:
那我們可以大膽的猜測(cè)一下嘛,如何使用 MVC 思想呢? 首先新建一個(gè)對(duì)象叫做 view 吧,再將我們的 html 代碼放進(jìn)去: const view = { html: ` <span>0</span> <button id="add">+</button> <button id="minus">-</button> ` }; 還有我們用來新建 div、將 html 代碼放入 div、再將 div 放進(jìn) app 的操作,應(yīng)該也是屬于視圖層。 所以我們給 view 對(duì)象添加一個(gè) render 方法: const view = { // ...html... render() { const counter = document.createElement('div'); counter.innerHTML = view.html; app.appendChild(counter); } }; view.render(); 這樣我們就搞定了 V,然后看看 C。除了視圖和數(shù)據(jù),其他的東西應(yīng)該都屬于 C,所以 DOM 元素的獲取放在 C 里、事件綁定也放在 C 里。 const controller = { ui: {}, bindEvents() {} }; 這里我們準(zhǔn)備將 DOM 元素放在 ui 對(duì)象里,但是這里需要腦子轉(zhuǎn)一下。 一旦我們?cè)谶@里寫了 所以我們得在里面寫一個(gè) init 函數(shù),這樣我們執(zhí)行初始化之后,他就會(huì)先去獲取 DOM、再去綁定事件: init() { this.ui = { numberWrapper: document.querySelector('#app span'), addBtn: document.querySelector('#add'), minusBtn: document.querySelector('#minus') }; controller.bindEvents(); }, 綁定事件的寫法就非常簡(jiǎn)單了: bindEvents() { controller.ui.addBtn.addEventListener('click', () => { const newNumber = parseInt(controller.ui.numberWrapper.innerText) + 1; controller.ui.numberWrapper.innerText = newNumber.toString(); }); controller.ui.minusBtn.addEventListener('click', () => { const newNumber = parseInt(controller.ui.numberWrapper.innerText) - 1; controller.ui.numberWrapper.innerText = newNumber.toString(); }); } 接下來就是一個(gè)轉(zhuǎn)折點(diǎn)了,我們要?jiǎng)?chuàng)建一個(gè) model 對(duì)象來保存數(shù)據(jù)。 const model = { data: { number: 100 } }; 這個(gè)時(shí)候不知道大家有沒有領(lǐng)悟到一些東西。 既然已經(jīng)有了 model,我們何必還去操作 DOM 獲取數(shù)據(jù)呢? 直接操作 model 多優(yōu)雅呀! 所以 bindEvents 可以改成這樣: controller.ui.addBtn.addEventListener('click', () => { model.data.number += 1; }); controller.ui.minusBtn.addEventListener('click', () => { model.data.number -= 1; }); 那我們的 view 對(duì)象也需要修改,他也應(yīng)該從 model 中獲取數(shù)據(jù): const view = { html: ` <span>{{number}}</span> ...... `, render() { const counter = document.createElement('div'); counter.innerHTML = view.html.replace('{{number}}', model.data.number); app.appendChild(counter); } }; 但是我們這樣操作雖然說修改了數(shù)據(jù),可是并沒有重新渲染到頁面上呀。所以每次提交之后需要重新 render。 此時(shí)問題出現(xiàn)了:點(diǎn)擊 + 或者 - 后,數(shù)字只會(huì)變化一次,第二次點(diǎn)擊便毫無用處! 這是為什么呢? 很簡(jiǎn)單,因?yàn)槲覀冎匦?render,導(dǎo)致倆綁定了事件的 button 全都不是曾經(jīng)的那個(gè)他了。 所以我們使用事件代理來解決這個(gè)問題——將事件綁定在外層的 div 上,然后判斷點(diǎn)擊對(duì)象的 id 即可。 寫法如下: const compute = e => { switch (e.target.id) { case 'add': model.data.number += 1; break; case 'minus': model.data.number -= 1; break; default: return; } view.render(); }; 接下來我們會(huì)在 view 對(duì)象中添加一個(gè) el 屬性,用來存儲(chǔ)我們創(chuàng)建的外層 div。 const view = { el: null, // ...... render() { if (!view.el) { // 創(chuàng)建 div,并將 div 賦值給 el } else { // 將 el 的 innerHTML 更換為新的內(nèi)容 } } }; 最后我們?cè)龠M(jìn)行一步優(yōu)化。 我們本身不應(yīng)該知道在 render 時(shí),應(yīng)該 append 給哪一個(gè)元素。這個(gè)元素應(yīng)該是別人傳給我的,所以應(yīng)該這么寫: 總代碼: MVC 之 V const view = { el: null, html: ` <span>{{n}}</span> <button id="add">+</button> <button id="minus">-</button> `, render(container) { if (!view.el) { const counter = document.createElement('div'); view.el = counter; counter.innerHTML = view.html.replace( '{{n}}', model.data.number.toString() ); container.appendChild(counter); } else { view.el.innerHTML = view.html.replace( '{{n}}', model.data.number.toString() ); } } }; MVC 之 M const model = { data: { number: parseInt(window.localStorage.getItem('number')) || 0 }, save() { window.localStorage.setItem('number', model.data.number.toString()); } }; MVC 之 C const controller = { init(container) { controller.ui = { container }; view.render(container); controller.bindEvents(); }, bindEvents() { controller.ui.container.addEventListener('click', e => { switch (e.target.id) { case 'add': model.data.number += 1; break; case 'minus': model.data.number -= 1; break; default: return; } model.save(); view.render(); }); } }; 使用方式: const app = document.querySelector('#app'); controller.init(app); 這個(gè)時(shí)候我們的程序已經(jīng)是一個(gè)比較完整的 MVC 模式了,但直接全部 render 非常浪費(fèi)性能。 所以 React 之類的框架會(huì)使用虛擬 DOM 和 diff 算法來只修改變化的 DOM。 總的來說,我們的 MVC 思想可以抽想成為一個(gè)公式: 使用 class 來優(yōu)化代碼 class 優(yōu)化代碼可以提升我們的代碼復(fù)用程度,銘記:程序員永遠(yuǎn)不要重復(fù)自己的操作。 先看看 Model: class Model { constructor(options) { for (let key in options) { this[key] = options[key]; } } save() { console.error('還未傳入save函數(shù)'); } } export default Model; 這個(gè)非常簡(jiǎn)單,我們想要傳入任何的東西,都在這個(gè) option 里面,就像這樣: const model = new Model({ data: {}, save() {} }); 回想一下,我們使用 Vue 的時(shí)候,是不是也是如此? export default new Vue({ data() { return { msg: 'hello world' }; }, methods: {} }); 我沒讀過 Vue 的源碼,不知道 Vue 是否是按照本文的思路構(gòu)建代碼的。 但是 Vue、React 等框架追根溯源都能找到 MVC 的身上。所以毫無疑問,MVC 的思想是每一個(gè)程序員都需要學(xué)習(xí)的一種設(shè)計(jì)模式。 初學(xué)程序,用了幾個(gè)好用的框架與工具,不應(yīng)該只沉迷于其方便的一面,要善于從工具的運(yùn)用中尋找出其作者留下的蛛絲馬跡,反推學(xué)習(xí)、多查資料,才能夠慢慢進(jìn)化成為不懼怕新技術(shù)、框架越來越多的大神程序員! 工具也許會(huì)一個(gè)月一變、一天一變,但是思維是永恒的。 (完) |
|