JS进阶 - MVC

MVC与双向绑定与单向绑定

起源

说到前端框架,就总会谈论到什么「双向绑定」和「单向绑定」这些概念了。

但是要理解这些概览,最好还是从其最原始的形态入手,也就是自己搞出双向绑定和单向绑定。

这就要说到MVC 了。

2010年,Backbone#第一版发布,三年后在腾讯工作的我才开始用这么个东西。由此可以看出英文世界的前端知识一直都是领先于其他语言 的。

有人说Backbonejs是基于MVC思想的,也有人说Backbonejs是基于MVP思想的,我不打算给大家明确的答案,因为不管是MVC还是MV P,都是类似的。今天我们从需求的角度来理解MVX (X可以是任何东西)思路。

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.17.1/axios.min.js"></script>
<title>JS Bin</title>
</head>
<body>
<div id="app">

</div>

<div id="status"></div>

</body>
</html>

意大利面条式代码

使用框架的人总会说不使用框架的人写的是「意大利面条J

因为这些代码长长短短,还互相交织,你中有我,我中有你。

虽然乍看起来这种代码没有间题,但是时间久了之后,这种代码极难维护。

例子 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
axios.interceptors.response.use(function (response) {
let {config: {url, method, data}} = response
data = JSON.parse(data||'{}')
let row = {
id: 1, name: 'JavaScript高级程序设计', number: 2
}
if(url === '/books/1' && method === 'get'){
response.data = row
}else if(url === '/books/1' && method === 'put'){
response.data = Object.assign(row, data)
}
return response
})


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>
<button id="square">平方</button>
<button id="cube">立方</button>
<button id="reset">归零</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)
})
})

//平方
$('#square').on('click', (e) => {
let oldResult = parseInt($('#number').text(), 10)
let newResult = Math.pow(oldResult, 2)
saveDb({number: newResult}).then(({data}) => {
$('#number').text(data.number)
})
})

//立方
$('#cube').on('click', (e) => {
let oldResult = parseInt($('#number').text(), 10)
let newResult = Math.pow(oldResult, 3)
saveDb({number: newResult}).then(({data}) => {
$('#number').text(data.number)
})
})

//归零
$('#reset').on('click', (e) => {
let newResult = 0
saveDb({number: newResult}).then(({data}) => {
$('#number').text(data.number)
})
})
})

解决面条

一些程序员通过自己的总结,发现这些代码总是可以分成三类:

​ 1. 专门操作远程数据的代码(fetchDb和saveDb等等)

​ 2. 专门呈现页面元素的代码(innerHTML等等)

​ 3. 其他控制逻辑的代码(点击某按钮之后做啥的代码)

为什么分成这三类呢?因为我们前端抄袭了后端的分类思想,后端代码也经常分为三类:

​ 1. 专门操作MySQL数据库的代码

​ 2. 专门渲染HTML的代码

​ 3. 其他控制逻辑的代码(用户请求首页之后去读数据库,然后渲染HTML作为响应等等)

这些思路经过慢慢的演化,最终被广大程序员完善为MVC思想。

​ 1. M专门负责数据

​ 2. V专门负责表现

​ 3. C负责其他逻辑

如果我们来反思一下,会发现这个分类是无懈可击的:

​ 1. 每个网页都有数据

​ 2. 每个网页都有表现(具体为HTML)

​ 3. 每个网页都有其他逻辑

于是乎,MVC成了经久不衰的设计模式(设计模式就是「套路」的意思)

现在我们来改写一下例子1。

例子2: https://jsbin.com/yuwopuf/3/edit?js,output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
axios.interceptors.response.use(function(response) {
console.log(response)
let {
config: {
url, method, data
}
} = response
data = JSON.parse(data || '{}')
let row = {
id: 1,
name: 'JavaScript高级程序设计',
number: 2
}
if (url === '/books/1' && method === 'get') {
response.data = row
} else if (url === '/books/1' && method === 'put') {
response.data = Object.assign(row, data)
}
return response
})

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>
<button id="decreaseByOne">减1</button>
<button id="square">平方</button>
<button id="cube">立方</button>
<button id="reset">归零</button>
</div>
`,
render(data) {
let html = this.template.replace('__name__', data.name)
.replace('__number__', data.number)
console.log(data)
$(this.el).html(html)
}
}


var 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)
},
minus(){
let newData = {number: this.model.data.number - 1}
this.updateModel(newData)
},
square(){
let newData = {number: Math.pow(this.model.data.number, 2)}
this.updateModel(newData)
},
cube(){
let newData = {number: Math.pow(this.model.data.number, 3)}
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)
})
}
}


controller.init({view, model})

改进了以下几点:

​ 1. 把意大利面条变成三块有结构有组织的对象:model、view和controller

​ 2. model只负责存储数据、请求数据、更新数据

​ 3. view只负责渲染HTML (可接受一个data来定制数据)

​ 4. controller 负责调度 model 和 view

模板代码(也就是类)

_个页面或模块只需要model view controller三个对象 第二个页面就需要再来model2 view2 controlled三个对象 第三个页面就需要再来model3 view3 controlled三个对象

第N个页面就需要再来modeIN viewN controllerN三个对象

你每次写一个model都要写很类似的代码 你每次写一个view都要写很类似的代码 你每次写一个controller都要写很类似的代码

为什么不利用模板代码(俗称面向对象)把重复的代码写到一个类呢OS里面就是把「共有属性」放到原型里)

代码如下

例子3: https://jsbin.com/sodojac/5/edit?js,output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
axios.interceptors.response.use(function (response) {
let {config: {url, method, data}} = response
data = JSON.parse(data||'{}')
let row = {
id: 1, name: 'JavaScript高级程序设计', number: 2
}
if(url === '/books/1' && method === 'get'){
response.data = row
}else if(url === '/books/1' && method === 'put'){
response.data = Object.assign(row, data)
}
return response
})



class Model{
constructor(options) {
this.data = options.data || {}
this.resource = options.resource
this.baseUrl = options.baseUrl || '/'
}
fetch(id) {
return axios.get(this.baseUrl + this.resource + 's/' + id)
.then(({data})=>{ this.data = data })
}
create(data) {
return axios.post(this.baseUrl + this.resource + 's', data)
.then(({data})=>{ this.data = data })
}
destroy() {
let id = this.data.id
return axios.delete(this.baseUrl + this.resource + 's/' + id)
.then(()=>{ this.data = {} })
}
update(newData) {
let id = this.data.id
return axios.put(this.baseUrl + this.resource + 's/' + id, newData)
.then(({data})=>{
this.data = data;
})
}
}

var model = new Model({
resource: 'book',
data: {
id: null,
number: 0,
name: null
}
})

class View {
constructor({
el, template
}) {
this.el = el
this.$el = $(el)
this.template = template
}
render(data) {
let html = this.template
for (let key in data) {
let value = data[key]
html = html.replace(`__${key}__`, value)
}
this.$el.html(html)
}
}

var view = new View({
el: '#app',
template: `
<div>
书名:《__name__》,
数量:__number__
</div>
<div class="actions">
<button id="increaseByOne">加1</button>
<button id="decreaseByOne">减1</button>
<button id="square">平方</button>
<button id="cube">立方</button>
<button id="reset">归零</button>
</div>
`,
})

class Controller {
constructor({view, model, events, init, ...rest }) {
this.view = view
this.model = model
this.events = events
Object.assign(this, rest)
this.bindEvents()
this.view.render(this.model.data)
init.apply(this, arguments)
}
bindEvents() {
this.events.map((e) => {
this.view.$el.on(e.type, e.el, this[e.fn].bind(this))
})
}
}

var controller = new Controller({
view: view,
model: model,
events: [
{ type: 'click', el: '#increaseByOne', fn: 'add' },
{ type: 'click', el: '#decreaseByOne', fn: 'minus' },
{ type: 'click', el: '#square', fn: 'square' },
{ type: 'click', el: '#cube', fn: 'cube' },
{ type: 'click', el: '#reset', fn: 'reset' }
],
init(options) {

this.model.fetch(1)
.then(() => {
this.view.render(this.model.data)
})
},
add() {
let newData = {number: this.model.data.number + 1}
this.updateModel(newData)
},
minus() {
// 注意这里有 bug
this.model.data.number = this.model.data.number - 1
this.updateModel(this.model.data)
},
square() {
let newData = {number: Math.pow(this.model.data.number, 2)}
this.updateModel(newData)
},
cube() {
let newData = {number: Math.pow(this.model.data.number, 3)}
this.updateModel(newData)
},
reset() {
this.updateModel({number: 0})
},
updateModel(newData) {
this.model.update(newData).then(()=>{
this.view.render(this.model.data)
})
}
})

烦人的地方

一般来说,如果代码有重复或类似,就能优化(也可以不优化)。

例3的代码有这样的重复代码,我们_个_个来解决。

第一个烦人的地方:每次用model获取数据之后,还要「手动」调用this.view.render(this.model.data),你看代码中有四处手动调用了 update Model。

怎么解决呢? 一个方案是给Model加上事件机制。

优化后的代码如下

例子4: https://jsbin.com/sodojac/10/edit?js,output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
axios.interceptors.response.use(function (response) {
let {config: {url, method, data}} = response
data = JSON.parse(data||'{}')
let row = {
id: 1, name: 'JavaScript高级程序设计', number: 2
}
if(url === '/books/1' && method === 'get'){
response.data = row
}else if(url === '/books/1' && method === 'put'){
response.data = Object.assign(row, data)
}
return response
})

class EventHub {
constructor(){
this.events = {}
}
on(eventName, fn){
if(!this.events[eventName]){
this.events[eventName] = []
}
this.events[eventName].push(fn)
}
emit(eventName, params){
let fnList = this.events[eventName]
fnList.map((fn)=>{
fn.apply(null, params)
})
}
off(eventName, fn){
let fnList = this.events[eventName]
for(let i =0; i<fnList.length; i++){
if(fnList[i] === fn){
delete fnList[i]
break
}
}
}
}

class Model extends EventHub{
constructor(options) {
super()
this.data = options.data || {}
this.resource = options.resource
this.baseUrl = options.baseUrl || '/'
}
fetch(id) {
return axios.get(this.baseUrl + this.resource + 's/' + id)
.then(({data})=>{ this.data = data; this.emit('changed') })
}
create(data) {
return axios.post(this.baseUrl + this.resource + 's', data)
.then(({data})=>{ this.data = data; this.emit('changed') })
}
destroy() {
let id = this.data.id
return axios.delete(this.baseUrl + this.resource + 's/' + id)
.then(()=>{ this.data = {}; this.emit('changed') })
}
update(newData) {
let id = this.data.id
return axios.put(this.baseUrl + this.resource + 's/' + id, newData)
.then(({data})=>{
this.data = data;
this.emit('changed')
})
}
}

var model = new Model({
resource: 'book',
data: {
id: null,
number: 0,
name: null
}
})

class View {
constructor({
el, template
}) {
this.el = el
this.$el = $(el)
this.template = template
}
render(data) {
let html = this.template
for (let key in data) {
let value = data[key]
html = html.replace(`__${key}__`, value)
}
this.$el.html(html)
}
}

var view = new View({
el: '#app',
template: `
<div>
书名:《__name__》,
数量:__number__
</div>
<div class="actions">
<button id="increaseByOne">加1</button>
<button id="decreaseByOne">减1</button>
<button id="square">平方</button>
<button id="cube">立方</button>
<button id="reset">归零</button>
</div>
`,
})

class Controller {
constructor({
view, model, events, init, ...rest
}) {
this.view = view
this.model = model
this.events = events
Object.assign(this, rest)
this.bindEvents()
this.view.render(this.model.data)
init.apply(this, arguments)
}
bindEvents() {
this.events.map((e) => {
this.view.$el.on(e.type, e.el, this[e.fn].bind(this))
})
}
}

var controller = new Controller({
view: view,
model: model,
events: [
{ type: 'click', el: '#increaseByOne', fn: 'add' },
{ type: 'click', el: '#decreaseByOne', fn: 'minus' },
{ type: 'click', el: '#square', fn: 'square' },
{ type: 'click', el: '#cube', fn: 'cube' },
{ type: 'click', el: '#reset', fn: 'reset' }
],
init(options) {
this.model.on('changed', ()=>{
this.view.render(this.model.data)
})
this.model.fetch(1)
.then(() => {
this.view.render(this.model.data)
})
},
add() {
let newData = {number: this.model.data.number + 1}
this.updateModel(newData)
},
minus() {
// 注意这里有 bug
this.model.data.number = this.model.data.number - 1
this.updateModel(this.model.data)
},
square() {
let newData = {number: Math.pow(this.model.data.number, 2)}
this.updateModel(newData)
},
cube() {
let newData = {number: Math.pow(this.model.data.number, 3)}
this.updateModel(newData)
},
reset() {
this.updateModel({number: 0})
},
updateModel(newData) {
this.model.update(newData)
}
})


第二个烦人的地方有个BUG:

每次render都会更新#app的innerHTML,这可能会丢失用户的写在页面某个input里面的数据。(上课示例)

这有两个解决办法:

​ 2.1用户只要输入了什么,就记录在JS的data里。(数据绑定的初步思想出现了)

​ 2.2.不要粗暴的操作innerHTML,而是只更新需要更新的部位(虚拟DOM的初步思想出现了)

Angular就是基于第一个思想而发明的,而React则是基于第二个思想。

有些人还觉得有第三个烦人的地方:

events能不能直接写到HTML上面,而不是写到里。

确实这样写代码更直观,所以Angular和React都釆纳了这一想法。但是又一波人不喜欢这种写法,他们认为HTML和JS应该是分离的: 我们花了好几年才昔及内容与行为分离的观点,没想到Angular和React—下子就回到解放前了。

这种争论到2017年才渐渐休止,大家逐渐都接受了直接在HTML上绑定JS事件的写法。

接下来我们介绍Angular对于2.1间题的解法。

Angular与Vue的双向绑定

由于Angular太复杂(概念),很多人表示学不会,这时出现了 一个简化版的Angular Vue0.8。

当时的Vue主要借鉴了 Angular的双向绑定思想,所以我们用Vue来举例更好理解,这样我就不用花一个小时来向你介绍Angular 了。

例子5: https://jsbin.com/vixoku/4/edit?js,output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
axios.interceptors.response.use(function (response) {
let {config: {url, method, data}} = response
data = JSON.parse(data||'{}')
let row = {
id: 1, name: 'JavaScript高级程序设计', number: 2
}
if(url === '/books/1' && method === 'get'){
response.data = row
}else if(url === '/books/1' && method === 'put'){
response.data = Object.assign(row, data)
}
return response
})

class EventHub {
constructor(){
this.events = {}
}
on(eventName, fn){
if(!this.events[eventName]){
this.events[eventName] = []
}
this.events[eventName].push(fn)
}
emit(eventName, params){
let fnList = this.events[eventName]
fnList.map((fn)=>{
fn.apply(null, params)
})
}
off(eventName, fn){
let fnList = this.events[eventName]
for(let i =0; i<fnList.length; i++){
if(fnList[i] === fn){
delete fnList[i]
break
}
}
}
}

class Model extends EventHub{
constructor(options) {
super()
this.data = options.data || {}
this.resource = options.resource
this.baseUrl = options.baseUrl || '/'
}
fetch(id) {
return axios.get(this.baseUrl + this.resource + 's/' + id)
.then(({data})=>{
this.data = data
this.emit('changed')
})
}
create(data) {
return axios.post(this.baseUrl + this.resource + 's', data)
.then(({data})=>{
this.data = data
this.emit('changed')
})
}
destroy() {
let id = this.data.id
return axios.delete(this.baseUrl + this.resource + 's/' + id)
.then(()=>{
this.data = {}
this.emit('changed')
})
}
update(newData) {
let id = this.data.id
return axios.put(this.baseUrl + this.resource + 's/' + id, newData)
.then(({data})=>{
this.data = data;
this.emit('changed')
})
}
}

var model = new Model({
resource: 'book',
data: {
id: null,
number: 0,
name: null
}
})

var view = new Vue({
el: '#app',
data:{
name:'未命名',
number: 0,
n: 1
},
template: `
<div>
<div>
书名:《{{name}}》,
数量:{{number}}
</div>
<div><input v-model="n"></div>
<div class="actions">
<button id="increaseByOne">加N</button>
<button id="decreaseByOne">减N</button>
<button id="square">平方</button>
<button id="cube">立方</button>
<button id="reset">归零</button>
</div>
</div>
`,
})

class Controller {
constructor({
view, model, events, init, ...rest
}) {
this.view = view
this.model = model
this.events = events
Object.assign(this, rest)
this.bindEvents()
init.apply(this, arguments)
}
bindEvents() {

this.events.map((e) => {
$(this.view.$el).on(e.type, e.el, this[e.fn].bind(this))
})


}
}

var controller = new Controller({
view: view,
model: model,
events: [
{ type: 'click', el: '#increaseByOne', fn: 'add' },
{ type: 'click', el: '#decreaseByOne', fn: 'minus' },
{ type: 'click', el: '#square', fn: 'square' },
{ type: 'click', el: '#cube', fn: 'cube' },
{ type: 'click', el: '#reset', fn: 'reset' }
],
init(options) {
this.model.on('changed', ()=>{
console.log('c')
this.view.name = this.model.data.name
this.view.number = this.model.data.number
})
this.model.fetch(1)
},
add() {
let newData = {number: this.model.data.number + (this.view.n-0)}
this.updateModel(newData)
},
minus() {
// 注意这里有 bug
this.model.data.number = this.model.data.number - this.view.n
this.updateModel(this.model.data)
},
square() {
let newData = {number: Math.pow(this.model.data.number, 2)}
this.updateModel(newData)
},
cube() {
let newData = {number: Math.pow(this.model.data.number, 3)}
this.updateModel(newData)
},
reset() {
this.updateModel({number: 0})
},
updateModel(newData) {
this.model.update(newData)
}
})


Vue代替了 View,这就是Vue的名字和其读音的来历。

Vue的双向绑定(也是Angular的双向绑定)有这些功能:

  1. 只要 JS 改变了 view.number 或 view.name 或 view.n (注意 Vue 把 data 里面的 number、name 和 n 放到了 view 上面,没有 view.data 这 个东西),HTML就会局部更新

  2. 只要用户在input里输入了值,JS里的view.n就会更新。

这就像双向绑定:」S数据与页面元素互相绑定。

同时Vue也实现了局部更新(2.2)。

Vue还有很多其他功能,使得Controller显得多余:

例子6: https://jsbin.com/ruzikax/2/edit?js,output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
axios.interceptors.response.use(function(response) {
let {
config: {
url, method, data
}
} = response
data = JSON.parse(data || '{}')
let row = {
id: 1,
name: 'JavaScript高级程序设计',
number: 2
}
if (url === '/books/1' && method === 'get') {
response.data = row
} else if (url === '/books/1' && method === 'put') {
response.data = Object.assign(row, data)
}
return response
})

class EventHub {
constructor() {
this.events = {}
}
on(eventName, fn) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
this.events[eventName].push(fn)
}
emit(eventName, params) {
let fnList = this.events[eventName]
fnList.map((fn) => {
fn.apply(null, params)
})
}
off(eventName, fn) {
let fnList = this.events[eventName]
for (let i = 0; i < fnList.length; i++) {
if (fnList[i] === fn) {
delete fnList[i]
break
}
}
}
}

class Model extends EventHub {
constructor(options) {
super()
this.data = options.data || {}
this.resource = options.resource
this.baseUrl = options.baseUrl || '/'
}
fetch(id) {
return axios.get(this.baseUrl + this.resource + 's/' + id)
.then(({
data
}) => {
this.data = data
this.emit('changed')
})
}
create(data) {
return axios.post(this.baseUrl + this.resource + 's', data)
.then(({
data
}) => {
this.data = data
this.emit('changed')
})
}
destroy() {
let id = this.data.id
return axios.delete(this.baseUrl + this.resource + 's/' + id)
.then(() => {
this.data = {}
this.emit('changed')
})
}
update(newData) {
let id = this.data.id
return axios.put(this.baseUrl + this.resource + 's/' + id, newData)
.then(({
data
}) => {
this.data = data;
this.emit('changed')
})
}
}

var model = new Model({
resource: 'book',
data: {
id: null,
number: 0,
name: null
}
})

var view = new Vue({
el: '#app',
data: {
name: '未命名',
number: 0,
n: 1
},
template: `
<div>
<div>
书名:《{{name}}》,
数量:{{number}}
</div>
<div><input v-model="n"></div>
<div class="actions">
<button v-on:click="add">加N</button>
<button v-on:click="minus">减N</button>
<button v-on:click="square">平方</button>
<button v-on:click="cube">立方</button>
<button v-on:click="reset">归零</button>
</div>
</div>
`,
created(){

},
methods: {
add() {
let newData = {
number: this.number + (this.n - 0)
}
this.updateModel(newData)
},
minus() {
let newData = {
number: this.number - (this.n - 0)
}
this.updateModel(newData)
},
square() {
let newData = {
number: Math.pow(this.number, 2)
}
this.updateModel(newData)
},
cube() {
let newData = {
number: Math.pow(this.number, 3)
}
this.updateModel(newData)
},
reset() {
this.updateModel({
number: 0
})
},
updateModel(newData) {
model.update(newData)
}
}
})

model.on('changed', () => {
console.log('c')
view.name = model.data.name
view.number = model.data.number
})
model.fetch(1)

然后model.data也显得多余:

例子7: https://jsbin.com/cuhurit/5/edit?js,output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
axios.interceptors.response.use(function(response) {
let {
config: {
url, method, data
}
} = response
data = JSON.parse(data || '{}')
let row = {
id: 1,
name: 'JavaScript高级程序设计',
number: 2
}
if (url === '/books/1' && method === 'get') {
response.data = row
} else if (url === '/books/1' && method === 'put') {
response.data = Object.assign(row, data)
}
console.log('response')
console.log(response)
return response
})



class Model{
constructor(options) {
this.resource = options.resource
this.baseUrl = options.baseUrl || '/'
}
fetch(id) {
return axios.get(this.baseUrl + this.resource + 's/' + id)

}
create(data) {
return axios.post(this.baseUrl + this.resource + 's', data)

}
destroy(id) {
return axios.delete(this.baseUrl + this.resource + 's/' + id)

}
update(id, newData) {
return axios.put(this.baseUrl + this.resource + 's/' + id, newData)

}
}

var model = new Model({
resource: 'book'
})

var view = new Vue({
el: '#app',
data: {
book: {
id: null,
name: '未命名',
number: 0,
},
n: 1
},
template: `
<div>
<div>
书名:《{{book.name}}》,
数量:{{book.number}}
</div>
<div><input v-model="n"></div>
<div class="actions">
<button v-on:click="add">加N</button>
<button v-on:click="minus">减N</button>
<button v-on:click="square">平方</button>
<button v-on:click="cube">立方</button>
<button v-on:click="reset">归零</button>
</div>
</div>
`,
created() {
model.fetch(1).then(({
data
}) => {
view.book = data
})
},
methods: {
add() {
let newData = {
number: this.book.number + (this.n - 0)
}
this.updateModel(newData)
},
minus() {
let newData = {
number: this.book.number - (this.n - 0)
}
this.updateModel(newData)
},
square() {
let newData = {
number: Math.pow(this.book.number, 2)
}
this.updateModel(newData)
},
cube() {
let newData = {
number: Math.pow(this.book.number, 3)
}
this.updateModel(newData)
},
reset() {
this.updateModel({
number: 0
})
},
updateModel(newData) {
model.update(this.book.id, newData).then(({data})=>{
console.log(data)
this.book = data
})
}
}
})



可以看到,事情变得「容易」了很多。

React 单向绑定

双向绑定看起来很magic (魔法),但是有些人觉得单向更好:

例子8: https://jsbin.com/fodasut/3/edit?html,js,output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
axios.interceptors.response.use(function(response) {
let {
config: {
url, method, data
}
} = response
data = JSON.parse(data || '{}')
let row = {
id: 1,
name: 'JavaScript高级程序设计',
number: 2
}
if (url === '/books/1' && method === 'get') {
response.data = row
} else if (url === '/books/1' && method === 'put') {
response.data = Object.assign(row, data)
}
return response
})



class Model{
constructor(options) {
this.resource = options.resource
this.baseUrl = options.baseUrl || '/'
}
fetch(id) {
return axios.get(this.baseUrl + this.resource + 's/' + id)

}
create(data) {
return axios.post(this.baseUrl + this.resource + 's', data)

}
destroy(id) {
return axios.delete(this.baseUrl + this.resource + 's/' + id)

}
update(id, newData) {
return axios.put(this.baseUrl + this.resource + 's/' + id, newData)

}
}

var model = new Model({
resource: 'book'
})

class BookCard extends React.Component {
constructor(props) {
super(props);
this.state = {
book: {id: null, name: '', number: 0},
n: 1
}
}
componentDidMount(){
model.fetch(1).then((response) => {
this.setState({
book: response.data
})
})
}
render() {
return (
<div>
<div>
书名:《{this.state.book.name}》,
数量:{this.state.book.number}
</div>
<div>
<input value={this.state.n} onChange={this.changeN}/>
</div>
<div className="actions">
<button onClick={this.add.bind(this)}>加N</button>
<button onClick={this.minus.bind(this)}>减N</button>
<button onClick={this.square.bind(this)}>平方</button>
<button onClick={this.cube.bind(this)}>立方</button>
<button onClick={this.reset.bind(this)}>归零</button>
</div>
</div>
)
}
changeN(e){
console.log(e)
this.setState({
n: this.state.n
})
}
add(){
const newData = {
number: this.state.book.number + this.state.n
}
model.update(this.state.book.id, newData)
.then((response)=> {
this.setState({book: response.data})
})
}
minus(){
const newData = {
number: this.state.book.number - this.state.n
}
model.update(this.state.book.id, newData)
.then((response)=> {
this.setState({book: response.data})
})
}
square(){
const newData = {
number: Math.pow(this.state.book.number,2)
}
model.update(this.state.book.id, newData)
.then((response)=> {
this.setState({book: response.data})
})
}
cube(){
const newData = {
number: Math.pow(this.state.book.number,3)
}
model.update(this.state.book.id, newData)
.then((response)=> {
this.setState({book: response.data})
})
}
reset(){
const newData = {
number: 0
}
model.update(this.state.book.id, newData)
.then((response)=> {
this.setState({book: response.data})
})
}
}

var view = <BookCard/>;

ReactDOM.render(
view,
document.getElementById('app')
);

我们来想想为什么双向绑定不好呢?

因为「双向绑定」有一点点反「组件化」:跨组件的双向绑定很竒怪。

但是局部使用双向绑定是非常爽的。

单向绑定的要点

  1. 单向
  2. VirtualDOM

双向绑定的要点

  1. 实现方式
    1. Dirty Checking(AngularJS 1.x) 的方式
    2. Reactive
      1. 使用 getter setter
      2. 使用 Proxy
编辑