首先你要弄明白什么是双向数据绑定,双向数据绑定就是视图层发生变化,能实时反应到数据层,数据层发生变化也能实时响应到视图层。现在的各种javascript的UI框架,很多都是实现了数据双向绑定的,backbone,angular,vue,react等等。那么实现双向数据绑定有哪些方法呢,目前我知道的有发布订阅者模式,数据劫持,数据脏检查,下面我就说最普遍的发布订阅者模式吧。
我们知道javascript本身就是一门事件驱动语言,它的事件驱动特性是解决很多问题的最佳实践。我们可以给视图层绑定事件,一般来说,像input,select,textarea等表单控件发生变化,可以触发input,change等事件,当触发事件时,在事件处理函数里面把数据更新到数据层就可以。数据层的话,我们可以通过自定义事件来触发变化,比如说你自定义了一个dataUpdate事件,当从ajax返回数据时,可以把触发dataUpdate事件,在事件处理函数里面,把新数据更新到视图层就可以了。其实发布订阅者模式可以想象成是报社和订阅报纸的人,你只要发布了报纸,那么订阅这个报纸的人就能接收到
======================================================================================================
11年刚开始用前端MVC框架时写过一篇文章,当时Knockout和Backbone都在用,但之后的项目全是在用Backbone,主要因为它简单、灵活,无论是富JS应用还是企业网站都用得上。相比React针对View和单向数据流的设计,Backbone更能体现MVC的思路,所以针对它写一篇入门范例,说明如下:
1. 结构上分4节,介绍Model/View/Collection,实现从远程获取数据显示到表格且修改删除;
2. 名为“范例”,所以代码为主,每节的第1段代码都是完整代码,复制粘贴就能用,每段代码都是基于前一段代码来写的,因此每段代码的新内容不会超过20行(大括号计算在内);
3. 每行代码没有注释,但重要内容之后有写具体的说明;
4. 开发环境是Chrome,使用github的API,这样用Chrome即使在本地路径(形如file://的路径)也能获取数据。
几乎所有的框架都是在做两件事:一是帮你把代码写在正确的地方;二是帮你做一些脏活累活。Backbone实现一种清晰的MVC代码结构,解决了数据模型和视图映射的问题。虽然所有JS相关的项目都可以用,但Backbone最适合的还是这样一种场景:需要用JS生成大量的页面内容(HTML为主),用户跟页面元素有很多的交互行为。
Backbone对象有5个重要的函数,Model/Collection/View/Router/History。Router和History是针对Web应用程序的优化,建议先熟悉pushState的相关知识。入门阶段可以只了解Model/Collection/View。将Model视为核心,Collection是Model的集合,View是为了实现Model的改动在前端的反映。
Model是所有JS应用的核心,很多Backbone教程喜欢从View开始讲,其实View的内容不多,而且理解了View意义不大,理解Model更重要。以下代码实现从github的API获取一条gist信息,显示到页面上:
description | URL | created_at |
---|
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
|
href
=
"http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css"
rel
=
"stylesheet"
>
|
打开Chrome的Console,输入gist,可以看到Model获得的属性:
Model提供数据和与数据相关的逻辑。上图输出的属性是数据,代码中的fetch/parse/get/set都是对数据进行操作,其他的还有escape/unset/clear/destory,从函数名字就大致可以明白它的用途。还有很常用的validate函数,在set/save操作时用来做数据验证,验证失败会触发invalid事件:
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
|
/* 替换之前代码的JS部分(LINE16~34) */
var
Gist
=
Backbone
.
Model
.
extend
(
{
url
:
'https://api.github.com/gists/public'
,
parse
:
function
(
response
)
{
return
(
response
[
0
]
)
;
}
,
defaults
:
{
website
:
'dmyz'
}
,
validate
:
function
(
attrs
)
{
if
(
attrs
.
website
==
'dmyz'
)
{
return
'Website Error'
;
}
}
}
)
,
gist
=
new
Gist
(
)
;
gist
.
on
(
'invalid'
,
function
(
model
,
error
)
{
alert
(
error
)
;
}
)
;
gist
.
on
(
'change'
,
function
(
model
)
{
var
tbody
=
document
.
getElementById
(
'js-id-gists'
)
.
children
[
1
]
,
tr
=
document
.
getElementById
(
model
.
get
(
'id'
)
)
;
if
(
!
tr
)
{
tr
=
document
.
createElement
(
'tr'
)
;
tr
.
setAttribute
(
'id'
,
model
.
get
(
'id'
)
)
;
}
tr
.
innerHTML
=
'
'
+
model
.
get
(
'description'
)
+
' | '
+
model
.
get
(
'url'
)
+
' | '
+
model
.
get
(
'created_at'
)
+
' | '
;
tbody
.
appendChild
(
tr
)
;
}
)
;
gist
.
save
(
)
;
|
跟之前的代码比较,有4处改动:
因为没有fetch,所以页面上不会显示数据。执行save操作,会调用validate函数,验证失败会触发invalid事件,alert出错误提示。同时save操作也会向Model的URL发起一个PUT请求,github这个API没有处理PUT,所以会返回404错误。
在Console中输入gist.set(‘description’, ‘demo’),可以看到页面元素也会有相应的变化。执行gist.set(‘description’, gist.previous(‘description’))恢复之前的值。这就是Model和View的映射,现在还是自己实现的,下一节会用Backbone的View来实现。
用Backbone的View来改写之前代码LINE24~33部分:
description | URL | created_at |
---|
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
|
href
=
"http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css"
rel
=
"stylesheet"
>
|
点击行末的a标签,页面显示的这条记录的URL会被修改成http://dmyz.org。
这个View名为GistRow,选择的却是tbody标签,这显然是不合理的。接下来更改JS代码,显示API返回的30条数据:
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
|
/* 替换之前代码的JS部分(LINE16~45) */
var
Gist
=
Backbone
.
Model
.
extend
(
)
,
Gists
=
Backbone
.
Model
.
extend
(
{
url
:
'https://api.github.com/gists/public'
,
parse
:
function
(
response
)
{
return
response
;
}
}
)
,
gists
=
new
Gists
(
)
;
var
GistRow
=
Backbone
.
View
.
extend
(
{
tagName
:
'tr'
,
render
:
function
(
object
)
{
var
model
=
new
Gist
(
object
)
;
this
.
el
.
innerHTML
=
'
'
+
model
.
get
(
'description'
)
+
' | '
+
model
.
get
(
'url'
)
+
' | '
+
model
.
get
(
'created_at'
)
+
' | '
|
return
this
;
}
}
)
;
var
GistsView
=
Backbone
.
View
.
extend
(
{
el
:
'tbody'
,
model
:
gists
,
initialize
:
function
(
)
{
this
.
listenTo
(
this
.
model
,
'change'
,
this
.
render
)
;
}
,
render
:
function
(
)
{
var
html
=
''
;
_
.
forEach
(
this
.
model
.
attributes
,
function
(
object
)
{
var
tr
=
new
GistRow
(
)
;
html
+=
tr
.
render
(
object
)
.
el
.
outerHTML
;
}
)
;
this
.
el
.
innerHTML
=
html
;
return
this
;
}
}
)
;
var
gistsView
=
new
GistsView
(
)
;
gists
.
fetch
(
)
;
|
Backbone的View更多的是组织代码的作用,它实际干的活很少。View的model属性在本节第一段代码用的是大写,表明只是一个名字,并不是说给View传一个Model它会替你完成什么,控制逻辑还是要自己写。还有View中经常会用到的template函数,也是要自己定义的,具体结合哪种模板引擎来用就看自己的需求了。
这段代码中的Gists比较难操作其中的每一个值,它其实应该是Gist的集合,这就是Backbone的Collection做的事了。
Collection是Model的集合,在这个Collection中的Model如果触发了某个事件,可以在Collection中接收到并做处理。第2节的代码用Collection实现:
description | URL | created_at |
---|
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
|
href
=
"http://cdn.bootcss.com/bootstrap/3.1.1/css/bootstrap.min.css"
rel
=
"stylesheet"
>
|
Collection是Backbone里功能最多的函数(虽然其中很多是Underscore的),而且只要理解了Model和View的关系,使用Collection不会有任何障碍。给Collection绑定各种事件来实现丰富的交互功能了,以下这段JS代码会加入删除/编辑的操作,可以在JSBIN上查看源代码和执行结果。只是增加了事件,没有什么新内容,所以就不做说明了,附上JSBIN的演示地址:http://jsbin.com/jevisopo/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
|
/* 替换之前代码的JS部分(LINE16~51) */
var
Gist
=
Backbone
.
Model
.
extend
(
)
,
Gists
=
Backbone
.
Collection
.
extend
(
{
model
:
Gist
,
url
:
'https://api.github.com/gists/public'
,
parse
:
function
(
response
)
{
return
response
;
}
}
)
,
gists
=
new
Gists
(
)
;
var
GistRow
=
Backbone
.
View
.
extend
(
{
tagName
:
'tr'
,
render
:
function
(
model
)
{
this
.
el
.
id
=
model
.
cid
;
this
.
el
.
innerHTML
=
'
'
+
model
.
get
(
'description'
)
+
' | '
+
model
.
get
(
'url'
)
+
' | '
+
model
.
get
(
'created_at'
)
+
' | X E | '
return
this
;
}
}
)
;
var
GistsView
=
Backbone
.
View
.
extend
(
{
el
:
'tbody'
,
collection
:
gists
,
events
:
{
'click a.js-remove'
:
function
(
e
)
{
var
cid
=
e
.
currentTarget
.
parentElement
.
parentElement
.
id
;
gists
.
get
(
cid
)
.
destroy
(
)
;
gists
.
remove
(
cid
)
;
}
,
'click a.js-edit'
:
'editRow'
,
'blur td[contenteditable]'
:
'saveRow'
}
,
editRow
:
function
(
e
)
{
var
tr
=
e
.
currentTarget
.
parentElement
.
parentElement
,
i
=
0
;
while
(
i
<
3
)
{
tr
.
children
[
i
]
.
setAttribute
(
'contenteditable'
,
true
)
;
i
++
;
}
}
,
saveRow
:
function
(
e
)
{
var
tr
=
e
.
currentTarget
.
parentElement
,
model
=
gists
.
get
(
tr
.
id
)
;
model
.
set
(
{
'description'
:
tr
.
children
[
0
]
.
innerText
,
'url'
:
tr
.
children
[
1
]
.
innerText
,
'created_at'
:
tr
.
children
[
2
]
.
innerText
}
)
;
model
.
save
(
)
;
}
,
initialize
:
function
(
)
{
var
self
=
this
;
_
.
forEach
(
[
'reset'
,
'remove'
,
'range'
]
,
function
(
e
)
{
self
.
listenTo
(
self
.
collection
,
e
,
self
.
render
)
;
}
)
;
}
,
render
:
function
(
)
{
var
html
=
''
;
_
.
forEach
(
this
.
collection
.
models
,
function
(
model
)
{
var
tr
=
new
GistRow
(
)
;
html
+=
tr
.
render
(
model
)
.
el
.
outerHTML
;
}
)
;
this
.
el
.
innerHTML
=
html
;
return
this
;
}
}
)
;
var
gistsView
=
new
GistsView
(
)
;
gists
.
fetch
(
{
reset
:
true
}
)
;
|
虽然是入门范例,但因为篇幅有限,有些基本语言特征和Backbone的功能不可能面面俱到,如果还看不懂肯定是我漏掉了需要解释的点,请(在Google之后)评论或是邮件告知。
Backbone不是jQuery插件,引入以后整个DOM立即实现增删改查了,也做不到KnockoutJS/AnglarJS那样,在DOM上做数据绑定就自动完成逻辑。它是将一些前端工作处理得更好更规范,如果学习前端MVC的目的是想轻松完成工作,Backbone可能不是最佳选择。如果有一个项目,100多行HTML和1000多行JS,JS主要都在操作页面DOM(如果讨厌+号连接HTML还可以搭配React/JSX来写),那就可以考虑用Backbone来重写了,它比其他庞大的MVC框架要容易掌握得多,作为入门学习也是非常不错的。