MVC的故事
现在的前端学习者,会有一个断层,知识的断层,为什么vue, react 会出现,好像他一开始就出现在哪里一样
所谓的老前端,眼见这些年前端怎么发展,怎么逐步完善,怎么逐步变成今天所谓的框架
然而,在一定程度上,两者表现上看起来没有什么区别,对一些前端新人来说,他们更年轻更有精力,用相对成熟的vue,react拧着螺丝的经历甚至比老前端更为丰富。
当然,这只是表象,当脱离了所谓的框架,新人还会写出具有mvc思想的代码吗?
前端MVC的开始
2010 年,Backbone.js 第一版发布,它是基于MVX思想的(X可以是任何东西),那时候用Backbone的人,用意大利面条来形容没有组织的代码,因为这些代码长长短短,还互相交织,你中有我,我中有你。
然而似乎在三年后国内,才开始使用这个东西。可见英文世界的前端知识,一直都是领先于中文世界的。
当然现在Backbone已经没人用了。 大家从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__》,
数量:<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来了
一些高端的程序员发现,意大利面条代码总是可以分为三类:
- 专门操作远程数据的代码(fetchDb 和 saveDb 等等)
- 专门呈现页面元素的代码(innerHTML 等等)
- 其他控制逻辑的代码(点击某按钮之后做啥的代码)
为什么分成这三类呢?因为我们前端抄袭了后端的分类思想,后端代码也经常分为三类:
- 专门操作 MySQL 数据库的代码
- 专门渲染 HTML 的代码
- 其他控制逻辑的代码(用户请求首页之后去读数据库,然后渲染 HTML 作为响应等等)
这些思路经过慢慢的演化,最终被广大程序员完善为 MVC 思想。
- M 专门负责数据
- V 专门负责表现
- C 负责其他逻辑
如果我们来反思一下,会发现这个分类是无懈可击的:
- 每个网页都有数据
- 每个网页都有表现(具体为 HTML)
- 每个网页都有其他逻辑
于是乎,MVC 成了经久不衰的设计模式(设计模式就是「套路」的意思)
让我们看看以上代码经过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__》,
数量:__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
它改进了以下几点:
- 把意大利面条变成三块有结构有组织的对象:model、view 和 controller
- model 只负责存储数据、请求数据、更新数据
- view 只负责渲染 HTML(可接受一个 data 来定制数据)
- controller 负责调度 model 和 view
看起来代码变多了,但是我们似乎写了很多公共方法,可以用类封装起来,成为所谓的模版代码(俗称面向对象)
let model = new Model()
let controller = new Controller()
let view = new View()
等等! 好像发现了新姿势, 好像读音都一样的
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>`
})
双向绑定
其实仔细看看,以上改进的mvc代码有一个很严重的BUG,每次 render 都会更新 #app 的 innerHTML,这可能会丢失用户的写在页面某个 input 里面的数据。
这有两个解决办法:
- 用户只要输入了什么,就记录在 JS 的 data 里(数据绑定的初步思想出现了)
- 不要粗暴的操作 innerHTML,而是只更新需要更新的部位(虚拟 DOM 的初步思想出现了)
后来的后来,vue0.8出现了,他就是简化版的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 就会更新
看起来很魔法,其实还是思想,其实我们自己也可以轻松实现个双向绑定。不就是检测到数据变动后,自动render了一次,对吧。发布订阅者模式了解一下
Vue 还有很多其他功能,使得 Controller 显得多余:
created(){ },
methods: {
add() {}
}
从此以后,事情变得容易了很多。 vue已经magic一般的帮我们实现了双向绑定,你也了解了双绑的原理,用起来游刃有余。人们称之为MVVM
单向绑定
又有一批程序员,他们发现「双向绑定」有一点点反「组件化」,在跨组件的双向绑定变得很奇怪。于是,他们发明了单向绑定,在跨组件时使用,保证了数据的流向,让数据变得可控,组件耦合度降低。
但是不得不提,局部使用双向绑定是非常爽的。这就是为什么我们在用react的时候,input还是喜欢让他双绑起来更舒服一点。
双绑怎么玩
顺带提一提双向绑定,其实也走过了好几代。
- dirty check,也就是将触发数据变化的onChange改写了,在改写的onChange中render(Angular 1.X)
- getter setter, 利用ECMAScript 2015中提出的getter setter,可以轻松实现在get和set Value的时候顺便render一下。 但有一个Bug,你无法监听不存在的key (Vue)
- proxy, 利用ECMAScript 2017中新增的proxy,可以为对象定义基本操作的自定义行为,也可以完美解决上一代setter的bug,下一代Vue就要用这个重写了哦