MVC的故事
現(xiàn)在的前端學(xué)習(xí)者,會有一個斷層酪穿,知識的斷層凳干,為什么vue, react 會出現(xiàn),好像他一開始就出現(xiàn)在哪里一樣
所謂的老前端被济,眼見這些年前端怎么發(fā)展救赐,怎么逐步完善,怎么逐步變成今天所謂的框架
然而只磷,在一定程度上经磅,兩者表現(xiàn)上看起來沒有什么區(qū)別泌绣,對一些前端新人來說,他們更年輕更有精力预厌,用相對成熟的vue阿迈,react擰著螺絲的經(jīng)歷甚至比老前端更為豐富。
當(dāng)然配乓,這只是表象仿滔,當(dāng)脫離了所謂的框架,新人還會寫出具有mvc思想的代碼嗎犹芹?
前端MVC的開始
2010 年崎页,Backbone.js 第一版發(fā)布,它是基于MVX思想的(X可以是任何東西)腰埂,那時候用Backbone的人飒焦,用意大利面條來形容沒有組織的代碼,因為這些代碼長長短短屿笼,還互相交織牺荠,你中有我,我中有你驴一。
然而似乎在三年后國內(nèi)休雌,才開始使用這個東西「味希可見英文世界的前端知識杈曲,一直都是領(lǐng)先于中文世界的。
當(dāng)然現(xiàn)在Backbone已經(jīng)沒人用了胸懈。 大家從backbone到了ng到了vue.js 0.8
意大利面條代碼: 如果不從第一行看到最后一行担扑,就不知道干什么的
function fetchDb() {
return axios.get('/books/1')
}
function saveDb(newData) {
return axios.put('/books/1', newData)
}
var template = `
<div>
書名:《__name__》,
數(shù)量:<span id="number">__number__</span>
</div>
<div class="actions">
<button id="increaseByOne">加1</button>
<button id="decreaseByOne">減1</button>
</div>
`
fetchDb().then((response) => {
let result = response.data
$('#app').html(
template.replace('__number__', result.number)
.replace('__name__', result.name)
)
//加1
$('#increaseByOne').on('click', (e) => {
let oldResult = parseInt($('#number').text(), 10)
let newResult = oldResult + 1
saveDb({number: newResult}).then(function({data}) {
console.log(data)
$('#number').text(data.number)
})
})
//減1
$('#decreaseByOne').on('click', (e) => {
let oldResult = parseInt($('#number').text(), 10)
let newResult = oldResult - 1
saveDb({number: newResult}).then(({data}) => {
$('#number').text(data.number)
})
})
如果一段代碼趣钱,你從頭到尾一行行看的很流暢官扣,額那這基本就是面條代碼了
MVC來了
一些高端的程序員發(fā)現(xiàn)骤视,意大利面條代碼總是可以分為三類:
- 專門操作遠程數(shù)據(jù)的代碼(fetchDb 和 saveDb 等等)
- 專門呈現(xiàn)頁面元素的代碼(innerHTML 等等)
- 其他控制邏輯的代碼(點擊某按鈕之后做啥的代碼)
為什么分成這三類呢?因為我們前端抄襲了后端的分類思想妨马,后端代碼也經(jīng)常分為三類:
- 專門操作 MySQL 數(shù)據(jù)庫的代碼
- 專門渲染 HTML 的代碼
- 其他控制邏輯的代碼(用戶請求首頁之后去讀數(shù)據(jù)庫棒旗,然后渲染 HTML 作為響應(yīng)等等)
這些思路經(jīng)過慢慢的演化场航,最終被廣大程序員完善為 MVC 思想唱蒸。
- M 專門負(fù)責(zé)數(shù)據(jù)
- V 專門負(fù)責(zé)表現(xiàn)
- C 負(fù)責(zé)其他邏輯
如果我們來反思一下敷矫,會發(fā)現(xiàn)這個分類是無懈可擊的:
- 每個網(wǎng)頁都有數(shù)據(jù)
- 每個網(wǎng)頁都有表現(xiàn)(具體為 HTML)
- 每個網(wǎng)頁都有其他邏輯
于是乎,MVC 成了經(jīng)久不衰的設(shè)計模式(設(shè)計模式就是「套路」的意思)
讓我們看看以上代碼經(jīng)過MVC洗滌后變成什么樣子:
let model = {
data: {
number: 0,
name: ''
},
fetch(id) {
return axios.get(`/books/${id}`).then((response)=>{
this.data = response.data
})
},
update(newData) {
let id = this.data.id
return axios.put(`/books/${id}`, newData).then(({data})=>{
this.data = data
})
}
}
let view = {
el: '#app',
template: `
<div>
書名:《__name__》低矮,
數(shù)量:__number__
</div>
<div class="actions">
<button id="increaseByOne">加1</button>
</div>
`,
render(data) {
let html = this.template.replace('__name__', data.name)
.replace('__number__', data.number)
console.log(data)
$(this.el).html(html)
}
}
let controller = {
init({ view, model}) {
this.view = view
this.model = model
this.view.render(this.model.data)
this.bindEvents()
console.log(1)
this.fetchModel()
console.log(2)
},
events: [
{ type: 'click', selector: '#increaseByOne', fnName: 'add' },
{ type: 'click', selector: '#decreaseByOne', fnName: 'minus' },
{ type: 'click', selector: '#square', fnName: 'square' },
{ type: 'click', selector: '#cube', fnName: 'cube' },
],
bindEvents() {
this.events.map((event)=>{
$(this.view.el).on(event.type, event.selector, this[event.fnName].bind(this))
})
},
add(){
let newData = {number: this.model.data.number + 1}
this.updateModel(newData)
},
fetchModel(){
this.model.fetch(1).then(() => {
this.view.render(this.model.data)
})
},
updateModel(newData){
this.model.update(newData).then(()=>{
this.view.render(this.model.data)
})
}
}
是不是看命名就很MVC
它改進了以下幾點:
- 把意大利面條變成三塊有結(jié)構(gòu)有組織的對象:model、view 和 controller
- model 只負(fù)責(zé)存儲數(shù)據(jù)被冒、請求數(shù)據(jù)军掂、更新數(shù)據(jù)
- view 只負(fù)責(zé)渲染 HTML(可接受一個 data 來定制數(shù)據(jù))
- controller 負(fù)責(zé)調(diào)度 model 和 view
看起來代碼變多了轮蜕,但是我們似乎寫了很多公共方法,可以用類封裝起來蝗锥,成為所謂的模版代碼(俗稱面向?qū)ο螅?/h5>
let model = new Model()
let controller = new Controller()
let view = new View()
let model = new Model()
let controller = new Controller()
let view = new View()
等等跃洛! 好像發(fā)現(xiàn)了新姿勢, 好像讀音都一樣的
let view = new View() === let view = new Vue()
沒有錯,其實vue就是這么來的终议。
var view = new Vue({
el: '#app',
data: {
name: 'vue',
},
template: `<div id='app'>hello {{data.name}}</div>`
})
雙向綁定
其實仔細(xì)看看汇竭,以上改進的mvc代碼有一個很嚴(yán)重的BUG,每次 render 都會更新 #app 的 innerHTML穴张,這可能會丟失用戶的寫在頁面某個 input 里面的數(shù)據(jù)细燎。
這有兩個解決辦法:
- 用戶只要輸入了什么,就記錄在 JS 的 data 里(數(shù)據(jù)綁定的初步思想出現(xiàn)了)
- 不要粗暴的操作 innerHTML皂甘,而是只更新需要更新的部位(虛擬 DOM 的初步思想出現(xiàn)了)
后來的后來玻驻,vue0.8出現(xiàn)了,他就是簡化版的Angular偿枕,他借鑒了Angular的雙向綁定思想璧瞬,真的就把new View變成了new Vue
Vue 的雙向綁定(也是 Angular 的雙向綁定)有這些功能:
- 只要 JS 改變了 view.number 或 view.name 或 view.n (注意 Vue 把 data 里面的 number、name 和 n 放到了 view 上面渐夸,沒有 view.data 這個東西)嗤锉, HTML 就會局部更新
- 只要用戶在 input 里輸入了值,JS 里的 view.n 就會更新
看起來很魔法墓塌,其實還是思想瘟忱,其實我們自己也可以輕松實現(xiàn)個雙向綁定。不就是檢測到數(shù)據(jù)變動后桃纯,自動render了一次酷誓,對吧。發(fā)布訂閱者模式了解一下
Vue 還有很多其他功能态坦,使得 Controller 顯得多余:
created(){ },
methods: {
add() {}
}
從此以后盐数,事情變得容易了很多。 vue已經(jīng)magic一般的幫我們實現(xiàn)了雙向綁定伞梯,你也了解了雙綁的原理玫氢,用起來游刃有余。人們稱之為MVVM
單向綁定
又有一批程序員谜诫,他們發(fā)現(xiàn)「雙向綁定」有一點點反「組件化」漾峡,在跨組件的雙向綁定變得很奇怪。于是喻旷,他們發(fā)明了單向綁定生逸,在跨組件時使用,保證了數(shù)據(jù)的流向,讓數(shù)據(jù)變得可控槽袄,組件耦合度降低烙无。
但是不得不提,局部使用雙向綁定是非常爽的遍尺。這就是為什么我們在用react的時候截酷,input還是喜歡讓他雙綁起來更舒服一點。
雙綁怎么玩
順帶提一提雙向綁定乾戏,其實也走過了好幾代迂苛。
- dirty check,也就是將觸發(fā)數(shù)據(jù)變化的onChange改寫了鼓择,在改寫的onChange中render(Angular 1.X)
- getter setter, 利用ECMAScript 2015中提出的getter setter三幻,可以輕松實現(xiàn)在get和set Value的時候順便render一下。 但有一個Bug惯退,你無法監(jiān)聽不存在的key (Vue)
- proxy赌髓, 利用ECMAScript 2017中新增的proxy,可以為對象定義基本操作的自定義行為催跪,也可以完美解決上一代setter的bug锁蠕,下一代Vue就要用這個重寫了哦