This repository was archived by the owner on Apr 11, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathhow.html
More file actions
330 lines (304 loc) · 20 KB
/
how.html
File metadata and controls
330 lines (304 loc) · 20 KB
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>BiSheng.js</title>
<meta name="author" content="nuysoft@gmail.com">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="双向数据绑定" />
<link href="../bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
body {
font: 14px/1.7 'helvetica neue', 'hiragino sans gb', stheiti,'wenquanyi micro hei',\5FAE\8F6F\96C5\9ED1,\5B8B\4F53, sans-serif;
}
img {
/*width: 100%;*/
}
.container {
max-width: 1000px;
}
pre {
padding: 0px;
}
.pre {
white-space: pre-wrap;
font-family: monospace, serif;
font-size: 1em;
}
.w250 {
width: 250px;
}
.gist .gist-file .gist-data .line-numbers {
line-height: 20px;
}
.gist .gist-file .gist-data .line-data {
line-height: 20px;
}
/* header */
h1, h2, h3 {
margin-top: 20px;
margin-bottom: 10px;
}
h4, h5, h6 {
margin-top: 20px;
margin-bottom: 10px;
}
blockquote p {
font-size: 14px;
line-height: 1.428571429;
font-weight: normal;
color: #777;
}
em, strong {
margin-left: 3px;
margin-right: 3px;
}
footer {
margin-bottom: 30px;
}
</style>
<!-- -->
<script src="../bower_components/jquery/jquery.js"></script>
<script src="../bower_components/handlebars/handlebars.js"></script>
<script src="../bower_components/mockjs/dist/mock.js"></script>
<script src="../dist/bisheng.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-9"><style type="text/css">
img {
width: 100%;
}
</style>
<p><h1>毕昇工作原理 <small>How BiSheng.js works</smaLL></h1></p>
<hr>
<p><img src="image/he.png" alt=""></p>
<h2 id="-">文前</h2>
<p>数据双向绑定,是指自动在视图和业务逻辑之间建立连接,并自动同步变化的前端技术。当业务逻辑导致数据发生变化时,自动同步更新 DOM;当用户操作导致表单元素发生变化时,自动同步更新数据。这种运行机制,避免掉了手工操作 DOM 所需的代码,使得前端攻城师可以用 <a href="http://en.wikipedia.org/wiki/Data-driven_programming">更清晰优雅的方式</a> 来设计和实现业务逻辑。</p>
<blockquote>
<p>数据双向绑定对开发体验的提升很是显著,而且会变革前端的开发模式和设计思路,犹如从雕版印刷到活字印刷的进步,我在开发 BiSheng.js 的过程中对此深有体会,非常值得各位试一试。</p>
</blockquote>
<p>这篇文章不会再谈论 <a href="./why.html">实施数据双向绑定带来的好处</a> 是如何诱人,而是 <a href="http://baike.baidu.com/view/1166106.htm">形而下</a> 地专注于对数据双向绑定的分析和实现。文章的内容基于编写 BiSheng.js 时的思考和尝试,灵感则来自于 <a href="http://angularjs.org/">AngularJS</a> 和 <a href="http://emberjs.com/">EmberJS</a>,结构借鉴了 <a href="http://addyosmani.com/largescalejavascript/">Patterns For Large-Scale JavaScript Application Architecture</a>(<a href="http://nuysoft.com/2013/08/13/large-scale-javascript/">中文翻译</a>)。</p>
<h2 id="-">我是谁,以及我为什么写这个主题?</h2>
<p>我目前是 <a href="http://www.alimama.com/">阿里妈妈</a> 的一名 JavaScript 开发人员,负责 <a href="http://zuanshi.taobao.com">钻石展位广告管理系统</a> 和 <a href="http://dmp.taobao.com/">DMP 数据营销系统</a> 的前端开发。由于这些应用程序不仅复杂,而且需要快速迭代和高度可复用的架构,因此我的职责之一就是确保开发模式尽可能是可维护和可持续的。</p>
<p>已有的数据双向绑定实现大都是大而全的框架,对于既有应用程序的架构体系冲击太大,实施成本可比推倒重建,而且起步价大多是 IE8 或 IE9,所以借鉴意义更大些。为了学习这些实现,解决开发过程中没完没了没完没了的 DOM 操作,我开发了一个纯粹的数据双向绑定工具 <a href="https://github.com/thx/bisheng">BiSheng.js</a>,现在我把思路和过程记录下来,让它们更条理一些,顺便作为 BiSheng.js 的设计文档。</p>
<p>BiSheng.js 的名称源自活字印刷术的发明者“<a href="http://baike.baidu.com/subview/33366/11034585.htm?fromtitle=%E6%AF%95%E5%8D%87&fromid=64860&type=syn">毕昇</a>”。因为单向绑定犹如“刻版印刷”,双向绑定犹如“活字印刷”,故名 BiSheng.js。</p>
<h2 id="-140-">可以用 140 个字概述这篇文章吗?</h2>
<p>如果你时间不够,下面是这篇文章的摘要,只有一条 tweet 的长度:</p>
<p><strong>修改语法树,插入定位符,渲染模板和定位符,解析定位符,建立数据到 DOM 元素的连接,建立 DOM 元素到数据的连接。</strong></p>
<h2 id="-">数据双向绑定的关注点</h2>
<p>我们先直观地分解一下数据双向绑定所要实现的目标:</p>
<ol>
<li>需要能够监听到数据的变化。</li>
<li>需要能够将数据的属性关联到 DOM 元素上。</li>
<li>需要能够监听到表单元素的变化。</li>
</ol>
<p>其中,第 1 个和第 3 个目标是自动同步变化的关键,第 2 个目标则是在视图和业务逻辑之间建立连接的关键。</p>
<p>对于第 3 个目标,基于浏览器事件系统,为表单元素绑定一些默认事件(例如,keyup、change),就可以监听到表单元素的变化;关键还要看第 1 个和第 2 个目标的实现,这也是本文的核心内容。</p>
<h2 id="-">监听数据</h2>
<p>监听数据变化的可选方案并不少,一一逐条罗列和分析:</p>
<ol>
<li>建立数据的副本,用定时器(<a href="http://stackoverflow.com/questions/729921/settimeout-or-setinterval">setTimeout 或 setInterval</a>)周期性地与副本进行比较,并将变化封装成事件,用观察者(Pub/Sub)模式来实现事件广播。</li>
<li>采用 ES5 规范中的 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty">Object.defineProperty</a> 和 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties">Object.defineProperties</a> 为属性定义 <code>get()</code> 和 <code>set()</code> 方法,依此来监听属性的读取和设置操作,功能上非常完善,但是 <a href="http://kangax.github.io/es5-compat-table/">IE9- 不支持</a>,一些 Polyfill(例如,TODO)也不完善。</li>
<li><a href="http://wiki.ecmascript.org/doku.php?id=harmony:observe">Object.observe()</a> 也可以用来监听属性的变化,可是距离可应用也太远了。</li>
<li>甚至你可能会想到 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineGetter">Object.prototype.__defineGetter__</a> 和 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineSetter">Object.prototype.__defineSetter__</a>,但是未被纳入规范,不过这不重点,关键是 <a href="TODO">IE11 才会支持</a>。</li>
</ol>
<p>所以,实用些的做法是,在 IE9+ 和其他浏览器中使用 <code>Object.defineProperty/defineProperties</code>,在 IE9- 中使用定时器 <code>setTimeout</code>。</p>
<blockquote>
<p><code>Object.defineProperty/defineProperties</code> 的缺点在于无法检测未知属性,例如,对象中新增的属性和数组中新增的元素;定时器 <code>setTimeout</code> 的缺点在于(周期性运行必然会导致)不能及时反映数据的变化,此外,也会有性能和电量损耗的问题。</p>
</blockquote>
<p>BiSheng.js 采用的是:</p>
<ul>
<li>对于表单元素的变化,自动检测输入、更新数据和更新 DOM 元素。</li>
<li>对于手动更新数据,则调用 <code>BiSheng.apply()</code> 来触发更新 DOM 元素。</li>
</ul>
<h2 id="-">模板引擎</h2>
<p>模板引擎负责解析模板,并且用提供的真实数据替换替换占位符(变量、函数、循环等),因此模板引擎也是一种数据绑定实现,但尚是静态的、单向的,我们可以利用模板引擎的语法(即占位符的语法),将它扩展成动态的、双向的。</p>
<div class="row">
<div class="col-md-6">
<img src="image/One_Way_Data_Binding.png">
</div>
<div class="col-md-6">
<img src="image/Two_Way_Data_Binding.png">
</div>
</div>
<p><em>图片来自 <a href="http://docs.angularjs.org/guide/databinding">http://docs.angularjs.org/guide/databinding</a></em></p>
<p>从存储方式上,模板引擎可以分为字符串模板和 DOM 模板。DOM 模板更方便解析数据属性和 DOM 元素之间的关系,字符串模板则在使用上更加灵活和方便。</p>
<p>从语法上,又可以分为弱逻辑语法和强逻辑语法。弱逻辑模板并不意味着模板中只能有简单的占位符,但是某些智能标签(即数组迭代、条件渲染)的功能确实相当有限,但弱模板可以在客户端和服务端之间开发和重用,提供最佳性能的同时仍然易于维护;强逻辑模板引擎则有更丰富的功能,并且可扩展,但容易形成意大利面条式的糟糕代码。</p>
<blockquote>
<p>允许在模板中放置任意代码终究不是最好的主意。</p>
</blockquote>
<p>从解析模板的方式上,模板引擎可以分为基于手写解析器和基于解析器生成器。基于手写解析器的模板引擎功能通常比较简单,大多通过正则或特殊符号对字符串进行解析、转义,而且不易扩展;而基于解析器生成器的模板引擎,则会先通过词法分析和语义分析把模板解析为语法树(<a href="http://en.wikipedia.org/wiki/Abstract_syntax_tree">AST Abstract Syntax Tree</a>),然后再把语法树编译为可执行的函数,在分层结构上,比前者更加清晰和内聚。基于语法树的模板引擎也有着更好的扩展性,允许第三方通过再次解析或修改语法树,进而扩展出更多的功能,例如,反向生成数据,转译成其他模板引擎的语法,以及本文的主题:支持数据双向绑定。</p>
<p>模板引擎的实现各有权衡,如何选择则视需求而定。可以在 <a href="http://garann.github.io/template-chooser/">这里</a> 看到各种模板引擎,并且通过几个问题从中筛选出你所中意的。</p>
<p><a href="http://garann.github.io/template-chooser/"><img src="image/template-chooser.png" alt=""></a></p>
<p>我认为适合实施数据双向绑定的有以下 2 种组合方案:</p>
<ol>
<li>DOM 模板 + 弱逻辑语法 + 基于语法树</li>
<li>字符串模板 + 弱逻辑语法 + 基于语法树</li>
</ol>
<p>DOM 模板可以直接在 DOM 元素和数据属性之间建立连接,这实在是天生的优势,确实更容易一些。例如,AngularJS 就采用了这种方案。</p>
<p>字符串模板则需要做更多的处理,因为它的语法树没有体现出 DOM 结构,所以在把渲染结果转换为 DOM 元素后,还需要遍历 DOM 元素,建立与数据属性之间的连接。例如,EmberJS 就采用了这种方案。</p>
<p>总体而言,我倾向于选择 <a href="http://handlebarsjs.com/">Handlebars.js</a>,它属于第 2 种组合,可以运行在客户端和服务端,支持在服务端预编译模板,并且支持 Helper 方法。</p>
<p>BiSheng.js 也选择了 Handlebars.js 作为它支持的第一款模板引擎,不过这并不是唯一的选择,BiSheng.js 还允许扩展支持更多的模板引擎。</p>
<h2 id="-">执行绑定</h2>
<p>BiSheng.js 提供了方法 <code>BiSheng.bind(data, tpl, callback(content))</code>,用于在模板和数据之间执行双向绑定,文档请访问 <a href="/doc/bisheng.html">HTML</a> 或 <a href="/doc/bisheng.md">Markdown</a>。</p>
<p><code>BiSheng.bind()</code> 绑定的关键步骤共有 5 步:</p>
<ol>
<li>修改语法树,插入定位符。</li>
<li>渲染模板和定位符。</li>
<li>解析定位符。</li>
<li>建立数据到 DOM 元素的连接。</li>
<li>建立 DOM 元素到数据的连接。</li>
</ol>
<p>下面以模板 <code>{{title}}</code> 为例来说明 <code>BiSheng.bind()</code> 的绑定过程。绑定代码如下:</p>
<pre><code>// HTML 模板
var tpl = '{{title}}'
// 数据对象
var data = {
title: '注意,title 的值在这里'
}
// 执行双向绑定
BiSheng.bind(data, tpl, function(content){
// 然后在回调函数中将绑定后的 DOM 元素插入文档中
$('div.container').append(content)
});
// 改变数据 data.title,对应的文档区域会更新
data.title = 'bar'</code></pre>
<h3 id="1-">1. 修改语法树,插入定位符</h3>
<p><code>BiSheng.bind()</code> 首先在 <code>{{title}}</code> 的前后插入两个转义后的定位符,在转义之前是:</p>
<pre><code><script guid="1" slot="start" type="" path="{{$lastest title}}" isHelper="false"></script>
<script guid="1" slot="end"></script></code></pre>
<p>其中,<code>$lastest</code> 是一个全局 Helper 方法,用于获取和输出被定位属性 <code>title</code> 的访问路径,代码如下所示:</p>
<pre><code>Handlebars.registerHelper('$lastest', function(items, options) {
return items && items.$path || this && this.$path
})</code></pre>
<p>其中,<code>$path</code> 指示了当前属性的路径,由 BiSheng.js 自动计算和设置。</p>
<p>对应的语法树的变化代码太多,就不贴在这了,请移步这里 <a href="https://gist.github.com/nuysoft/8055993">https://gist.github.com/nuysoft/8055993</a>。</p>
<h3 id="2-">2. 渲染模板和定位符</h3>
<p>然后执行 <code>Handlebars.compile(ast)(data)</code> 渲染模板和定位符,结果如下:</p>
<pre><code>&lt;script guid="1" slot="start" type="" path="1.title" isHelper="false"&gt;&lt;/script&gt;注意,title 的值在这里&lt;script guid="1" slot="end"&gt;&lt;/script&gt;</code></pre>
<p>转以后的代码有些不易读,不过没关系,下一步就会解析它。</p>
<h3 id="3-dom-">3. 扫描 DOM 元素,解析定位符</h3>
<p>然后解析渲染结果中的定位符,结果如下:</p>
<pre><code><script guid="1" slot="start" type="" path="1.title" isHelper="false"></script>注意,title 的值在这里<script guid="1" slot="end"></script></code></pre>
<h3 id="4-dom-">4. 建立数据到 DOM 元素的连接</h3>
<p>现在,对于属性 <code>title</code> 所对应的 DOM 元素,我们可以通过两个 script 元素来精确定位。</p>
<p>具体的做法是,当属性 <code>title</code> 更新时,可以通过选择器表达式 <code>script[slot="start"][path="1.title"]</code> 定位到第一个 script 元素,进而定位到所对应的 DOM 元素,然后更新 DOM 元素。</p>
<h3 id="5-dom-">5. 建立 DOM 元素到数据的连接</h3>
<p>这点在前面已经提过,通过为表单元素绑定一些默认事件(例如,keyup、change),就可以监听到表单元素的变化,进而更新数据属性。</p>
<h3 id="-">小结</h3>
<p>前面以 <code>{{title}}</code> 为例阐述了绑定过程。这尚是最简单的情况,事实上内部的实现比示例要复杂和繁琐许多,不过处理的步骤是一样的。</p>
<h2 id="-">源码导读</h2>
<p><strong>代码的结构</strong>按照职责来设计,见下表;<strong>打包后的文件</strong>在 <a href="https://github.com/thx/bisheng/tree/master/dist/">dist/</a> 目录下;<strong>API 和文档</strong>在 <a href="https://github.com/thx/bisheng/tree/master/doc/">doc/</a> 目录下;<strong>测试用例</strong>在 <a href="https://github.com/thx/bisheng/tree/master/test/">test/</a> 目录下,基本覆盖了目前已实现的功能。</p>
<table>
<thead>
<tr>
<th>源文件</th>
<th>职责 & 功能</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/thx/bisheng/tree/master/src/ast.js">src/ast.js</a></td>
<td>修改语法树,插入定位符。</td>
</tr>
<tr>
<td><a href="https://github.com/thx/bisheng/tree/master/src/bisheng.js">src/bisheng.js</a></td>
<td>双向数据绑定的入口。</td>
</tr>
<tr>
<td><a href="https://github.com/thx/bisheng/tree/master/src/expose.js">src/expose.js</a></td>
<td>模块化,适配主流加载器。</td>
</tr>
<tr>
<td><a href="https://github.com/thx/bisheng/tree/master/src/flush.js">src/flush.js</a></td>
<td>更新 DOM 元素。</td>
</tr>
<tr>
<td><a href="https://github.com/thx/bisheng/tree/master/src/locator.js">src/locator.js</a></td>
<td>生成定位符,解析、更新定位符的属性。</td>
</tr>
<tr>
<td><a href="https://github.com/thx/bisheng/tree/master/src/loop.js">src/loop.js</a></td>
<td>数据属性监听工具。</td>
</tr>
<tr>
<td><a href="https://github.com/thx/bisheng/tree/master/src/scan.js">src/scan.js</a></td>
<td>扫描 DOM 元素,解析定位符。</td>
</tr>
</tbody>
</table>
<!-- 注释行数大约占总行数的 40~50%。 -->
<p><em>TODO</em>
<!-- 结构、模块职责划分、扩展点 --></p>
<h2 id="-">扩展</h2>
<p>理论上,BiSheng.js 可以支持任何基于语法树进行渲染的模板引擎。</p>
<p>源文件 <code>src/ast.js</code> 负责修改语法树,插入一些用于定位 DOM 元素的占位符。如果需要扩展对更多模板引擎的支持,则可以从这个文件开始。</p>
<h2 id="-">下一步</h2>
<!--
功能上:覆盖了编译型模板的大部分功能,尚未处理的有:子模板、Helper。
测试用例:已覆盖了目前开发的所有功能。
API 文档:已覆盖目前的公开 API。
其他文档:主页,实现原理
正在做的:用 HTML 注释节点来替换 script 节点,作为定位符。
-->
<ol>
<li>支持 <a href="http://gitlab.alibaba-inc.com/thx/crox">CROX</a>、<a href="http://docs.kissyui.com/1.4/docs/html/api/xtemplate/index.html">KISSY XTempalte</a></li>
<li>√ 定位符由 script 改为注释节点。</li>
<li>从修改语法树、扫描语法树、更新数据、更新视图等环节,优化性能。</li>
<li>√ 解决兼容性问题(IE)</li>
</ol>
<p><em>TODO</em></p>
<h2 id="-">扩展阅读</h2>
<ul>
<li><a href="http://yunpan.taobao.com/share/link/746RBbA94">MVVM for KISSY</a> by 翰文</li>
<li><a href="http://vdisk.weibo.com/s/aMO9PyIQCnLOF/1375154475">前端MVVM的应用</a> by 司徒正美</li>
</ul>
<p><em>TODO</em></p>
<h2 id="-">总结</h2>
<p><em>TODO</em></p>
</div>
<div class="col-sm-3">
<div class="panel panel-default" style="position: fixed; margin-top: 75px;">
<div class="panel-heading">目录</div>
<div class="panel-body catalog"></div>
</div>
</div>
</div><!-- /row-->
</div>
<footer class="container">
<hr>
<p class="pull-left">
<a href="https://github.com/thx/bisheng">GitHub</a>
</p>
<p class="pull-right">
<a href="http://nuysoft.com/about.html">nuysoft</a>
</p>
</footer>
<!-- Table -->
<script type="text/javascript">
$(function(){
$('table').addClass('table')
})
</script>
<!-- Catalog -->
<script type="text/javascript">
var ul = $('<ul>').addClass('list-unstyled')
$('h2').each(function(index, item){
var name = $(item).html()
$(item).before('<a name="' + name + '"></a>')
ul.append(
$('<li>').append(
$('<a>').attr('href', '#'+ name)
.html(name)
)
)
})
$('div.catalog').append(ul)
</script>
<link rel="stylesheet" href="../bower_components/highlightjs/styles/rainbow.css">
<script src="../bower_components/highlightjs/highlight.pack.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</body>
</html>