forked from godbasin/godbasin.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
459 lines (273 loc) · 412 KB
/
atom.xml
File metadata and controls
459 lines (273 loc) · 412 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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Here. There.</title>
<subtitle>Love ice cream. Love sunshine. Love life. Love the world. Love myself. Love you.</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://godbasin.github.io/"/>
<updated>2021-04-05T13:38:53.491Z</updated>
<id>https://godbasin.github.io/</id>
<author>
<name>被删</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>Angular框架解读--视图抽象定义</title>
<link href="https://godbasin.github.io/2021/04/05/angular-design-dom-define/"/>
<id>https://godbasin.github.io/2021/04/05/angular-design-dom-define/</id>
<published>2021-04-05T13:37:23.000Z</published>
<updated>2021-04-05T13:38:53.491Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中与视图有关的一些定义进行介绍。</p><a id="more"></a><h1 id="Angular-中的视图抽象"><a href="#Angular-中的视图抽象" class="headerlink" title="Angular 中的视图抽象"></a>Angular 中的视图抽象</h1><p>Angular 版本可在不同的平台上运行:在浏览器中、在移动平台上或在 Web Worker 中。因此,需要特定级别的抽象来介于平台特定的 API 和框架接口之间。</p><p>Angular 中通过抽象封装了不同平台的差异,并以下列引用类型的形式出现:<code>ElementRef</code>,<code>TemplateRef</code>,<code>ViewRef</code>,<code>ComponentRef</code>和<code>ViewContainerRef</code>。</p><h2 id="各抽象类视图定义"><a href="#各抽象类视图定义" class="headerlink" title="各抽象类视图定义"></a>各抽象类视图定义</h2><p>在阅读源码的时候,如果不清楚这些定义之间的区别,很容易搞混淆。所以,这里我们先来理解下它们之间的区别。</p><h3 id="元素-ElementRef"><a href="#元素-ElementRef" class="headerlink" title="元素 ElementRef"></a>元素 ElementRef</h3><p><code>ElementRef</code>是最基本的抽象。如果观察它的类结构,可以看到它仅包含与其关联的本地元素:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> ElementRef<T = <span class="built_in">any</span>> {</span><br><span class="line"> <span class="comment">// 基础原生元素</span></span><br><span class="line"> <span class="comment">// 如果不支持直接访问原生元素(例如当应用程序在 Web Worker 中运行时),则为 null</span></span><br><span class="line"> <span class="keyword">public</span> nativeElement: T;</span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params">nativeElement: T</span>) {</span><br><span class="line"> <span class="keyword">this</span>.nativeElement = nativeElement;</span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>该 API 可用于直接访问本地 DOM 元素,可以比作<code>document.getElementById('myId')</code>。但 Angular 并不鼓励直接使用,尽可能使用 Angular 提供的模板和数据绑定。</p><h3 id="模板-TemplateRef"><a href="#模板-TemplateRef" class="headerlink" title="模板 TemplateRef"></a>模板 TemplateRef</h3><p>在 Angular 中,模板用来定义要如何在 HTML 中渲染组件视图的代码。</p><p>模板通过<code>@Component()</code>装饰器与组件类类关联起来。模板代码可以作为<code>template</code>属性的值用内联的方式提供,也可以通过 <code>templateUrl</code>属性链接到一个独立的 HTML 文件。</p><p>用<code>TemplateRef</code>对象表示的其它模板用来定义一些备用视图或内嵌视图,它们可以来自多个不同的组件。<code>TemplateRef</code>是一组 DOM 元素(<code>ElementRef</code>),可在整个应用程序的视图中重复使用:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">abstract</span> <span class="keyword">class</span> TemplateRef<C> {</span><br><span class="line"> <span class="comment">// 此嵌入视图的父视图中的 anchor 元素</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">get</span> elementRef(): ElementRef;</span><br><span class="line"> <span class="comment">// 基于此模板实例化嵌入式视图,并将其附加到视图容器</span></span><br><span class="line"> <span class="keyword">abstract</span> createEmbeddedView(context: C): EmbeddedViewRef<C>;</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>就其本身而言,<code>TemplateRef</code>类是一个简单的类,仅包括:</p><ul><li><code>elementRef</code>属性:拥有对其宿主元素的引用</li><li><code>createEmbeddedView</code>方法:它允许我们创建视图并将其引用作为<code>ViewRef</code>返回。</li></ul><p>模板会把纯 HTML 和 Angular 的数据绑定语法、指令和模板表达式组合起来。Angular 的元素会插入或计算那些值,以便在页面显示出来之前修改 HTML 元素。</p><h2 id="Angular-中的视图"><a href="#Angular-中的视图" class="headerlink" title="Angular 中的视图"></a>Angular 中的视图</h2><p>在 Angular 中,视图是可显示元素的最小分组单位,它们会被同时创建和销毁。Angular 哲学鼓励开发人员将 UI 视为视图的组合(而不是独立的 html 标签树)。</p><p>组件(<code>component</code>) 类及其关联的模板(<code>template</code>)定义了一个视图。具体实现上,视图由一个与该组件相关的<code>ViewRef</code>实例表示。 </p><h3 id="ViewRef"><a href="#ViewRef" class="headerlink" title="ViewRef"></a>ViewRef</h3><p><code>ViewRef</code>表示一个 Angular 视图:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">declare</span> <span class="keyword">abstract</span> <span class="keyword">class</span> ViewRef <span class="keyword">extends</span> ChangeDetectorRef {</span><br><span class="line"> <span class="comment">// 销毁该视图以及与之关联的所有数据结构</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">get</span> destroyed(): <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="comment">// 报告此视图是否已被销毁</span></span><br><span class="line"> <span class="keyword">abstract</span> destroy(): <span class="built_in">void</span>;</span><br><span class="line"> <span class="comment">// 生命周期挂钩,为视图提供其他开发人员定义的清理功能</span></span><br><span class="line"> <span class="keyword">abstract</span> onDestroy(callback: <span class="built_in">Function</span>): <span class="built_in">any</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中,<code>ChangeDetectorRef</code>提供更改检测功能的基类,用于更改检测树收集所有要检查更改的视图:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">declare</span> <span class="keyword">abstract</span> <span class="keyword">class</span> ChangeDetectorRef {</span><br><span class="line"> <span class="comment">// 当输入已更改或视图中触发了事件时,通常会将组件标记为脏(需要重新渲染)</span></span><br><span class="line"> <span class="comment">// 调用此方法以确保即使没有发生这些触发器,也要检查组件</span></span><br><span class="line"> <span class="keyword">abstract</span> checkNoChanges(): <span class="built_in">void</span>;</span><br><span class="line"> <span class="comment">// 从变更检测树中分离该视图。在重新连接分离视图之前,不会对其进行检查。</span></span><br><span class="line"> <span class="comment">// 与 detectChanges() 结合使用可实现本地变更检测检查</span></span><br><span class="line"> <span class="keyword">abstract</span> detach(): <span class="built_in">void</span>;</span><br><span class="line"> <span class="comment">// 检查此视图及其子级,与 detach() 结合使用可实现本地更改检测检查</span></span><br><span class="line"> <span class="keyword">abstract</span> detectChanges(): <span class="built_in">void</span>;</span><br><span class="line"> <span class="comment">// 检查变更检测器及其子级,如果检测到任何变更,则抛出该异常</span></span><br><span class="line"> <span class="keyword">abstract</span> markForCheck(): <span class="built_in">void</span>;</span><br><span class="line"> <span class="comment">// 将先前分离的视图重新附加到变更检测树</span></span><br><span class="line"> <span class="comment">// 默认情况下,视图将附加到树上</span></span><br><span class="line"> <span class="keyword">abstract</span> reattach(): <span class="built_in">void</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="两种类型的视图"><a href="#两种类型的视图" class="headerlink" title="两种类型的视图"></a>两种类型的视图</h3><p>Angular 支持两种类型的视图:</p><p><strong>(1) 链接到模板(<code>template</code>)的嵌入式视图(<code>embeddedView</code>)。</strong></p><p>嵌入式视图表示视图容器中的 Angular 视图。模板只是保存视图的蓝图,可以使用上述的<code>createEmbeddedView</code>方法从模板实例化视图。</p><p><strong>(2) 链接到组件(<code>component</code>)的宿主视图(<code>hostView</code>)。</strong></p><p>直属于某个组件的视图叫做宿主视图。</p><p>宿主视图是在动态实例化组件时创建的,可以使用<code>ComponentFactoryResolver</code>动态创建实例化一个组件。在 Angular 中,每个组件都绑定到特定的注入器实例,因此在创建组件时我们将传递当前的注入器实例。</p><p>视图中各个元素的属性可以动态修改以响应用户的操作,而这些元素的结构(数量或顺序)则不能。你可以通过在它们的视图容器(<code>ViewContainer</code>)中插入、移动或移除内嵌视图来修改这些元素的结构。</p><h3 id="ViewContainerRef"><a href="#ViewContainerRef" class="headerlink" title="ViewContainerRef"></a>ViewContainerRef</h3><p><code>ViewContainerRef</code>是可以将一个或多个视图附着到组件中的容器:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">declare</span> <span class="keyword">abstract</span> <span class="keyword">class</span> ViewContainerRef {</span><br><span class="line"> <span class="comment">// 锚元素,用于指定此容器在包含视图中的位置</span></span><br><span class="line"> <span class="comment">// 每个视图容器只能有一个锚元素,每个锚元素只能有一个视图容器</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">get</span> element(): ElementRef;</span><br><span class="line"> <span class="comment">// 此视图容器的 DI</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">get</span> injector(): Injector;</span><br><span class="line"> <span class="comment">// 此容器当前附加了多少视图</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">get</span> length(): <span class="built_in">number</span>;</span><br><span class="line"> <span class="comment">// 销毁此容器中的所有视图</span></span><br><span class="line"> <span class="keyword">abstract</span> clear(): <span class="built_in">void</span>;</span><br><span class="line"> <span class="comment">// 实例化单个组件,并将其宿主视图插入此容器</span></span><br><span class="line"> <span class="keyword">abstract</span> createComponent<C>(componentFactory: ComponentFactory<C>, index?: <span class="built_in">number</span>, injector?: Injector, projectableNodes?: <span class="built_in">any</span>[][], ngModule?: NgModuleRef<<span class="built_in">any</span>>): ComponentRef<C>;</span><br><span class="line"> <span class="comment">// 实例化一个嵌入式视图并将其插入</span></span><br><span class="line"> <span class="keyword">abstract</span> createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: <span class="built_in">number</span>): EmbeddedViewRef<C>;</span><br><span class="line"> <span class="comment">// 从此容器分离视图而不销毁它</span></span><br><span class="line"> <span class="keyword">abstract</span> detach(index?: <span class="built_in">number</span>): ViewRef | <span class="literal">null</span>;</span><br><span class="line"> <span class="comment">// 从此容器检索视图</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">get</span>(index: <span class="built_in">number</span>): ViewRef | <span class="literal">null</span>;</span><br><span class="line"> <span class="comment">// 返回当前容器内视图的索引</span></span><br><span class="line"> <span class="keyword">abstract</span> indexOf(viewRef: ViewRef): <span class="built_in">number</span>;</span><br><span class="line"> <span class="comment">// 将视图移动到此容器中的新位置</span></span><br><span class="line"> <span class="keyword">abstract</span> insert(viewRef: ViewRef, index?: <span class="built_in">number</span>): ViewRef;</span><br><span class="line"> <span class="keyword">abstract</span> move(viewRef: ViewRef, currentIndex: <span class="built_in">number</span>): ViewRef;</span><br><span class="line"> <span class="comment">// 销毁附加到此容器的视图</span></span><br><span class="line"> <span class="keyword">abstract</span> remove(index?: <span class="built_in">number</span>): <span class="built_in">void</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>任何 DOM 元素都可以用作视图容器,Angular 不会在元素内插入视图,而是将它们附加到绑定到<code>ViewContainer</code>的元素之后。</p><blockquote><p>通常,标记<code>ng-container</code>元素是标记应创建<code>ViewContainer</code>的位置的最佳选择。它作为注释呈现,因此不会在 DOM 中引入多余的 HTML 元素。</p></blockquote><p>通过<code>ViewContainerRef</code>,可以用<code>createComponent()</code>方法实例化组件时创建宿主视图,也可以用 <code>createEmbeddedView()</code>方法实例化<code>TemplateRef</code>时创建内嵌视图。</p><p>视图容器的实例还可以包含其它视图容器,以创建层次化视图(视图树)。</p><h3 id="视图树(View-hierarchy)"><a href="#视图树(View-hierarchy)" class="headerlink" title="视图树(View hierarchy)"></a>视图树(View hierarchy)</h3><p>在 Angular 中,通常会把视图组织成一些视图树(view hierarchies)。视图树是一棵相关视图的树,它们可以作为一个整体行动,是 Angular 变更检测的关键部件之一。</p><p>视图树的根视图就是组件的宿主视图。宿主视图可以是内嵌视图树的根,它被收集到了宿主组件上的一个视图容器(<code>ViewContainerRef</code>)中。当用户在应用中导航时(比如使用路由器),视图树可以动态加载或卸载。</p><p>视图树和组件树并不是一一对应的:</p><ul><li>嵌入到指定视图树上下文中的视图,也可能是其它组件的宿主视图</li><li>组件可能和宿主组件位于同一个<code>NgModule</code>中,也可能属于其它<code>NgModule</code></li></ul><h3 id="组件、模板、视图与模块"><a href="#组件、模板、视图与模块" class="headerlink" title="组件、模板、视图与模块"></a>组件、模板、视图与模块</h3><p>在 Angular 中,可以通过组件的配套模板来定义其视图。模板就是一种 HTML,它会告诉 Angular 如何渲染该组件。</p><p>视图通常会分层次进行组织,让你能以 UI 分区或页面为单位进行修改、显示或隐藏。与组件直接关联的模板会定义该组件的宿主视图。该组件还可以定义一个带层次结构的视图,它包含一些内嵌的视图作为其它组件的宿主。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/component-tree.png" alt></p><p>带层次结构的视图可以包含同一模块(<code>NgModule</code>)中组件的视图,也可以(而且经常会)包含其它模块中定义的组件的视图。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文简单介绍了 Angular 中元素、视图、模板、组件中与视图相关的一些定义,包括<code>ElementRef</code>,<code>TemplateRef</code>,<code>ViewRef</code>,<code>ComponentRef</code>和<code>ViewContainerRef</code>。</p><p>其中,视图是 Angular 中应用程序 UI 的基本构建块,它是一起创建和销毁的最小元素组。</p><p><code>ViewContainerRef</code>主要用于创建和管理内嵌视图或组件视图。组件可以通过配置模板来定义视图,与组件直接关联的模板会定义该组件的宿主视图,同时组件还可以包括内嵌视图。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://angular.cn/guide/architecture-components" target="_blank" rel="noopener">Angular-组件简介</a></li><li><a href="https://angular.cn/guide/glossary" target="_blank" rel="noopener">Angular 词汇表</a></li><li><a href="https://hackernoon.com/exploring-angular-dom-abstractions-80b3ebcfc02" target="_blank" rel="noopener">Exploring Angular DOM manipulation techniques using ViewContainerRef</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中与视图有关的一些定义进行介绍。</p>
</summary>
<category term="Angular源码" scheme="https://godbasin.github.io/categories/Angular%E6%BA%90%E7%A0%81/"/>
<category term="功能设计" scheme="https://godbasin.github.io/tags/%E5%8A%9F%E8%83%BD%E8%AE%BE%E8%AE%A1/"/>
</entry>
<entry>
<title>Angular框架解读--元数据和装饰器</title>
<link href="https://godbasin.github.io/2021/03/27/angular-design-metadata/"/>
<id>https://godbasin.github.io/2021/03/27/angular-design-metadata/</id>
<published>2021-03-27T05:25:31.000Z</published>
<updated>2021-03-27T05:33:33.875Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中随处可见的元数据,来进行介绍。</p><a id="more"></a><p>装饰器是使用 Angular 进行开发时的核心概念。在 Angular 中,装饰器用于为类或属性附加元数据,来让自己知道那些类或属性的含义,以及该如何处理它们。</p><h2 id="装饰器与元数据"><a href="#装饰器与元数据" class="headerlink" title="装饰器与元数据"></a>装饰器与元数据</h2><p>不管是装饰器还是元数据,都不是由 Angular 提出的概念。因此,我们先来简单了解一下。</p><h3 id="元数据(Metadata)"><a href="#元数据(Metadata)" class="headerlink" title="元数据(Metadata)"></a>元数据(Metadata)</h3><p>在通用的概念中,元数据是描述用户数据的数据。它总结了有关数据的基本信息,可以使查找和使用特定数据实例更加容易。例如,作者,创建日期,修改日期和文件大小是非常基本的文档元数据的示例。</p><p>在用于类的场景下,元数据用于装饰类,来描述类的定义和行为,以便可以配置类的预期行为。</p><h3 id="装饰器(Decorator)"><a href="#装饰器(Decorator)" class="headerlink" title="装饰器(Decorator)"></a>装饰器(Decorator)</h3><p>装饰器是 JavaScript 的一种语言特性,是一项位于阶段 2(stage 2)的试验特性。</p><p>装饰器是定义期间在类,类元素或其他 JavaScript 语法形式上调用的函数。</p><p>装饰器具有三个主要功能:</p><ol><li>可以用具有相同语义的匹配值替换正在修饰的值。(例如,装饰器可以将方法替换为另一种方法,将一个字段替换为另一个字段,将一个类替换为另一个类,等等)。</li><li>可以将元数据与正在修饰的值相关联;可以从外部读取此元数据,并将其用于元编程和自我检查。</li><li>可以通过元数据提供对正在修饰的值的访问。对于公共值,他们可以通过值名称来实现;对于私有值,它们接收访问器函数,然后可以选择共享它们。</li></ol><p>本质上,装饰器可用于对值进行元编程和向其添加功能,而无需从根本上改变其外部行为。</p><p>更多的内容,可以参考 <a href="https://github.com/tc39/proposal-decorators" target="_blank" rel="noopener">tc39/proposal-decorators</a> 提案。</p><h2 id="Angular-中的装饰器和元数据"><a href="#Angular-中的装饰器和元数据" class="headerlink" title="Angular 中的装饰器和元数据"></a>Angular 中的装饰器和元数据</h2><p>我们在开发 Angular 应用时,不管是组件、指令,还是服务、模块等,都需要通过装饰器来进行定义和开发。装饰器会出现在类定义的紧前方,用来声明该类具有指定的类型,并且提供适合该类型的元数据。</p><p>比如,我们可以用下列装饰器来声明 Angular 的类:<code>@Component()</code>、<code>@Directive()</code>、<code>@Pipe()</code>、<code>@Injectable()</code>、<code>@NgModule()</code>。</p><h3 id="使用装饰器和元数据来改变类的行为"><a href="#使用装饰器和元数据来改变类的行为" class="headerlink" title="使用装饰器和元数据来改变类的行为"></a>使用装饰器和元数据来改变类的行为</h3><p>以<code>@Component()</code>为例,该装饰器的作用包括:</p><ol><li>将类标记为 Angular 组件。</li><li>提供可配置的元数据,用来确定应在运行时如何处理、实例化和使用该组件。</li></ol><p>关于<code>@Component()</code>该如何使用可以参考<a href></a>,这里不多介绍。我们来看看这个装饰器的定义:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 提供 Angular 组件的配置元数据接口定义</span></span><br><span class="line"><span class="comment">// Angular 中,组件是指令的子集,始终与模板相关联</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> Component <span class="keyword">extends</span> Directive {</span><br><span class="line"> <span class="comment">// changeDetection 用于此组件的变更检测策略</span></span><br><span class="line"> <span class="comment">// 实例化组件时,Angular 将创建一个更改检测器,该更改检测器负责传播组件的绑定。</span></span><br><span class="line"> changeDetection?: ChangeDetectionStrategy;</span><br><span class="line"> <span class="comment">// 定义对其视图 DOM 子对象可见的可注入对象的集合</span></span><br><span class="line"> viewProviders?: Provider[];</span><br><span class="line"> <span class="comment">// 包含组件的模块的模块ID,该组件必须能够解析模板和样式的相对 URL</span></span><br><span class="line"> moduleId?: <span class="built_in">string</span>;</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 模板和 CSS 样式的封装策略</span></span><br><span class="line"> encapsulation?: ViewEncapsulation;</span><br><span class="line"> <span class="comment">// 覆盖默认的插值起始和终止定界符(`{{`和`}}`)</span></span><br><span class="line"> interpolation?: [<span class="built_in">string</span>, <span class="built_in">string</span>];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 组件装饰器和元数据</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> Component: ComponentDecorator = makeDecorator(</span><br><span class="line"> <span class="string">'Component'</span>,</span><br><span class="line"> <span class="comment">// 使用默认的 CheckAlways 策略,在该策略中,更改检测是自动进行的,直到明确停用为止。</span></span><br><span class="line"> (c: Component = {}) => ({changeDetection: ChangeDetectionStrategy.Default, ...c}),</span><br><span class="line"> Directive, <span class="literal">undefined</span>,</span><br><span class="line"> (<span class="keyword">type</span>: Type<<span class="built_in">any</span>>, meta: Component) => SWITCH_COMPILE_COMPONENT(<span class="keyword">type</span>, meta));</span><br></pre></td></tr></table></figure><p>以上便是组件装饰、组件元数据的定义,我们来看看装饰器的创建过程。</p><h3 id="装饰器的创建过程"><a href="#装饰器的创建过程" class="headerlink" title="装饰器的创建过程"></a>装饰器的创建过程</h3><p>我们可以从源码中找到,组件和指令的装饰器都会通过<code>makeDecorator()</code>来产生:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">makeDecorator</span><<span class="title">T</span>>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> name: <span class="built_in">string</span>, props?: (...args: <span class="built_in">any</span>[]) => <span class="built_in">any</span>, parentClass?: <span class="built_in">any</span>, <span class="comment">// 装饰器名字和属性</span></span></span></span><br><span class="line"><span class="function"><span class="params"> additionalProcessing?: (<span class="keyword">type</span>: Type<T>) => <span class="built_in">void</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> typeFn?: (<span class="keyword">type</span>: Type<T>, ...args: <span class="built_in">any</span>[]) => <span class="built_in">void</span></span>):</span></span><br><span class="line"><span class="function"> </span>{<span class="keyword">new</span> (...args: <span class="built_in">any</span>[]): <span class="built_in">any</span>; <span class="function">(<span class="params">...args: <span class="built_in">any</span>[]</span>): <span class="params">any</span>; (<span class="params">...args: <span class="built_in">any</span>[]</span>): (<span class="params">cls: <span class="built_in">any</span></span>) =></span> <span class="built_in">any</span>;} {</span><br><span class="line"> <span class="comment">// noSideEffects 用于确认闭包编译器包装的函数没有副作用</span></span><br><span class="line"> <span class="keyword">return</span> noSideEffects(<span class="function"><span class="params">()</span> =></span> { </span><br><span class="line"> <span class="keyword">const</span> metaCtor = makeMetadataCtor(props);</span><br><span class="line"> <span class="comment">// 装饰器工厂</span></span><br><span class="line"> <span class="function"><span class="keyword">function</span> <span class="title">DecoratorFactory</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">this</span>: unknown|<span class="keyword">typeof</span> DecoratorFactory, ...args: <span class="built_in">any</span>[]</span>): (<span class="params">cls: Type<T></span>) => <span class="title">any</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span> <span class="keyword">instanceof</span> DecoratorFactory) {</span><br><span class="line"> <span class="comment">// 赋值元数据</span></span><br><span class="line"> metaCtor.call(<span class="keyword">this</span>, ...args);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span> <span class="keyword">as</span> <span class="keyword">typeof</span> DecoratorFactory;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 创建装饰器工厂</span></span><br><span class="line"> <span class="keyword">const</span> annotationInstance = <span class="keyword">new</span> (DecoratorFactory <span class="keyword">as</span> <span class="built_in">any</span>)(...args);</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span> <span class="title">TypeDecorator</span>(<span class="params">cls: Type<T></span>) </span>{</span><br><span class="line"> <span class="comment">// 编译类</span></span><br><span class="line"> <span class="keyword">if</span> (typeFn) typeFn(cls, ...args);</span><br><span class="line"> <span class="comment">// 使用 Object.defineProperty 很重要,因为它会创建不可枚举的属性,从而防止该属性在子类化过程中被复制。</span></span><br><span class="line"> <span class="keyword">const</span> annotations = cls.hasOwnProperty(ANNOTATIONS) ?</span><br><span class="line"> (cls <span class="keyword">as</span> <span class="built_in">any</span>)[ANNOTATIONS] :</span><br><span class="line"> <span class="built_in">Object</span>.defineProperty(cls, ANNOTATIONS, {value: []})[ANNOTATIONS];</span><br><span class="line"> annotations.push(annotationInstance);</span><br><span class="line"> <span class="comment">// 特定逻辑的执行</span></span><br><span class="line"> <span class="keyword">if</span> (additionalProcessing) additionalProcessing(cls);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> cls;</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (parentClass) {</span><br><span class="line"> <span class="comment">// 继承父类</span></span><br><span class="line"> DecoratorFactory.prototype = <span class="built_in">Object</span>.create(parentClass.prototype);</span><br><span class="line"> }</span><br><span class="line"> DecoratorFactory.prototype.ngMetadataName = name;</span><br><span class="line"> (DecoratorFactory <span class="keyword">as</span> <span class="built_in">any</span>).annotationCls = DecoratorFactory;</span><br><span class="line"> <span class="keyword">return</span> DecoratorFactory <span class="keyword">as</span> <span class="built_in">any</span>;</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面的例子中,我们通过<code>makeDecorator()</code>产生了一个用于定义组件的<code>Component</code>装饰器工厂。当使用<code>@Component()</code>创建组件时,Angular 会根据元数据来编译组件。</p><h3 id="根据装饰器元数据编译组件"><a href="#根据装饰器元数据编译组件" class="headerlink" title="根据装饰器元数据编译组件"></a>根据装饰器元数据编译组件</h3><p>Angular 会根据该装饰器元数据,来编译 Angular 组件,然后将生成的组件定义(<code>ɵcmp</code>)修补到组件类型上:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">compileComponent</span>(<span class="params"><span class="keyword">type</span>: Type<<span class="built_in">any</span>>, metadata: Component</span>): <span class="title">void</span> </span>{</span><br><span class="line"> <span class="comment">// 初始化 ngDevMode</span></span><br><span class="line"> (<span class="keyword">typeof</span> ngDevMode === <span class="string">'undefined'</span> || ngDevMode) && initNgDevMode();</span><br><span class="line"> <span class="keyword">let</span> ngComponentDef: <span class="built_in">any</span> = <span class="literal">null</span>;</span><br><span class="line"> <span class="comment">// 元数据可能具有需要解析的资源</span></span><br><span class="line"> maybeQueueResolutionOfComponentResources(<span class="keyword">type</span>, metadata);</span><br><span class="line"> <span class="comment">// 这里使用的功能与指令相同,因为这只是创建 ngFactoryDef 所需的元数据的子集</span></span><br><span class="line"> addDirectiveFactoryDef(<span class="keyword">type</span>, metadata);</span><br><span class="line"> <span class="built_in">Object</span>.defineProperty(<span class="keyword">type</span>, NG_COMP_DEF, {</span><br><span class="line"> <span class="keyword">get</span>: <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (ngComponentDef === <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">const</span> compiler = getCompilerFacade();</span><br><span class="line"> <span class="comment">// 根据元数据解析组件</span></span><br><span class="line"> <span class="keyword">if</span> (componentNeedsResolution(metadata)) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 异常处理</span></span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 创建编译组件需要的完整元数据</span></span><br><span class="line"> <span class="keyword">const</span> templateUrl = metadata.templateUrl || <span class="string">`ng:///<span class="subst">${<span class="keyword">type</span>.name}</span>/template.html`</span>;</span><br><span class="line"> <span class="keyword">const</span> meta: R3ComponentMetadataFacade = {</span><br><span class="line"> ...directiveMetadata(<span class="keyword">type</span>, metadata),</span><br><span class="line"> typeSourceSpan: compiler.createParseSourceSpan(<span class="string">'Component'</span>, <span class="keyword">type</span>.name, templateUrl),</span><br><span class="line"> template: metadata.template || <span class="string">''</span>,</span><br><span class="line"> preserveWhitespaces,</span><br><span class="line"> styles: metadata.styles || EMPTY_ARRAY,</span><br><span class="line"> animations: metadata.animations,</span><br><span class="line"> directives: [],</span><br><span class="line"> changeDetection: metadata.changeDetection,</span><br><span class="line"> pipes: <span class="keyword">new</span> Map(),</span><br><span class="line"> encapsulation,</span><br><span class="line"> interpolation: metadata.interpolation,</span><br><span class="line"> viewProviders: metadata.viewProviders || <span class="literal">null</span>,</span><br><span class="line"> };</span><br><span class="line"> <span class="comment">// 编译过程需要计算深度,以便确认编译是否最终完成</span></span><br><span class="line"> compilationDepth++;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (meta.usesInheritance) {</span><br><span class="line"> addDirectiveDefToUndecoratedParents(<span class="keyword">type</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 根据模板、环境和组件需要的元数据,来编译组件</span></span><br><span class="line"> ngComponentDef = compiler.compileComponent(angularCoreEnv, templateUrl, meta);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// 即使编译失败,也请确保减少编译深度</span></span><br><span class="line"> compilationDepth--;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (compilationDepth === <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 当执行 NgModule 装饰器时,我们将模块定义加入队列,以便仅在所有声明都已解析的情况下才将队列出队,并将其自身作为模块作用域添加到其所有声明中</span></span><br><span class="line"> <span class="comment">// 此调用运行检查以查看队列中的任何模块是否可以出队,并将范围添加到它们的声明中</span></span><br><span class="line"> flushModuleScopingQueueAsMuchAsPossible();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果组件编译是异步的,则声明该组件的 @NgModule 批注可以执行并在组件类型上设置 ngSelectorScope 属性</span></span><br><span class="line"> <span class="comment">// 这允许组件在完成编译后,使用模块中的 directiveDefs 对其自身进行修补</span></span><br><span class="line"> <span class="keyword">if</span> (hasSelectorScope(<span class="keyword">type</span>)) {</span><br><span class="line"> <span class="keyword">const</span> scopes = transitiveScopesFor(<span class="keyword">type</span>.ngSelectorScope);</span><br><span class="line"> patchComponentDefWithScope(ngComponentDef, scopes);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ngComponentDef;</span><br><span class="line"> },</span><br><span class="line"> ...</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译组件的过程可能是异步的(比如需要解析组件模板或其他资源的 URL)。如果编译不是立即进行的,<code>compileComponent</code>会将资源解析加入到全局队列中,并且将无法返回<code>ɵcmp</code>,直到通过调用<code>resolveComponentResources</code>解决了全局队列为止。</p><h3 id="编译过程中的元数据"><a href="#编译过程中的元数据" class="headerlink" title="编译过程中的元数据"></a>编译过程中的元数据</h3><p>元数据是有关类的信息,但它不是类的属性。因此,用于配置类的定义和行为的这些数据,不应该存储在该类的实例中,我们还需要在其他地方保存此数据。</p><p>在 Angular 中,编译过程产生的元数据,会使用<code>CompileMetadataResolver</code>来进行管理和维护,这里我们主要看指令(组件)相关的逻辑:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> CompileMetadataResolver {</span><br><span class="line"> <span class="keyword">private</span> _nonNormalizedDirectiveCache =</span><br><span class="line"> <span class="keyword">new</span> Map<Type, {annotation: Directive, metadata: cpl.CompileDirectiveMetadata}>();</span><br><span class="line"> <span class="comment">// 使用 Map 的方式来保存</span></span><br><span class="line"> <span class="keyword">private</span> _directiveCache = <span class="keyword">new</span> Map<Type, cpl.CompileDirectiveMetadata>(); </span><br><span class="line"> <span class="keyword">private</span> _summaryCache = <span class="keyword">new</span> Map<Type, cpl.CompileTypeSummary|<span class="literal">null</span>>();</span><br><span class="line"> <span class="keyword">private</span> _pipeCache = <span class="keyword">new</span> Map<Type, cpl.CompilePipeMetadata>();</span><br><span class="line"> <span class="keyword">private</span> _ngModuleCache = <span class="keyword">new</span> Map<Type, cpl.CompileNgModuleMetadata>();</span><br><span class="line"> <span class="keyword">private</span> _ngModuleOfTypes = <span class="keyword">new</span> Map<Type, Type>();</span><br><span class="line"> <span class="keyword">private</span> _shallowModuleCache = <span class="keyword">new</span> Map<Type, cpl.CompileShallowModuleMetadata>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _config: CompilerConfig, <span class="keyword">private</span> _htmlParser: HtmlParser,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _ngModuleResolver: NgModuleResolver, <span class="keyword">private</span> _directiveResolver: DirectiveResolver,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _pipeResolver: PipeResolver, <span class="keyword">private</span> _summaryResolver: SummaryResolver<<span class="built_in">any</span>>,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _schemaRegistry: ElementSchemaRegistry,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _directiveNormalizer: DirectiveNormalizer, <span class="keyword">private</span> _console: Console,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _staticSymbolCache: StaticSymbolCache, <span class="keyword">private</span> _reflector: CompileReflector,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _errorCollector?: ErrorCollector</span>) {}</span><br><span class="line"> <span class="comment">// 清除特定某个指令的元数据</span></span><br><span class="line"> clearCacheFor(<span class="keyword">type</span>: Type) {</span><br><span class="line"> <span class="keyword">const</span> dirMeta = <span class="keyword">this</span>._directiveCache.get(<span class="keyword">type</span>);</span><br><span class="line"> <span class="keyword">this</span>._directiveCache.delete(<span class="keyword">type</span>);</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 清除所有元数据</span></span><br><span class="line"> clearCache(): <span class="built_in">void</span> {</span><br><span class="line"> <span class="keyword">this</span>._directiveCache.clear();</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 加载 NgModule 中,已声明的指令和的管道</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> loadNgModuleDirectiveAndPipeMetadata(moduleType: <span class="built_in">any</span>, isSync: <span class="built_in">boolean</span>, throwIfNotFound = <span class="literal">true</span>):</span><br><span class="line"> <span class="built_in">Promise</span><<span class="built_in">any</span>> {</span><br><span class="line"> <span class="keyword">const</span> ngModule = <span class="keyword">this</span>.getNgModuleMetadata(moduleType, throwIfNotFound);</span><br><span class="line"> <span class="keyword">const</span> loading: <span class="built_in">Promise</span><<span class="built_in">any</span>>[] = [];</span><br><span class="line"> <span class="keyword">if</span> (ngModule) {</span><br><span class="line"> ngModule.declaredDirectives.forEach(<span class="function">(<span class="params">id</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> promise = <span class="keyword">this</span>.loadDirectiveMetadata(moduleType, id.reference, isSync);</span><br><span class="line"> <span class="keyword">if</span> (promise) {</span><br><span class="line"> loading.push(promise);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> ngModule.declaredPipes.forEach(<span class="function">(<span class="params">id</span>) =></span> <span class="keyword">this</span>._loadPipeMetadata(id.reference));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.all(loading);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 加载指令(组件)元数据</span></span><br><span class="line"> loadDirectiveMetadata(ngModuleType: <span class="built_in">any</span>, directiveType: <span class="built_in">any</span>, isSync: <span class="built_in">boolean</span>): SyncAsync<<span class="literal">null</span>> {</span><br><span class="line"> <span class="comment">// 若已加载,则直接返回</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>._directiveCache.has(directiveType)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> directiveType = resolveForwardRef(directiveType);</span><br><span class="line"> <span class="keyword">const</span> {annotation, metadata} = <span class="keyword">this</span>.getNonNormalizedDirectiveMetadata(directiveType)!;</span><br><span class="line"> <span class="comment">// 创建指令(组件)元数据</span></span><br><span class="line"> <span class="keyword">const</span> createDirectiveMetadata = <span class="function">(<span class="params">templateMetadata: cpl.CompileTemplateMetadata|<span class="literal">null</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> normalizedDirMeta = <span class="keyword">new</span> cpl.CompileDirectiveMetadata({</span><br><span class="line"> isHost: <span class="literal">false</span>,</span><br><span class="line"> <span class="keyword">type</span>: metadata.type,</span><br><span class="line"> isComponent: metadata.isComponent,</span><br><span class="line"> selector: metadata.selector,</span><br><span class="line"> exportAs: metadata.exportAs,</span><br><span class="line"> changeDetection: metadata.changeDetection,</span><br><span class="line"> inputs: metadata.inputs,</span><br><span class="line"> outputs: metadata.outputs,</span><br><span class="line"> hostListeners: metadata.hostListeners,</span><br><span class="line"> hostProperties: metadata.hostProperties,</span><br><span class="line"> hostAttributes: metadata.hostAttributes,</span><br><span class="line"> providers: metadata.providers,</span><br><span class="line"> viewProviders: metadata.viewProviders,</span><br><span class="line"> queries: metadata.queries,</span><br><span class="line"> guards: metadata.guards,</span><br><span class="line"> viewQueries: metadata.viewQueries,</span><br><span class="line"> entryComponents: metadata.entryComponents,</span><br><span class="line"> componentViewType: metadata.componentViewType,</span><br><span class="line"> rendererType: metadata.rendererType,</span><br><span class="line"> componentFactory: metadata.componentFactory,</span><br><span class="line"> template: templateMetadata</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">if</span> (templateMetadata) {</span><br><span class="line"> <span class="keyword">this</span>.initComponentFactory(metadata.componentFactory!, templateMetadata.ngContentSelectors);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 存储完整的元数据信息,以及元数据摘要信息</span></span><br><span class="line"> <span class="keyword">this</span>._directiveCache.set(directiveType, normalizedDirMeta);</span><br><span class="line"> <span class="keyword">this</span>._summaryCache.set(directiveType, normalizedDirMeta.toSummary());</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (metadata.isComponent) {</span><br><span class="line"> <span class="comment">// 如果是组件,该过程可能为异步过程,则需要等待异步过程结束后的模板返回</span></span><br><span class="line"> <span class="keyword">const</span> template = metadata.template !;</span><br><span class="line"> <span class="keyword">const</span> templateMeta = <span class="keyword">this</span>._directiveNormalizer.normalizeTemplate({</span><br><span class="line"> ngModuleType,</span><br><span class="line"> componentType: directiveType,</span><br><span class="line"> moduleUrl: <span class="keyword">this</span>._reflector.componentModuleUrl(directiveType, annotation),</span><br><span class="line"> encapsulation: template.encapsulation,</span><br><span class="line"> template: template.template,</span><br><span class="line"> templateUrl: template.templateUrl,</span><br><span class="line"> styles: template.styles,</span><br><span class="line"> styleUrls: template.styleUrls,</span><br><span class="line"> animations: template.animations,</span><br><span class="line"> interpolation: template.interpolation,</span><br><span class="line"> preserveWhitespaces: template.preserveWhitespaces</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">if</span> (isPromise(templateMeta) && isSync) {</span><br><span class="line"> <span class="keyword">this</span>._reportError(componentStillLoadingError(directiveType), directiveType);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 并将元数据进行存储</span></span><br><span class="line"> <span class="keyword">return</span> SyncAsync.then(templateMeta, createDirectiveMetadata);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 指令,直接存储元数据</span></span><br><span class="line"> createDirectiveMetadata(<span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 获取给定指令(组件)的元数据信息</span></span><br><span class="line"> getDirectiveMetadata(directiveType: <span class="built_in">any</span>): cpl.CompileDirectiveMetadata {</span><br><span class="line"> <span class="keyword">const</span> dirMeta = <span class="keyword">this</span>._directiveCache.get(directiveType)!;</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">return</span> dirMeta;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 获取给定指令(组件)的元数据摘要信息</span></span><br><span class="line"> getDirectiveSummary(dirType: <span class="built_in">any</span>): cpl.CompileDirectiveSummary {</span><br><span class="line"> <span class="keyword">const</span> dirSummary =</span><br><span class="line"> <cpl.CompileDirectiveSummary><span class="keyword">this</span>._loadSummary(dirType, cpl.CompileSummaryKind.Directive);</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">return</span> dirSummary;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,在编译过程中,不管是组件、指令、管道,还是模块,这些类在编译过程中的元数据,都使用<code>Map</code>来存储。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本节我们介绍了 Angular 中的装饰器和元数据,其中元数据用于描述类的定义和行为。</p><p>在 Angular 编译过程中,会使用<code>Map</code>的数据结构来维护和存储装饰器的元数据,并根据这些元数据信息来编译组件、指令、管道和模块等。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="http://st.inf.tu-dresden.de/files/teaching/ss14/cbse/slides/11-cbse-metamodelling.pdf" target="_blank" rel="noopener">11. Metadata, Metamodelling, and Metaprogramming</a></li><li><a href="http://nicholasjohnson.com/blog/how-angular2-di-works-with-typescript/" target="_blank" rel="noopener">How does the TypeScript Angular DI magic work?</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中随处可见的元数据,来进行介绍。</p>
</summary>
<category term="Angular源码" scheme="https://godbasin.github.io/categories/Angular%E6%BA%90%E7%A0%81/"/>
<category term="功能设计" scheme="https://godbasin.github.io/tags/%E5%8A%9F%E8%83%BD%E8%AE%BE%E8%AE%A1/"/>
</entry>
<entry>
<title>Angular框架解读--预热篇</title>
<link href="https://godbasin.github.io/2021/03/13/angular-design-0-prestart/"/>
<id>https://godbasin.github.io/2021/03/13/angular-design-0-prestart/</id>
<published>2021-03-13T12:23:31.000Z</published>
<updated>2021-03-13T12:36:06.988Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文作为背景和铺垫,先简单以我自身的了解来介绍一下 Angular 这个框架把吧。</p><a id="more"></a><h2 id="前端框架"><a href="#前端框架" class="headerlink" title="前端框架"></a>前端框架</h2><h3 id="三大前端“框架”"><a href="#三大前端“框架”" class="headerlink" title="三大前端“框架”"></a>三大前端“框架”</h3><p>虽然这几年前端的发展和变化十分迅猛,但被公认为前端“框架”的 Top3 位置却没有什么变化,依然是 Angular/React/Vue。</p><p>其中,React/Vue 专注于构建用户界面,在一定程度上来说为一个 Javascript 库;而 Angular 则提供了前端项目开发中较完整的解决方案。我们可以简单粗暴地这样认为:</p><figure class="highlight dsconfig"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">Angular </span>= <span class="string">React/</span><span class="string">Vue </span>+ 路由库(<span class="string">react-router/</span><span class="string">vue-router)</span> + 状态管理(<span class="string">Redux/</span><span class="string">Flux/</span><span class="string">Mobx/</span><span class="string">Vuex)</span> + 脚手架/构建(<span class="built_in">create-react-app/Vue</span> <span class="string">CLI/</span><span class="string">Webpack)</span> + ...</span><br></pre></td></tr></table></figure><h3 id="低热度的-Angular"><a href="#低热度的-Angular" class="headerlink" title="低热度的 Angular"></a>低热度的 Angular</h3><p>作为三大前端框架之一,Angular 在国内的热度实在是低。个人认为原因包括:</p><ul><li>AngularJS 到 Angular2 的断崖式升级,失去了很多开发者的信任</li><li>Angular 除了依赖注入(Provider/Service)、指令/管道等功能设计,在断崖升级时引入的 Typescript/Decorator/模块化组织/AOT/JIT 等新的能力,对当时大多数前端开发带来了不少的学习门槛</li><li>Angular 针对大型应用而引入的设计和功能,对于大多数前端应用来说无法物尽其用,反而增加了学习成本</li><li>Angular 提供了一整套完整的解决方案,反而不像 Vue/React 等库灵活,可以随意搭配其他的状态管理、构建库等(其实也是可以的,可能成本和门槛会高一些),显得笨重</li></ul><p>由于以上原因,大家在选框架的时候常常讨论的是用 React 还是 Vue,虽然同样作为热门框架,Angular 似乎在无形中就被大家排除在外。使用 Angular 框架的人越少,相关的中文相关的文档和教程也会很少,大家对它的了解和认知都容易不够全面。</p><p>其实,很多人会问我喜欢 React 还是 Vue,我总会告诉他们我喜欢 Angular,他们也总会觉得很奇怪哈哈哈哈。实际上,AngularJS 是我最早接触的一个前端框架,我也曾经使用过断崖式升级的 Angular2,它们带给我的除了很多未知的知识和领域,还有拓宽了我对前端编程的一些认知。我想,喜欢 Angular,也可能是因为有一些缘分在内的原因叭~</p><h3 id="我对-Angular-的理解"><a href="#我对-Angular-的理解" class="headerlink" title="我对 Angular 的理解"></a>我对 Angular 的理解</h3><p>Angular2 的出现时间大概在 2017 年前后,那会 React 的函数式编程正开始受到很多人追捧,而 Vue 也开始进入大家的视野中。但 Angular2 带来的技术和设计很是前卫,以至于对很多人来说门槛太高。</p><p>但是,如今 2021 年了,我们再来回看一下,Angular 框架中使用到的很多技术和设计,都渐渐地被更多的人在使用了。</p><p>其中最火的便是 Typescript,显然,如今对大多数前端开发来说,Typescript 都是需要掌握的。在 Angular 之后,React、Vue 也都支持了 Typescript,Vue3 更是直接使用 Typescript 来重构了。</p><p>除此之外,模块化、AOT/JIT、依赖注入等设计,以及 Rxjs 、元数据(<code>Reflect.metadata</code>)的引入等,也被更多的产品和工具库使用。当然,这里并不是说这些产品和工具是参考了 Angular。Angular 中的这些设计理念,大多数也并不是由 Angular 第一个提出的,但 Angular 大概是第一个在前端框架中(甚至是在前端领域中)将它们毫无违和感地一起引入并使用的。</p><p>这样的趋势,我认为很大的原因是前端应用的规模在不断变大,这也是因为前端的技术栈在不断拓展,负责的领域也逐渐在扩大。前端应用慢慢地变得复杂,比如 VsCode 编辑器、在线文档编辑这些,项目本身的复杂度和规模都不小,而这样大型的项目里肯定需要往模块化组织的方向发展,那么想必各个模块间的依赖耦合会很严重,因此依赖注入便是一个很好的方式来管理,VsCode 便是这样做的。</p><p>当然,即使在未来,大型项目在所有前端项目中的占比肯定也不至于很高,但大型项目如何设计和管理这块领域对前端来说依然比较陌生。我们可以借助常见的后台系统架构设计来进行参考和反思,比如微服务、领域驱动设计、职责驱动设计等。但这些终究是设计思想,如何才能很好地落地,对前端开发都是不小的考验。</p><p>我接触过各式各样的项目,而当这些项目在面对项目的规模变大的时候,虽然新人的加入、每个人都按照自己的想法去开发,最终总会变得难以维护,历史债务十分严重。而 Angular 则是唯一一个能限制开发的自由发挥的,可以让经验不足和经验丰富的开发都写出一样易维护的代码。</p><p>回归主题,既然 Angular 提供了大型前端应用的完整解决方案,那么我们不妨多些对它的学习和了解,当我们真正遇到问题的时候,便多了一个可落地方案的参考,这也是为什么我们要不断汲取新知识和技术的原因。</p><h2 id="Angular-框架解读"><a href="#Angular-框架解读" class="headerlink" title="Angular 框架解读"></a>Angular 框架解读</h2><p>源码阅读对很多人来说,都是一件挑战很大的事情,对我来说也一样。</p><p>虽然我有较多地阅读过 Vue 的源码(可参考<a href="http://www.godbasin.com/vue-ebook/" target="_blank" rel="noopener">《深入理解 Vue.js 实战》</a>这本书),但前提是我对这个框架有足够多的理解和使用经验,在尝试介绍和解说时,也更是倾向使用者的角度来进行。而对于 React,则是因为理解和使用经验的缺乏,未曾有机会深入地去了解它,但也有阅读过写得不错的源码解读(可参考<a href="https://github.com/BetaSu/just-react" target="_blank" rel="noopener">《React 技术揭秘》</a>一书)。</p><p>对于阅读源码来说,最好的方式便是从已知的理解和认知中开始。阅读源码,并不是为了熟悉掌握源码本身,更是为了掌握其中的一些值得借鉴的思考方式和设计,因此我也尽可能减少代码占据的篇幅,使用自己理解后的方式来进行描述。</p><p>那么,后面我会从自己认为最值得参考和学习的地方开始,一点点学习其中的精华。以我当前有限的认知,大概会包括以下内容:</p><ul><li>依赖注入整体框架的设计</li><li>组件设计与管理</li><li>Provider 与 Service 的设计</li><li>NgModule 模块化组织(多级/分层)的设计</li><li>模板引擎/模板编译过程的整体设计</li><li>Zone 设计:提升计算速度</li><li>JIT/AOT 设计</li><li>元数据设计:(<code>Reflect.metadata</code>)的引入和使用思考</li><li>响应式编程:Rxjs 的引入和使用思考</li></ul><p>在时隔 3 年之后再次接触 Angular,还是直接以阅读源码的方式来进行,对我来说是个不小的挑战。但这些年来,我也一直在尝试做各种不同的新的事情,如果因为觉得困难而放弃,那么这个天花板便是我自身,而不是什么“环境所迫”、“没有时间”这样的借口。或许我会写得很慢,但我依然希望自己能一点点去细细钻研,也希望至少以上的内容能最终掌握。</p><p>这是一个大工程,因为我写下这篇文章来给自己预热,也希望能打打气,更是尝试立下一个 FLAG 吧。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>这是一篇从我个人的理解出发的文章,同样的这个系列也会以我一个人这样局限的角度来出发进行介绍。因此它们或许可能存在片面和局限的时候,但我希望即使是这样的内容,也能给你们带来一些思考和收获。</p><p>分享和交流,并不是为了各自的理由而争执,而是为了弥补自己看不到的一面,共同进步,不是吗?</p>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文作为背景和铺垫,先简单以我自身的了解来介绍一下 Angular 这个框架把吧。</p>
</summary>
<category term="Angular源码" scheme="https://godbasin.github.io/categories/Angular%E6%BA%90%E7%A0%81/"/>
<category term="功能设计" scheme="https://godbasin.github.io/tags/%E5%8A%9F%E8%83%BD%E8%AE%BE%E8%AE%A1/"/>
</entry>
<entry>
<title>在线文档的网络层开发思考--依赖关系梳理</title>
<link href="https://godbasin.github.io/2021/02/27/network-design-dependency-decoupling/"/>
<id>https://godbasin.github.io/2021/02/27/network-design-dependency-decoupling/</id>
<published>2021-02-27T10:40:31.000Z</published>
<updated>2021-02-27T10:54:13.646Z</updated>
<content type="html"><![CDATA[<p>最近在负责通用网络层的设计和开发,会记录该过程中的一些思考,本文主要介绍接入层设计过程中的一些依赖关系,以及处理这些依赖关系的一些思考。</p><a id="more"></a><p>在<a href="https://godbasin.github.io/2021/01/23/network-design-responsibility-driven-design/">上一篇</a>文章中,我尝试使用职责驱动设计来重新梳理了接入层的职责对象,最终得到了这样的依赖关系图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-8.jpg" alt></p><p>这里的依赖关系表示得很简单,实际上这样简单的表示是无法完成代码开发的,我们还需要根据每个对象的职责将它们之间的协作方式整理出来,可以通过接口或者 UML 图的方式来进行。</p><h2 id="依赖关系梳理"><a href="#依赖关系梳理" class="headerlink" title="依赖关系梳理"></a>依赖关系梳理</h2><p>技术方案设计离不开业务,我们开发的很多工具和 SDK 最终也是服务与业务,因此我们首先需要梳理出网络层与业务侧的一些依赖关系,从而可得到更加明确的职责范围。</p><h3 id="梳理网络层与业务侧依赖"><a href="#梳理网络层与业务侧依赖" class="headerlink" title="梳理网络层与业务侧依赖"></a>梳理网络层与业务侧依赖</h3><p>原先的网络层由于历史原因与业务中其他模块耦合严重,其中网络层的代码中对其他模块(包括数据层、离线模块、worker 模块等)的直接引用以及使用事件通信多达 50+处。因此,如果希望重构后的网络层能正常在业务中使用,我们首先需要将相关依赖全部梳理出来,确认是否可通过适配层的方式进行解耦,让网络层专注于自身的职责功能。</p><p>经过梳理,我们整理出网络层的与业务层的主要依赖关系,包括:</p><ol><li>业务侧为主动方时:</li></ol><ul><li>业务侧将数据提交到网络层</li><li>业务侧可控制网络层工作状态,可用于预防异常的情况</li><li>业务侧主动获取网络层自身的一些状态,包括网络层是否正确运行、网络层状态(在线/离线)等</li></ul><ol start="2"><li>业务侧为被动方时:</li></ol><ul><li>网络层告知业务侧,需要进行数据冲突处理</li><li>网络层告知业务侧服务端的最新状态,包括数据是否递交成功、是否有新的服务端消息等</li><li>网络层告知业务侧自身的一些状态变更,包括网络层状态变更(异常/挂起)、网络层工作是否存在异常等</li></ul><p>除此之外,网络层初始化也依赖一些业务侧的数据,包括初始版本信息、用户登录态、文档 ID 等等。</p><p>到这里,我们可以根据这些依赖关系,简化网络层与业务侧的关系:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-1.jpg" alt></p><p>能看到,简化后的网络层与业务侧关系主要包括三种:</p><ol><li>业务侧初始化网络层。</li><li>业务侧给网络层提交数据,以及控制网络层的工作状态。</li><li>业务侧监听网络层的状态变更。</li></ol><p>前面我们也说了,业务侧与网络层的协作主要通过接入层的总控制器来完成,也就是说总控制器的职责和协作方式包括:</p><ol><li>初始化整个网络层,创建网络层运行需要的各个协作对象,在这里总控制器也可视作创建者(creator)。</li><li>通过提供接口的方式,对业务层提供数据提交(<code>addData()</code>)和控制网络层状态(<code>pause()</code>/<code>resume()</code>/<code>shutdown()</code>)的方法。</li><li>通过提供事件监听的方式,对业务层提供网络层的各种状态变更(<code>onNetworkChange()</code>/<code>onDataCommitSuccess()</code>/<code>onDataCommitError()</code>/<code>onNewData()</code>)。</li></ol><p>具体网络层中总控制器是如何调度其他对象进行协作的,这些细节不需要暴露给业务侧。在对齐了业务侧的需要之后,我们再来看看具体网络层的细节。</p><h2 id="总控制器的职责梳理"><a href="#总控制器的职责梳理" class="headerlink" title="总控制器的职责梳理"></a>总控制器的职责梳理</h2><p>对业务侧来说,它只关注和网络层的协作,不关注具体网络层中接入层和连接层的关系。而对于接入层来说,其实它对连接层有直接的层级关系,因此这里我们将连接层以及服务端视作一个单独的职责对象:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-9.jpg" alt></p><p>实际上这些模块之间的依赖关系比这些还要复杂得多,比如发送数据控制器和接受数据控制器都会直接依赖连接层。为了方便描述,这里我们就不纠结这些细节了。</p><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>前面也说了,总控制器需要负责整个网络层的初始化,因此它需要控制各个职责对象的创建。那么,图中发送数据控制器和接受数据控制器对其他对象的依赖,可以通过初始化控制器对象时注入的方式来进行控制。</p><p>如果是注入的方式,则这样的依赖关系可描述为对接口的依赖,我们用虚线进行标记:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-10.jpg" alt></p><p>其中虚线的地方,都可以理解为初始化时需要注入的依赖对象。初始化相关的代码大致会长这样:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">NetworkController</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span>(options: INetworkControllerOptions) {</span><br><span class="line"> <span class="keyword">this</span>.init();</span><br><span class="line"> }</span><br><span class="line"> init() {</span><br><span class="line"> <span class="keyword">this</span>.versionManager = <span class="keyword">new</span> VersionManager(); <span class="comment">// 版本管理</span></span><br><span class="line"> <span class="keyword">this</span>.connectLayer = <span class="keyword">new</span> ConnectLayer(); <span class="comment">// 连接层</span></span><br><span class="line"> <span class="keyword">this</span>.netWorkManager = <span class="keyword">new</span> NetWorkManager(); <span class="comment">// 网络状态管理</span></span><br><span class="line"> <span class="keyword">this</span>.taskListManager = <span class="keyword">new</span> TaskListManager(<span class="keyword">this</span>.versionManager); <span class="comment">// 任务队列管理</span></span><br><span class="line"> <span class="keyword">this</span>.dataListManager = <span class="keyword">new</span> DataListManager(); <span class="comment">// 待提交数据队列</span></span><br><span class="line"> <span class="keyword">this</span>.sendDataController = <span class="keyword">new</span> SendDataController(</span><br><span class="line"> <span class="keyword">this</span>.taskListManager,</span><br><span class="line"> <span class="keyword">this</span>.dataListManager</span><br><span class="line"> ); <span class="comment">// 发送数据控制器</span></span><br><span class="line"> <span class="keyword">this</span>.receiveDataController = <span class="keyword">new</span> ReceiveDataController(</span><br><span class="line"> <span class="keyword">this</span>.taskListManager,</span><br><span class="line"> <span class="keyword">this</span>.dataListManager,</span><br><span class="line"> <span class="keyword">this</span>.netWorkManager</span><br><span class="line"> ); <span class="comment">// 接受数据控制器</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里虽然我们传入了实例对象,但在对象内部,依赖的对象除了是实例,还可以是抽象的接口。</p><h4 id="使用依赖倒置进行依赖解耦"><a href="#使用依赖倒置进行依赖解耦" class="headerlink" title="使用依赖倒置进行依赖解耦"></a>使用依赖倒置进行依赖解耦</h4><p>依赖倒置原则有两个,其中包括了:</p><ol><li>高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。</li><li>抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口。</li></ol><p>以<code>SendDataController</code>为例,它依赖<code>TaskListManager</code>其实主要是依赖的添加任务的接口<code>addTask()</code>,依赖<code>DataListManager</code>则是依赖添加数据<code>pushData()</code>、取出数据<code>shiftData()</code>,则我们可以表达为:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> ITaskListManagerDependency {</span><br><span class="line"> addTask: <span class="function">(<span class="params">task: BaseTask</span>) =></span> <span class="built_in">void</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">interface</span> IDataListManagerDependency {</span><br><span class="line"> pushData: <span class="function">(<span class="params">data: LocalData</span>) =></span> <span class="built_in">void</span>;</span><br><span class="line"> shiftData: <span class="function"><span class="params">()</span> =></span> LocalData;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">class</span> SendDataController {</span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> taskListManagerDependency: ITaskListManagerDependency,</span></span><br><span class="line"><span class="params"> dataListManagerDependency: IDataListManagerDependency</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="comment">// 相关依赖可以保存起来,在需要的时候使用</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实际上,我们可以给每个对象提供自身的接口描述,这样其他对象中可以直接<code>import</code>同一份接口也是可以的,管理和调整会比较方便。</p><p>如果项目中有完善的依赖注入框架,则可以使用项目中的依赖注入体系。在我们这个例子里,总控制器充当了依赖注入的控制角色,而具体其中的各个对象之间,实现了基于抽象接口的依赖,成功了进行了解耦。依赖注入在大型项目中比较常见,对于各个模块间的依赖关系管理很实用。</p><h3 id="提供接口和事件监听"><a href="#提供接口和事件监听" class="headerlink" title="提供接口和事件监听"></a>提供接口和事件监听</h3><p>除了初始化相关,总控制器的职责还包括对业务层提供接口和事件监听,其中接口中会依赖具体职责对象的协作:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">NetworkController</span> </span>{</span><br><span class="line"> <span class="comment">// 提供的接口</span></span><br><span class="line"> addData(data: ILocalData) {</span><br><span class="line"> <span class="keyword">this</span>.sendDataController.addData(data);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> pause() {</span><br><span class="line"> <span class="keyword">this</span>.taskListManager.pause();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> resume() {</span><br><span class="line"> <span class="keyword">this</span>.taskListManager.resume();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> shutdown() {</span><br><span class="line"> <span class="keyword">this</span>.taskListManager.shutdown();</span><br><span class="line"> <span class="keyword">this</span>.connectLayer.shutdown();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在最初的设计中,我们的状态变更这些也是通过注册回调的方式进行设计的:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">interface INetworkControllerOptions {</span><br><span class="line"> <span class="comment">// 其他参数</span></span><br><span class="line"> onNetworkChange: <span class="function">(<span class="params">newStatus: NetWorkStatus</span>) =></span> <span class="keyword">void</span>,</span><br><span class="line"> onDataCommitSuccess: <span class="function">(<span class="params">data: LocalData</span>) =></span> <span class="keyword">void</span></span><br><span class="line"> onDataCommitError: <span class="function">(<span class="params">data: LocalData</span>) =></span> <span class="keyword">void</span></span><br><span class="line"> onNewData: <span class="function">(<span class="params">data: ServerData</span>) =></span> <span class="keyword">void</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">NetworkController</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span>(options: INetworkControllerOptions) {</span><br><span class="line"> <span class="comment">// 需要将各个接口实现保存下来</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种方式意味着我们需要将这些接口实现保存下来,并传入到各个对象内部分别在恰当的时机进行调用,调用的时候还需要关注是否出现异常,同样以<code>SendDataController</code>为例:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">interface ICallbackDependency {</span><br><span class="line"> onDataCommitSuccess?: <span class="function">(<span class="params">data: LocalData</span>) =></span> <span class="keyword">void</span></span><br><span class="line"> onDataCommitError?: <span class="function">(<span class="params">data: LocalData</span>) =></span> <span class="keyword">void</span></span><br><span class="line">}</span><br><span class="line">interface ITaskListManagerDependency {</span><br><span class="line"> addTask: <span class="function">(<span class="params">task: BaseTask</span>) =></span> <span class="keyword">void</span>;</span><br><span class="line">}</span><br><span class="line">interface IDataListManagerDependency {</span><br><span class="line"> pushData: <span class="function">(<span class="params">data: LocalData</span>) =></span> <span class="keyword">void</span>;</span><br><span class="line"> shiftData: <span class="function"><span class="params">()</span> =></span> LocalData;</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SendDataController</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span>(</span><br><span class="line"> taskListManagerDependency: ITaskListManagerDependency,</span><br><span class="line"> dataListManagerDependency: IDataListManagerDependency,</span><br><span class="line"> // 在初始化的时候需要通过注入的方式传进来</span><br><span class="line"> callbackDependency: ICallbackDependency,</span><br><span class="line"> ) {}</span><br><span class="line"></span><br><span class="line"> handleDataCommitSuccess(data: LocalData) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 还该函数还可能为空</span></span><br><span class="line"> <span class="keyword">this</span>.callbackDependency.onDataCommitSuccess?.(data);</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="comment">// 使用的时候还需要注意异常问题</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>除此之外,这种方式还导致了业务侧在使用的时候,初始化就要传入很多的接口实现:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> netWorkLayer = <span class="keyword">new</span> NetworkController({</span><br><span class="line"> <span class="comment">// 其他参数</span></span><br><span class="line"> otherOptions: {},</span><br><span class="line"> onNetworkChange: () {</span><br><span class="line"> <span class="comment">// 网络状态变更处理</span></span><br><span class="line"> },</span><br><span class="line"> onDataCommitSuccess: () {</span><br><span class="line"> <span class="comment">// 提交数据成功处理</span></span><br><span class="line"> },</span><br><span class="line"> onDataCommitError: () {</span><br><span class="line"> <span class="comment">// 提交数据失败处理</span></span><br><span class="line"> },</span><br><span class="line"> onNewData: () {</span><br><span class="line"> <span class="comment">// 服务端新数据处理</span></span><br><span class="line"> },</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>可以看到,业务侧中初始化网络层的代码特别长(传入了 20 多个方法),实际上在不同的业务中这些接口可能是不必要的。</p><h4 id="使用事件驱动进行依赖解耦"><a href="#使用事件驱动进行依赖解耦" class="headerlink" title="使用事件驱动进行依赖解耦"></a>使用事件驱动进行依赖解耦</h4><p>在这里,我们使用了事件处理模型-观察者模式。事件驱动其实常常在各种系统设计中会用到,可以解耦目标对象和它的依赖对象。目标只需要通知它的依赖对象,具体怎么处理,依赖对象自己决定。</p><p>事件监听的实现,参考了<a href="https://godbasin.github.io/2020/07/05/vscode-event/">VsCode 的事件系统设计</a>的做法,比如在<code>SendDataController</code>中:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SendDataController</span> </span>{</span><br><span class="line"> private readonly _onDataCommitSuccess = <span class="keyword">new</span> Emitter<LocalData>();</span><br><span class="line"> readonly onDataCommitSuccess: Event<LocalData> = <span class="keyword">this</span>._onDataCommitSuccess.event;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span>(</span><br><span class="line"> taskListManagerDependency: ITaskListManagerDependency,</span><br><span class="line"> dataListManagerDependency: IDataListManagerDependency,</span><br><span class="line"> // 在初始化的时候需要通过注入的方式传进来</span><br><span class="line"> callbackDependency: ICallbackDependency,</span><br><span class="line"> ) {}</span><br><span class="line"></span><br><span class="line"> handleDataCommitSuccess(data: LocalData) {</span><br><span class="line"> <span class="keyword">this</span>._onDataCommitSuccess.fire(data);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在总控制器中,可以同样通过事件监听的方式传递出去:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">NetworkController</span> </span>{</span><br><span class="line"> <span class="comment">// 提供的事件</span></span><br><span class="line"> private readonly _onNetworkChange = <span class="keyword">new</span> Emitter<NetWorkStatus>();</span><br><span class="line"> readonly onNetworkChange: Event<NetWorkStatus> = <span class="keyword">this</span>._onNetworkChange.event;</span><br><span class="line"></span><br><span class="line"> private readonly _onDataCommitSuccess = <span class="keyword">new</span> Emitter<LocalData>();</span><br><span class="line"> readonly onDataCommitSuccess: Event<LocalData> = <span class="keyword">this</span>._onDataCommitSuccess.event;</span><br><span class="line"></span><br><span class="line"> private readonly _onNewData = <span class="keyword">new</span> Emitter<ServerData>();</span><br><span class="line"> readonly onNewData: Event<ServerData> = <span class="keyword">this</span>._onNewData.event;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span>(options: INetworkControllerOptions) {</span><br><span class="line"> <span class="keyword">this</span>.init();</span><br><span class="line"> <span class="keyword">this</span>.initEvent();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> initEvent() {</span><br><span class="line"> <span class="comment">// 监听 SendDataController 的事件,并触发自己的事件</span></span><br><span class="line"> <span class="keyword">this</span>.sendDataController.onDataCommitSuccess(<span class="function"><span class="params">data</span> =></span> {</span><br><span class="line"> <span class="keyword">this</span>._onDataCommitSuccess.fire(data);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用事件监听的方式,业务方就可以在需要的地方再进行监听了:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> netWorkLayer = <span class="keyword">new</span> NetworkController({</span><br><span class="line"> <span class="comment">// 其他参数</span></span><br><span class="line"> otherOptions: {},</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="comment">// 网络状态变更处理</span></span><br><span class="line">netWorkLayer.onNetworkChange(<span class="function"><span class="params">()</span> =></span> {});</span><br><span class="line"><span class="comment">// 服务端新数据处理</span></span><br><span class="line">netWorkLayer.onNewData(<span class="function"><span class="params">()</span> =></span> {});</span><br></pre></td></tr></table></figure><p>到这里,我们可以简单实现了总控制器的职责,也通过接口和事件监听的方式提供了与外界的协作方式,简化了业务侧的使用过程。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>在本文中,主要根据业务侧与网络层的依赖关系,清晰地梳理出总控制器的职责和协作方式,并尝试对其中的依赖关系进行解耦。</p><p>而具体到网络层中每个对象的设计和实现,也都是可以通过接口的方式提供给外部使用某些功能、通过事件监听的方式提供给外部获取状态变更。而恰当地使用依赖倒置原则和事件驱动的方式,可以有效地解耦对象间的依赖。</p>]]></content>
<summary type="html">
<p>最近在负责通用网络层的设计和开发,会记录该过程中的一些思考,本文主要介绍接入层设计过程中的一些依赖关系,以及处理这些依赖关系的一些思考。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>在线文档的网络层开发思考--职责驱动设计</title>
<link href="https://godbasin.github.io/2021/01/23/network-design-responsibility-driven-design/"/>
<id>https://godbasin.github.io/2021/01/23/network-design-responsibility-driven-design/</id>
<published>2021-01-23T05:35:32.000Z</published>
<updated>2021-01-23T05:46:31.447Z</updated>
<content type="html"><![CDATA[<p>最近在负责通用网络层的设计和开发,会记录该过程中的一些思考,本文主要介绍职责驱动设计,以及它在网络层设计中的一些思考。</p><a id="more"></a><p>之前有整理过<a href="https://godbasin.github.io/2020/08/23/online-doc-network/">《在线文档的网络层设计思考》</a>一文,其中有较完整地介绍了网络层的一些职责,包括:</p><ul><li>校验数据合法性</li><li>本地数据准确的提交给后台:包括有序递交和按序升版</li><li>协同数据正确处理后分发给数据层:包括本地未递交数据与服务端协同数据的冲突处理和协同数据的按序应用</li></ul><p>在最初的想法中,我认为的网络层整体设计大概如下:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_5.png" alt="图6"></p><p>这是一个特别粗略的设计,其中有不少问题:</p><ol><li>连接层的职责主要是与服务端的通信,因此房间管理、消息队列等逻辑不应该放在连接层中。</li><li>接入层的模块职责划分不清,各个功能职责耦合在一起。</li><li>网络层与业务的依赖关系不清晰,如果需要实际进行开发,则必须梳理清楚这些关系。</li></ol><h2 id="接入层设计"><a href="#接入层设计" class="headerlink" title="接入层设计"></a>接入层设计</h2><p>我们看到原本的接入层设计大概是这样的:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-2.jpg" alt></p><p>其中,发送数据的模块其实还包含着一个数据队列,而同时网络层的整体状态也看不到在哪里维护,导致这些问题主要是因为模块的职责划分不清晰。</p><h3 id="职责驱动设计"><a href="#职责驱动设计" class="headerlink" title="职责驱动设计"></a>职责驱动设计</h3><p>在面向对象编程中,有一种设计模式叫职责驱动设计(Responsibility-Driven Design,简称 RDD),最典型的就是“客户端-服务端”模型。职责驱动设计于 1990 年构想,是从将对象视为[数据+算法]到将对象视为[角色+职责]的转变。</p><p>驱动设计的概念或许大家都很熟悉:</p><ul><li>测试驱动开发(Test-driven Development,简称 TDD)讨论在编写生产代码之前先编写测试</li><li>数据驱动开发(Data-Driven Development)讨论在数据功能中定义处理策略</li><li>事件驱动开发(Event-Driven Programming)讨论在基于事件的程序中定义处理策略</li><li>领域驱动设计(Domain-Driven Design,简称 DDD)谈论通过使用通用语言来解决领域问题</li></ul><p>其中,在大型复杂系统设计中流行的领域驱动设计,主要是从业务领域的角度来对系统进行领域划分和建模。相对的,职责驱动设计(RDD)则可用于从系统内部的角度来进行职责划分、模块拆分以及协作方式。</p><p>在基于职责的模型中,对象扮演特定角色,并在应用程序体系结构中占据公认的位置。整个应用程序可视作一个运行平稳的对象社区,每个对象都负责工作的特定部分。每个对象分配特定的职责,对象之间以明确定义的方式协作,通过这种方式构建应用程序的协作模型。</p><h3 id="GRASP"><a href="#GRASP" class="headerlink" title="GRASP"></a>GRASP</h3><p>要给类和对象分配责任,可以参考 GRASP(General Responsibility Assignment Software Patterns)原则,其中使用到的模式有:控制器(controller)、创建者(creator)和信息专家(information expert);使用到的原理包括:间接性(indirection)、低耦合(low coupling)、高内聚(high cohesion)、多态(polymorphism)、防止变异(protected variations)和纯虚构(pure fabrication)。</p><p>这里面有很多都是大家开发过程中比较熟悉的概念,我来进行简单的介绍:</p><ol><li>信息专家:在职责分配过程中,我们会将某个职责分配给软件系统中的某个对象类,它拥有实现这个职责所必须的信息。我们称这个对象类叫“信息专家”。</li><li>创建者:创建者帮助我们创建新对象,它决定了如何创建这些对象,比如使用工厂方法和抽象工厂。</li><li>控制器:控制器是一种将工作委派给应用程序适当部分的服务,主要用于将职责进行分配,比如常见的 MVC 架构模式中的控制器。</li><li>低耦合、高内聚:每个软件系统在其模块和类之间都有关系和依赖性,耦合是衡量软件组件如何相互依赖的一种方法。低耦合基于抽象,使我们的系统更具模块化,不相关的事物不应相互依赖;高内聚则意味着对象专注于单一职责。低耦合和高内聚是每个设计良好的系统的目标。</li><li>多态:用于表示具有不同行为的相关类,使用抽象而不是特定的具体实现。</li><li>防止变异:可理解为封装,将细节封装在内部。如果内部表示或行为发生了变化,保持其公共接口的不变。</li><li>纯虚构:为了保持良好的耦合和内聚,捏造业务上不存在的对象来承担职责。</li></ol><p>其实,RDD 本身的设计具备更多的角色,包括服务提供商、接口、信息持有人、控制器、协调员、结构师;也具备更多的职责分配原则和模式,通常包括:</p><ul><li>将信息保存在一个地方,比如“单点原则”</li><li>保持较小的职责,比如“得墨忒耳定律(Law of Demeter)-最少的知识原理”</li><li>包装相关的操作,比如“Whole Value Object”</li><li>仅使用需要的内容,比如“接口隔离原则”</li><li>一致的职责,比如“单一职责原则”</li><li>等等</li></ul><p>我们来看看,在网络层中是否可以使用职责驱动的方式来得到更好的设计。</p><h2 id="接入层职责划分"><a href="#接入层职责划分" class="headerlink" title="接入层职责划分"></a>接入层职责划分</h2><p><a href="https://godbasin.github.io/2020/08/23/online-doc-network/">上一篇文章中</a>我也有介绍,在线文档中从后台获取的数据到前端的展示,大概可以这么进行分层:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_0.png" alt></p><p>其实当我们在给系统分层、分模块的时候,很多时候都会根据职责进行划分,比如在这里我们划分成了:</p><ul><li>网络层:负责与服务端的数据提交、接收等处理</li><li>数据层:负责数据的处理</li><li>渲染层:负责界面的渲染</li></ul><p>这是很粗略的划分,实际上关于网络层的数据如何更新到数据层,数据层的变更又如何通知给渲染层,这些模块之间是有很多依赖关系的。如果我们只做最简单的划分,而不把职责、协作方式等都定义清楚,很可能到后期就会变成 A 模块里直接调用 B 模块,B 模块里也直接调用 A、C、D 模块,或者是全局事件满天飞的情况。</p><p>关于模块与模块间的耦合问题,可以后面有空再讨论,这里我们先回到网络层的设计中。</p><h3 id="按职责拆分对象"><a href="#按职责拆分对象" class="headerlink" title="按职责拆分对象"></a>按职责拆分对象</h3><p>上面说的有点多,我们再来回顾下之前的接入层设计:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-2.jpg" alt></p><p>可以看到,发送数据的模块中,夹杂着补拉版本的工作,实际上里面还需要维护一个用于按需提交的数据队列;接受数据的模块中,也同样存在着与业务逻辑严重耦合的冲突处理和应用协同等工作。在这样的设计中,各个对象之间的职责并不清晰,也存在相互之间的耦合甚至大鱼吃小鱼的情况。</p><p>根据 RDD,我们先来根据职责划分出可选的对象:</p><ul><li>提交数据队列管理器:负责业务侧提交数据的管理</li><li>网络状态管理器:负责整个网络层的网络状态管理</li><li>版本管理器:负责网络层的版本管理/按序升版</li><li>发送数据管理器:负责接收来自业务侧的数据</li><li>接受数据管理器:负责接收来自连接层(服务端)的数据</li></ul><p>按照职责拆分后,我们的网络层模块就很清晰了:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-3.jpg" alt></p><p>除了这些,还有提交数据队列中的数据、来自连接层(服务端)的数据等,也都可以作为候选对象:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-4.jpg" alt></p><p>如果按照 GRASP 设计原则,这些都应该是信息专家(information expert),负责具体的某个职责。如果你仔细观察,会发现对比最初的设计,任务队列被丢掉了,因为它没有恨明确的职责划分。但是它真的不需要存在吗?我们继续来看看。</p><h3 id="职责对象间的边界"><a href="#职责对象间的边界" class="headerlink" title="职责对象间的边界"></a>职责对象间的边界</h3><p>前面也说过,如果我们只对系统进行职责划分,而不定义清楚对象之间的边界、协作方式,那么实际上我们并没有真正意义上地完成系统设计这件事。</p><p>在这里,我们根据职责划分简单地画出了各个对象间的依赖关系:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-5.jpg" alt></p><p>其实各个对象间的依赖关系远比这复杂,因此我们无法很清晰地解耦出各个对象间的依赖关系。此外,不管是业务侧还是连接层(服务端),都跟内部的具体某个对象有直接的依赖关系,这意味着外部模块依赖了内部的具体实现,不符合封装的设计,违反了接口隔离原则和防止变异(protected variations)原则。</p><p>为了解决这些情况,我们可以拆分出控制器来进行职责分配,以及使用纯虚构(pure fabrication)来让这些信息专家保持保持良好的耦合和内聚。</p><h3 id="拆分出控制器"><a href="#拆分出控制器" class="headerlink" title="拆分出控制器"></a>拆分出控制器</h3><p>其实在上述的职责对象划分中,有两个管理器的职责并没有很明确:发送数据管理器和接受数据管理器。实际上,它们扮演的角色应该更倾向于控制器:</p><ul><li>发送数据控制器:负责接收来自业务侧的数据,并提交到连接层(服务端)</li><li>接受数据控制器:负责接收来自连接层(服务端)的数据,并最终应用到业务侧</li></ul><p>为了达到真正的控制器职责,发送数据控制器不仅需要将数据提交到连接层(服务端),也需要关注最终提交成功还是失败;接受数据控制器不仅需要接收来自连接层(服务端)的数据,还需要根据数据的具体内容,确保将数据正确地传递给业务侧。</p><p>因此,与业务侧和连接层(服务端)的依赖关系,都转接到发送数据控制器和接受数据控制器中:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-6.jpg" alt></p><p>但其实这样也依然存在外层对象依赖具体的实现的情况,我们可以添加个总控制器,来专门对接业务侧和连接层(服务端):</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-7.jpg" alt></p><p>来自业务侧的提交数据,总控制器会交给发送数据控制器进行处理,包括添加到待提交数据队列、提交成功/失败的处理等;来自服务端的消息,总控制器则会交给接受数据控制器进行处理,包括版本相关的数据进行冲突处理、更新版本等等,最终也会通过总控制器同步给业务侧。</p><p>我们可以看到,通过控制器的加入,各个职责对象(信息专家)之间不再存在直接的依赖关系,相互之间的联系都是通过控制器来进行管理的,这样它们就可以保持单一的职责关系,也可以专注于与控制器的协作方式。</p><h3 id="使用纯虚构"><a href="#使用纯虚构" class="headerlink" title="使用纯虚构"></a>使用纯虚构</h3><p>前面说过,纯虚构模式是为了保持良好的耦合和内聚,捏造业务上不存在的对象来承担职责。其实在上面我们添加了总控制器,也有用到了纯虚构。</p><p>那么现在还存在什么问题呢?在这里不管是本地数据提交完毕,还是服务端新数据的推送,发送数据控制器和接受数据控制器都会对版本管理进行更新。但实际上版本需要按序升版,因此当双方同时进行操作时,可能会导致版本错乱的问题,也可能造成版本丢失。</p><p>为了解决这个问题,我们可以构造一个版本管理的任务队列,所有和版本相关的更新都放到队列里进行处理:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/network-design-8.jpg" alt></p><p>任务队列每次只运行一个任务,任务在更新版本的时候确保了在原版本上按序升版。这样,不管是发送数据成功后的版本更新,还是接受到新的数据需要进行版本更新,都可以通过生成相关任务并添加到任务队列的方式,来进行版本升级。至于不同类型的任务,我们可以使用多态的方式来进行抽象和设计。</p><p>这样,每个对象的职责我们已经可确认了:</p><ul><li>待提交数据队列管理器:负责维护业务侧提交的数据</li><li>网络状态管理器:负责维护整个网络层的网络状态</li><li>版本管理器:负责网络层的版本维护</li><li>任务队列管理器:负责按序升版相关的任务管理和执行</li><li>发送数据控制器:负责处理来自业务侧的数据,并保证数据顺序递交、按序升版</li><li>接受数据控制器:负责处理来自连接层(服务端)的数据,并保证数据完成冲突处理和应用</li><li>总控制器:负责接收来自业务侧和连接层(服务端)的数据,并分发给发送数据控制器和接受数据控制器</li></ul><p>到这里,我们会发现对比初版设计,新版设计刚开始丢掉的任务队列也重新回来了,各个职责对象间的依赖关系也清晰了很多。而在实际开发和系统设计中,我们可以使用 UML 图来详细地画出每个对象的具体职责、对象之间的协作方式,这样在写代码之前就把很多问题思考清楚,也避免了开发过程中来回修改代码、职责越改越模糊等问题。</p><h3 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h3><ul><li><a href="http://www.wirfs-brock.com/PDFs/A_Brief-Tour-of-RDD.pdf" target="_blank" rel="noopener">A Brief Tour of Responsibility-Driven DesignCompressed</a></li><li><a href="https://www2.cs.arizona.edu/~mercer/Presentations/OOPD/12-RDD-Jukebox.pdf" target="_blank" rel="noopener">Responsibility Driven Design</a></li><li><a href="https://levelup.gitconnected.com/what-are-general-responsibility-assignment-software-patterns-6ad9635a44da" target="_blank" rel="noopener">What are General Responsibility Assignment Software Patterns?</a></li><li><a href="https://xie.infoq.cn/article/0f3eab53ac4228d769909425a" target="_blank" rel="noopener">架构必修:领域边界划分方法 – 职责驱动设计 (RDD)</a></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在本文中,我主要围绕着职责驱动设计的方式来进行接入层的设计思考,也更多关注于接入层内部各个职责对象的划分和依赖关系梳理。</p><p>但在实际开发中,我们还需要考虑更多各个对象之间的协作方式,它们之间的依赖要怎么进行合理地解耦,具体到写代码里面又会是怎样的表现,这些看看后面要不要继续讲~</p>]]></content>
<summary type="html">
<p>最近在负责通用网络层的设计和开发,会记录该过程中的一些思考,本文主要介绍职责驱动设计,以及它在网络层设计中的一些思考。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>前端这几年--写文章这件事</title>
<link href="https://godbasin.github.io/2021/01/10/about-writing/"/>
<id>https://godbasin.github.io/2021/01/10/about-writing/</id>
<published>2021-01-10T13:35:01.000Z</published>
<updated>2021-01-10T13:39:09.341Z</updated>
<content type="html"><![CDATA[<p>上周有给一些小伙伴分享写文章的一些经验,本以为身为程序员的自己讲的内容却是写文章会有点水,没想到大家的反响还不错,因此这里我将这些内容分享出来,希望能对更多的人也有用处叭~</p><a id="more"></a><h2 id="为什么要写"><a href="#为什么要写" class="headerlink" title="为什么要写"></a>为什么要写</h2><p>做一件事之前肯定都会有些原因,对我来说,开始写文章最初是由于自身的记性差。</p><h3 id="1-记性差"><a href="#1-记性差" class="headerlink" title="(1) 记性差"></a>(1) 记性差</h3><p>前端是一个技术变化和更新迭代非常快的领域,因此我们需要不断地进行学习。</p><p>很多时候,学过的一些内容由于没有长期使用,很快又会忘记,也因此一些坑会反复掉进去很多遍。为了避免这样的情况,我用了最笨的方法:写下来。写下来之后就可以很方便地翻出来,也可以通过搜索引擎搜索到相应的内容。</p><h3 id="2-思考是一件很有意思的事情"><a href="#2-思考是一件很有意思的事情" class="headerlink" title="(2) 思考是一件很有意思的事情"></a>(2) 思考是一件很有意思的事情</h3><p>习惯写笔记之后,发现越来越多的东西可以写下来。写文章和拍照片、排视频不一样,我们每次动笔之前都需要思考并组织自己的语言。所有这些写下来的东西,再次翻阅的时候都会重新思考,你会发现自己的认知跟以前不大一样了,会不断更新自己的认知。</p><h3 id="3-分享可以拓展视野"><a href="#3-分享可以拓展视野" class="headerlink" title="(3) 分享可以拓展视野"></a>(3) 分享可以拓展视野</h3><p>如果每个人都将自己的经验分享出来,大家会共同进步,越走越快。而当我们将自己的思考和想法分享出来,可以让更多人一起思考和研究,激起大家的讨论。</p><p>在交流的过程中,你会发现“原来他们是这样看待这个问题的呀”。通过这样的方式,我们可以接触到各种各样的思维方式和角度。</p><h3 id="4-提升效率"><a href="#4-提升效率" class="headerlink" title="(4) 提升效率"></a>(4) 提升效率</h3><p>我们在工作中,开发过很多系统,也踩过很多的坑。因此,有时候会有一些遇到同样问题的人来问,如果每次我们都要详细地跟对方讲解,会耗费不少的时间和经历。如果我们有养成记录的习惯,当对方询问的时候,可以直接把自己的笔记或者文章链接,直接给到对方阅读。通过这样的方式,可以节省双方的时间。</p><h2 id="怎么写"><a href="#怎么写" class="headerlink" title="怎么写"></a>怎么写</h2><p>一两年前我也做过写文章的相关分享,当时我并没有提出特别多的写作技巧。因为一直以来,我都没有关注该怎么去写,只是单纯地把自己想要记录的内容整理一下,然后记下来而已。</p><p>而当有些人问我,写文章到底有什么方法,刚开始我答不上来。后来我也观察自己写文章的一些思考方式和习惯,发现的确会有些注意的地方,在这里分享给大家。</p><h3 id="文章的目的是什么"><a href="#文章的目的是什么" class="headerlink" title="文章的目的是什么"></a>文章的目的是什么</h3><p>在写文章之前,我们首先需要理清这篇文章主要目的是什么。对于开发来说,一般可能包括:</p><ul><li>某个问题的解决过程</li><li>对新知识/技术的理解</li><li>架构设计和解决方案</li><li>工具的使用经验</li><li>……</li></ul><p>在理清文章的大致方向之后,我们可以整理出大概的思路,比如:</p><ul><li>某个问题的解决过程 -> 问题描述、问题分析、解决过程、总结</li><li>对新知识/技术的理解 -> 技术介绍、应用场景、技术比对、自身思考</li><li>架构设计和解决方案 -> 背景介绍、现状问题、业界方案、方案设计、执行过程、执行效果、未来规划</li><li>工具的使用经验 -> 工具出现背景、设计原理、解决什么问题、工具说明、使用效果、踩坑记录</li></ul><p>以上这些只是举例参考,我们在梳理出文章的大致思路之后,就很容易继续往下写了。</p><h3 id="文章的目标对象是谁"><a href="#文章的目标对象是谁" class="headerlink" title="文章的目标对象是谁"></a>文章的目标对象是谁</h3><p>在开始写文章之前,我们还需要知道文章是写给谁看的。</p><p>前面也说过,我记性比较差,即使是自己写过的文章过段时间也常常记不住了,所以经常需要自己再去翻阅。因此,对我来说,很多时候文章都是写给自己看的,同时这篇文章也可以写给和我遇到同样问题的人。</p><p>当我如果想把这篇文章给到其他人看的时候,要知道其他人的认知和我并不会完全一致,因此我需要在这篇文章里做一个认知差距的补充:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/about-writing-1.png" alt></p><p>比如,我之前有写过一篇<a href="http://www.godbasin.com/front-end-basic/deep-learning/reactive-programing.html" target="_blank" rel="noopener">《响应式编程在前端领域的应用》</a>,阅读这篇文章的人可能并不认识响应式编程,因此我会在文章最开始补充这块的知识:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/about-writing-2.png" alt></p><h3 id="确认文章大纲"><a href="#确认文章大纲" class="headerlink" title="确认文章大纲"></a>确认文章大纲</h3><p>前面我们在整理文章的目的的时候,已经大致梳理了文章的写作思路,在这里我们就可以梳理出大纲。比如这篇文章怎么写这段内容的大纲:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">怎么写好一篇文章</span><br><span class="line">- 这篇文章的目的是什么</span><br><span class="line"> - 记录过程</span><br><span class="line"> - 新技术介绍</span><br><span class="line"> - 架构设计</span><br><span class="line"> - 工具使用经验</span><br><span class="line">- 文章的目标对象是谁</span><br><span class="line">- 确定文章大纲</span><br><span class="line">- 写文章技巧</span><br></pre></td></tr></table></figure><p>列大纲也可以使用思维导图的方式整理,看个人习惯就好。</p><h3 id="写文章技巧"><a href="#写文章技巧" class="headerlink" title="写文章技巧"></a>写文章技巧</h3><p>在确认了文章的大纲之后,我们就可以往里面填充内容了。在具体写的时候,有几个小技巧:</p><h4 id="1-多进行总结和概括"><a href="#1-多进行总结和概括" class="headerlink" title="(1) 多进行总结和概括"></a>(1) 多进行总结和概括</h4><p>可以采用总分总、总分、分总这样的文章结构,要有文章概要或者总结的部分,比如:</p><ul><li>文章的最开始,可以列出这篇文章大概会讲些什么内容,这样别人就可以一下子看出这篇文章里有没有他们想看的内容</li><li>在文章的最后,可以列一些未来的展望,或是这篇文章的总结、自身的感想等等作为结束</li><li>具体写作过程中,也可以阶段性地进行一些总结,同时还可以给这些内容加粗着重标志</li></ul><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/about-writing-3.png" alt></p><h4 id="2-避免一段文字太长"><a href="#2-避免一段文字太长" class="headerlink" title="(2) 避免一段文字太长"></a>(2) 避免一段文字太长</h4><p>尽量让每个段落保持在不超过 4-6 行的长度。如果一段文字内容太多,别人在阅读的时候稍微不注意就会忘记自己看到哪,导致阅读体验下降。</p><h4 id="3-适当地加入一些图片-图形"><a href="#3-适当地加入一些图片-图形" class="headerlink" title="(3) 适当地加入一些图片/图形"></a>(3) 适当地加入一些图片/图形</h4><p>通过图形的方式,别人可以更加形象地理解我们想要表达的内容,比如架构图、时序图、逻辑图<br>等。<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/about-writing-4.png" alt><br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/about-writing-5.png" alt></p><h4 id="4-拆分步骤、分条列出"><a href="#4-拆分步骤、分条列出" class="headerlink" title="(4) 拆分步骤、分条列出"></a>(4) 拆分步骤、分条列出</h4><p>这个过程我们也需要对自己的表达进行结构化整理,同时其他人在阅读的时候可以很清晰地理解文章的内容。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/about-writing-6.png" alt></p><h2 id="如何坚持写"><a href="#如何坚持写" class="headerlink" title="如何坚持写"></a>如何坚持写</h2><p>写文章其实不需要太多的技巧,写的过程中会慢慢地形成自身的习惯。</p><p>但写文章最难的点在于,如何坚持下去。在很多时候,写文章都会显得吃力不讨好,大家都不爱写。甚至像我这种经常写文章的,有时候会有人认为工作不饱和、种很闲没事做。那么,我们要怎么让自己坚持写文章呢?</p><h3 id="量变到质变"><a href="#量变到质变" class="headerlink" title="量变到质变"></a>量变到质变</h3><p>不用着急一次性写好,写文章就像写代码,需要不断地改善和优化。或许刚开始写的时候,一篇文章要三四天甚至一两周,但如果写多了慢慢地就会写得很快了。</p><h3 id="进入良性循环"><a href="#进入良性循环" class="headerlink" title="进入良性循环"></a>进入良性循环</h3><p>尝试让写文章这件事进入良性循环。</p><p>知识沉淀,其实对工作是很有帮助的。我们在和其他人分享自己的经验时,也可以获得其他人的一些经验,从而拓展了自身的视野。而当我们把文章分享出去之后,也会慢慢不断地收到的一些反馈,在积累过程中也给自身搭建了不少的自信和热情。</p><h3 id="让一件事变得更加有趣"><a href="#让一件事变得更加有趣" class="headerlink" title="让一件事变得更加有趣"></a>让一件事变得更加有趣</h3><p>文章收到反馈都不具备实时性,很可能我们在发出去之后,需要一周、一个月甚至一年之后才会收到反馈。因此,更多时候可以考虑如何将一件事变得更好玩。</p><p>写文章,和写前端有个共同的特点,所见即所得。这意味着我可以加很多自己喜欢、觉得好玩的事情进去,整个写的过程它是一个很有趣的过程。</p><p>可以尝试在工作里也这样做。比如,之前帮后台写一个内部管理系统,当接口返回 404 的时候,随机生成一个猫的图片。除此之外,我也常常在代码注释里写一些结合心情的内容和表情包。</p><p>在重新整理自己的博客为前端游乐场的时候,也加入了很多自己喜欢的猫:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/about-writing-7.png" alt></p><p>通过这样的方式,可以把一些事情变得很有趣,也会更加喜欢上做这些事情,也可以更好地坚持下去。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>我们工作的很大一部分内容,都是在反复踩别人的坑,研究别人的代码,而这部分的经验,都是可复制的。一个在某个领域、业务经验熟练的人,只需要把他的经验分享出来,就能快速让其他人获得这些经验。</p><p>这样做会让自己的不可替代性变弱吗?我觉得并不会,工作中基本上没有不可替代的人,但我节省下来的一些时间,可以做更多的事情,可以往各个方向拓展自己,也可以培养点兴趣爱好,甚至希望早点下班回家也都是可以的。</p><p>有些小伙伴会担心自己写不好,或者写出来后受到质疑,就不敢大胆地写,或者写了不敢大胆发出来。</p><p>在这里,分享自己很喜欢的一句话给大家:</p><blockquote><p>“如果因为怕别人看到就不做自己觉得该做的事情,把它隐藏起来,那就等于说谁都不能做这个事情。如果自己把它做出来并让别人看到,那就等于说谁都可以这样做,然后很多人都会这样去做。”<br>—曼德拉</p></blockquote>]]></content>
<summary type="html">
<p>上周有给一些小伙伴分享写文章的一些经验,本以为身为程序员的自己讲的内容却是写文章会有点水,没想到大家的反响还不错,因此这里我将这些内容分享出来,希望能对更多的人也有用处叭~</p>
</summary>
<category term="工作这杯茶" scheme="https://godbasin.github.io/categories/%E5%B7%A5%E4%BD%9C%E8%BF%99%E6%9D%AF%E8%8C%B6/"/>
<category term="心态" scheme="https://godbasin.github.io/tags/%E5%BF%83%E6%80%81/"/>
</entry>
<entry>
<title>如何设计一个任务管理器</title>
<link href="https://godbasin.github.io/2020/11/01/task-runner-design/"/>
<id>https://godbasin.github.io/2020/11/01/task-runner-design/</id>
<published>2020-11-01T02:13:02.000Z</published>
<updated>2020-11-01T02:47:01.939Z</updated>
<content type="html"><![CDATA[<p>一般来说,我们在遇到对顺序要求严格的任务执行时,就需要维护一个任务管理器,保证任务的执行顺序。前端开发过程中,设计队列/栈的场景比较多,而需要用到任务管理器的场景偏少,本文主要介绍如何实现一个任务管理器。</p><a id="more"></a><p>理解任务管理器比较好的场景大概是协同文档编辑的场景,比如 Google Docs、腾讯文档、Sketch 协同等。我们在进行协同编辑的时候,对版本和消息时序有比较严格的要求,因此常常需要维护一个任务管理器来管理版本相关的任务。</p><p>以上是一些科普知识,用于辅助大家理解接下来的任务管理器设计,下面我们来进入正文。</p><h2 id="单个任务的设计"><a href="#单个任务的设计" class="headerlink" title="单个任务的设计"></a>单个任务的设计</h2><p>对于单个任务的设计,主要考虑任务的执行。一个任务的作用就是用来运行的,那么对于任务来说,可能会有几个状态:待执行、正在执行、执行失败、执行成功等:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> TASK_STATUS {</span><br><span class="line"> INIT = <span class="string">'INIT'</span>, <span class="comment">// 初始状态</span></span><br><span class="line"> READY = <span class="string">'READY'</span>, <span class="comment">// 可执行</span></span><br><span class="line"> RUNNING = <span class="string">'RUNNING'</span>, <span class="comment">// 执行中</span></span><br><span class="line"> SUCCESS = <span class="string">'SUCCESS'</span>, <span class="comment">// 执行成功</span></span><br><span class="line"> FAILED = <span class="string">'FAILED'</span>, <span class="comment">// 执行失败</span></span><br><span class="line"> DESTROY = <span class="string">'DESTROY'</span>, <span class="comment">// 已销毁</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h3><p>既然涉及到任务的各个状态,我们也可以赋予任务一些生命周期。这里我们举一些例子,但最终的生命周期设计应该要和自己业务实际情况结合。</p><p><strong>onReady: 任务执行前准备工作</strong></p><p>在每个任务执行之前,我们都需要再次确认下这个任务的状态(是否已经失效),也可能需要做些准备工作,这个阶段可以命名为<code>onReady</code>:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> ICommonTask {</span><br><span class="line"> onReady(): <span class="built_in">Promise</span><<span class="built_in">boolean</span>>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,该生命周期以返回 Promise 的方式来运行,该 Promise 包括一个布尔值,用于判断任务是否继续执行。比如我们需要在执行任务之前,从服务端获取一些数据,那么可以这么实现:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> ATask <span class="keyword">implements</span> ICommonTask {</span><br><span class="line"> <span class="keyword">async</span> onReady() {</span><br><span class="line"> <span class="keyword">const</span> result = <span class="keyword">await</span> getSomeDate();</span><br><span class="line"> <span class="keyword">if</span> (result.isSuccess) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>onRun: 任务执行中</strong></p><p>任务准备工作完成之后,任务就需要开始真正运行了。同样的,我们将这个阶段命名为<code>onRun</code>:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> ICommonTask {</span><br><span class="line"> onReady(): <span class="built_in">Promise</span><<span class="built_in">boolean</span>>;</span><br><span class="line"> onRun(): <span class="built_in">Promise</span><CommonTask[] | <span class="built_in">void</span>>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里我们看到,<code>onRun</code>阶段执行同样返回一个 Promise,但 Promise 内容和<code>onReady</code>阶段不一致,它可能返回一个或者多个<code>CommonTask</code>组成的数组。这是因为一个任务执行的过程中,可能会产生新的任务,也可能由于其他条件限制,导致它需要创建一个别的任务先执行完毕,才能继续执行自己原本的任务。比如,B 任务在执行的时候,如果条件不满足,则需要先执行一个 A 任务:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> BTask <span class="keyword">implements</span> ICommonTask {</span><br><span class="line"> <span class="comment">// 其他省略</span></span><br><span class="line"> <span class="keyword">async</span> onRun() {</span><br><span class="line"> <span class="keyword">if</span> (needATask) {</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">new</span> ATask(), <span class="keyword">this</span>.resetTask()];</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 其他正常执行任务逻辑</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>onDestroy: 任务执行完毕,即将销毁</strong></p><p>很多时候我们实现一些模块功能,都会产生一些临时变量,也可能有一些事件绑定、DOM 元素需要在该模块注销的时候清除,因此进行主动的销毁和清理是一个很好的习惯。对于一个任务的执行来说也是一样的,我们将这个阶段命名为<code>onDestroy</code>:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> ICommonTask {</span><br><span class="line"> onReady(): <span class="built_in">Promise</span><<span class="built_in">boolean</span>>;</span><br><span class="line"> onRun(): <span class="built_in">Promise</span><CommonTask[] | <span class="built_in">void</span>>;</span><br><span class="line"> onDestroy(): <span class="built_in">Promise</span><<span class="built_in">void</span>>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于任务的生命周期相关,我们暂时讲到这里,接下来我们来看任务的执行。</p><h3 id="任务执行"><a href="#任务执行" class="headerlink" title="任务执行"></a>任务执行</h3><p>由于每个任务都会有状态、生命周期、执行功能、重置功能,我们可以实现一个通用的任务:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> CommonTask <span class="keyword">implements</span> ICommonTask {</span><br><span class="line"> <span class="comment">/** 生命周期钩子 **/</span></span><br><span class="line"> <span class="keyword">abstract</span> onReady: <span class="function"><span class="params">()</span> =></span> <span class="built_in">Promise</span><<span class="built_in">boolean</span>>;</span><br><span class="line"> <span class="keyword">abstract</span> onRun: <span class="function"><span class="params">()</span> =></span> <span class="built_in">Promise</span><CommonTask[] | <span class="built_in">void</span>>;</span><br><span class="line"> <span class="keyword">abstract</span> onDestroy: <span class="function"><span class="params">()</span> =></span> <span class="built_in">Promise</span><<span class="built_in">void</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 执行任务 **/</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">async</span> execute(): <span class="built_in">Promise</span><CommonTask[] | <span class="built_in">void</span>> {</span><br><span class="line"> <span class="comment">// step 1 准备任务</span></span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">await</span> <span class="keyword">this</span>.onReady()) {</span><br><span class="line"> <span class="comment">// 任务准备校验不通过,直接没必要执行了</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.onDestroy();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// step 2 执行任务</span></span><br><span class="line"> <span class="keyword">const</span> runResult = <span class="keyword">await</span> <span class="keyword">this</span>.onRun();</span><br><span class="line"> <span class="keyword">if</span> (runResult) {</span><br><span class="line"> <span class="comment">// 若分裂出新的任务,返回并不再继续执行了</span></span><br><span class="line"> <span class="keyword">return</span> runResult;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// step 3 销毁任务</span></span><br><span class="line"> <span class="keyword">this</span>.onDestroy();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里 CommonTask 提供了一个通用的<code>execute</code>方法用于执行任务,我们能看到其中的实现也是根据生命周期依次执行。当然,这里其实还需要在执行到对应生命周期的时候,扭转任务状态。除此之外,任务执行异常的处理也并不在这里,因此外界需要进行<code>try catch</code>处理。</p><p>那么到底在哪里需要进行异常处理呢?我们接下来看看任务管理器。</p><h2 id="任务管理器"><a href="#任务管理器" class="headerlink" title="任务管理器"></a>任务管理器</h2><p>显然,任务管理器的职责主要是保证任务队列中的任务有序、顺利地执行,其中会包括任务执行时的异常处理。除此之外,任务管理器还需要对外提供添加任务,以及暂停、恢复、停止这样的能力。</p><h3 id="任务管理器状态"><a href="#任务管理器状态" class="headerlink" title="任务管理器状态"></a>任务管理器状态</h3><p>既然任务管理器有对任务的管理,当然它也需要维护自身的状态,例如:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> QUEUE_STATUS {</span><br><span class="line"> WORKING = <span class="string">'WORKING'</span>, <span class="comment">// 工作中</span></span><br><span class="line"> PAUSE = <span class="string">'PAUSE'</span>, <span class="comment">// 暂停</span></span><br><span class="line"> IDLE = <span class="string">'IDLE'</span>, <span class="comment">// 空闲</span></span><br><span class="line"> SHUTDOWN = <span class="string">'SHUTDOWN'</span>, <span class="comment">// 关停</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这些是任务管理器基本的状态,包括空闲状态、工作中、暂停、停止等。对于每一个不同的状态来说,相应的任务管理器也会有一些更新状态的方法:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> TaskManager {</span><br><span class="line"> status: QUEUE_STATUS = QUEUE_STATUS.IDLE;</span><br><span class="line"> <span class="comment">// 暂停任务管理器</span></span><br><span class="line"> <span class="keyword">public</span> pause() {</span><br><span class="line"> <span class="keyword">this</span>.status = QUEUE_STATUS.PAUSE;</span><br><span class="line"> <span class="comment">// 当前正在运行的任务需要处理</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 恢复任务管理器</span></span><br><span class="line"> <span class="keyword">public</span> resume() {</span><br><span class="line"> <span class="comment">// 如果被关停了,则不能恢复啦</span></span><br><span class="line"> <span class="keyword">if</span> (isShutDown) { <span class="keyword">return</span>; }</span><br><span class="line"> <span class="keyword">this</span>.status = QUEUE_STATUS.WORKING;</span><br><span class="line"> <span class="keyword">this</span>.work();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 关停任务管理器</span></span><br><span class="line"> <span class="keyword">public</span> resume() {</span><br><span class="line"> <span class="keyword">this</span>.status = QUEUE_STATUS.SHUTDOWN;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 任务管理器工作</span></span><br><span class="line"> <span class="keyword">private</span> work() {</span><br><span class="line"> <span class="keyword">if</span>(!isWorking && hasNextTask) {</span><br><span class="line"> <span class="comment">// 如果有会继续执行下一个任务</span></span><br><span class="line"> <span class="comment">// 直到任务管理器被暂停、或者任务队列为空</span></span><br><span class="line"> runNextTask();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里面比较关键的点有两个:</p><ol><li>暂停任务管理器的时候,需要考虑如何处理正在运行的任务。</li><li>执行任务的时候,需要进行一些异常处理。同时,任务的运行可能会进行分裂并产生新的任务,需要对新任务进行处理。</li></ol><h3 id="暂停与恢复"><a href="#暂停与恢复" class="headerlink" title="暂停与恢复"></a>暂停与恢复</h3><p>我们先来看第一点:任务管理器暂停和恢复时的处理。</p><p>一个简单粗暴的处理方式是,将当前正在运行的任务继续运行完成。但这种处理方式,与我们对于暂停的理解有一些误差。因此,我们可以考虑让任务本身支持重置的功能,比如运行过程中判断任务状态是否需要继续执行,结合销毁当前任务、并将原有任务进行重置。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> CommonTask {</span><br><span class="line"> <span class="comment">/** 重置任务 **/</span></span><br><span class="line"> <span class="comment">// 会返回任务本身,该任务应该是被重置过的最初状态</span></span><br><span class="line"> <span class="keyword">abstract</span> reset(): CommonTask;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实现起来其实也不会很难:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> ATask <span class="keyword">extends</span> CommonTask {</span><br><span class="line"> <span class="keyword">public</span> reset() {</span><br><span class="line"> <span class="comment">// 销毁当前任务</span></span><br><span class="line"> <span class="keyword">this</span>.destroy();</span><br><span class="line"> <span class="comment">// 并返回一个重置后的新任务</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ATask();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于任务管理器来说,要做的事情也比较简单了:暂停任务管理器的时候,将当前任务重置、并扔回任务队列的头部。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> TaskManager {</span><br><span class="line"> <span class="comment">// 暂停任务管理器</span></span><br><span class="line"> <span class="keyword">public</span> pause() {</span><br><span class="line"> <span class="keyword">this</span>.status = QUEUE_STATUS.PAUSE;</span><br><span class="line"> <span class="comment">// 将当前任务重置,并扔回任务队列头部</span></span><br><span class="line"> taskList.unshift(currentTask.reset());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="任务管理器工作"><a href="#任务管理器工作" class="headerlink" title="任务管理器工作"></a>任务管理器工作</h3><p>任务管理器工作的时候,主要工作内容包括依次运行任务、处理任务异常、处理任务运行后分裂产生的新任务。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> TaskManager {</span><br><span class="line"> <span class="comment">// 任务管理器工作</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">async</span> work() {</span><br><span class="line"> <span class="keyword">if</span>(!isWorking && hasNextTask) {</span><br><span class="line"> <span class="comment">// 如果满足条件,会继续执行下一个任务</span></span><br><span class="line"> currentTask = getNextTask();</span><br><span class="line"> <span class="keyword">const</span> resultTask = <span class="keyword">await</span> currentTask.execute().catch(<span class="function">(<span class="params">error</span>) =></span> {</span><br><span class="line"> <span class="comment">// 异常处理</span></span><br><span class="line"> });</span><br><span class="line"> <span class="comment">// 判断是否有分裂的新任务</span></span><br><span class="line"> <span class="keyword">if</span> (resultTask) {</span><br><span class="line"> <span class="comment">// 如果有,就塞回到任务队列的头部,需要优先处理</span></span><br><span class="line"> taskList.unshift(resultTask);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 继续执行下一个任务</span></span><br><span class="line"> checkContinueWork();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以上大概是我们在设计一个任务管理器的过程中,需要进行思考的一些问题、和简单的实现方式。除此之外,在一个更加复杂的应用场景下,我们还可能会遇到多个任务队列的管理和资源调度、同步任务和异步任务的管理、任务支持优先级设置等各式各样的功能设计。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>任务管理也好、队列/堆栈的设计也好,都会在工程中经常遇到。而随着应用场景的不一样,我们的设计并不能简单地进行复用,每一次都可以结合业务本身、工程本身而设计出更加合适的调整,每一次我们也都可以给自己提出不一样的要求。</p>]]></content>
<summary type="html">
<p>一般来说,我们在遇到对顺序要求严格的任务执行时,就需要维护一个任务管理器,保证任务的执行顺序。前端开发过程中,设计队列/栈的场景比较多,而需要用到任务管理器的场景偏少,本文主要介绍如何实现一个任务管理器。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>在线Excel项目到底有多刺激</title>
<link href="https://godbasin.github.io/2020/10/10/why-spreadsheet-app-excited/"/>
<id>https://godbasin.github.io/2020/10/10/why-spreadsheet-app-excited/</id>
<published>2020-10-10T13:29:30.000Z</published>
<updated>2020-10-10T13:33:00.881Z</updated>
<content type="html"><![CDATA[<p>加入腾讯文档 Excel 开发团队已经有好几个月了,刚开始代码下载下来 100+W 行,代码量很大但模块设计和代码质量比我想象中好好多了,今天跟大家分享下一个 Excel 项目到底可以有多好玩。</p><a id="more"></a><h1 id="实时协同编辑的挑战"><a href="#实时协同编辑的挑战" class="headerlink" title="实时协同编辑的挑战"></a>实时协同编辑的挑战</h1><p>说到实时协同编辑的难点,大家的第一反应基本上是协同冲突处理。</p><h2 id="冲突处理"><a href="#冲突处理" class="headerlink" title="冲突处理"></a>冲突处理</h2><p>冲突处理的解决方案其实已经相对成熟,包括:</p><ol><li><strong>编辑锁</strong>:当有人在编辑某个文档时,系统会将这个文档锁定,避免其他人同时编辑。</li><li><strong>diff-patch</strong>:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,包括 GNU diff-patch、Myer’s diff-patch 等方案。</li><li><strong>最终一致性实现</strong>:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。</li></ol><p>编辑锁的实现方式简单粗暴,但会直接影响用户体验。diff-patch 可以对冲突进行自助合并,也可以在冲突出现时交给用户处理。OT 算法是 Google Docs 中所采用的方案,Atom 编辑器使用的则是 CRDT。</p><h3 id="OT-和-CRDT"><a href="#OT-和-CRDT" class="headerlink" title="OT 和 CRDT"></a>OT 和 CRDT</h3><p>OT 和 CRDT 两种方法的相似之处在于它们提供最终的一致性。不同之处在于他们的操作方式:</p><ul><li>OT 通过更改操作来做到这一点<ul><li>OT 会对编辑进行操作的拆分、转换,实现冲突处理的效果</li><li>OT 并不包括具体的实现,因此需要项目自行实现,但可以根据项目需要进行高精度的冲突处理</li></ul></li><li>CRDT 通过更改状态来做到这一点<ul><li>基本上,CRDT 是数据结构,当使用相同的操作集进行更新时,即使这些操作以不同的顺序应用,它们始终会收敛在相同的表示形式上</li><li>CRDT 有两种方法:基于操作和基于状态</li></ul></li></ul><p>OT 主要用于文本,通常常很复杂且不可扩展。CRDT 实现很简单,但 Google、Microsoft、CKSource 和许多其他公司依赖 OT 是有原因的,CRDT 研究的当前状态支持在两种主要类型的数据上进行协作:纯文本、任意 JSON 结构。</p><p>对于富文本编辑等更高级的结构,OT 用复杂性换来了对用户预期的实现,而 CRDT 则更加关注数据结构,随着数据结构的复杂度上升,算法的时间和空间复杂度也会呈指数上升的,会带来性能上的挑战。因此,如今大多数实时协同编辑都基于 OT 算法来实现。</p><h2 id="版本管理"><a href="#版本管理" class="headerlink" title="版本管理"></a>版本管理</h2><p>在多人协作的场景下,为了保证用户体验,一般会采用 diff-patch/OT 算法来进行冲突处理。而为了保证每次的用户操作都可以按照正确的时序来更新,需要会维护一个自增的版本号,每次有新的修改,都会更新版本号。</p><h3 id="数据版本更新"><a href="#数据版本更新" class="headerlink" title="数据版本更新"></a>数据版本更新</h3><p>数据版本能按照预期有序更新,需要几个前提:</p><ul><li><strong>协同数据版本正常更新</strong></li><li><strong>丢失数据版本成功补拉</strong></li><li><strong>提交数据版本有序递增</strong></li></ul><p>要怎么理解这几个前提呢?我们来举个例子。</p><p>小明打开了一个文档,该文档从服务器拉取到的数据版本是 100。这时候服务器下发了个消息,说是有人将该版本更新到了 101,于是小明需要将这个 101 版本的数据更新到界面中,这是<strong>协同数据版本正常更新</strong>。</p><p>小明基于最新的 101 版本进行了编辑,产生了个新的操作数据。当小明将这个数据提交到服务器的时候,服务器看到小明的数据基于 101 版本,就跟小明说现在最新的版本已经是 110 了。小明只能先去服务器将 102-110 的版本补拉回来,这是<strong>丢失数据版本成功补拉</strong>。</p><p>102-110 的数据版本补拉回来之后,小明之前的操作数据需要分别跟这些数据版本进行冲突处理,最后得到了一个基于 110 版本的操作数据。这时候小明重新将数据提交给服务器,服务器接受了并给小明分配了 111 版本,于是小明将自己本地的数据版本升级为 111 版本,这是<strong>提交数据版本有序递增</strong>。</p><h3 id="维护数据任务队列"><a href="#维护数据任务队列" class="headerlink" title="维护数据任务队列"></a>维护数据任务队列</h3><p>要管理好这些版本,我们需要维护一个用户操作的数据队列,用来有序提交数据。这个队列的职责包括:</p><ul><li>用户操作数据正常进入队列</li><li>队列任务正常提交到接入层</li><li>队列任务提交异常后进行重试</li><li>队列任务确认提交成功后移除</li></ul><p>这样一个队列可能还会面临用户突然关闭页面等可能,我们还需要维护一个缓存数据,当用户再次打开页面的时候,将用户编辑但未提交的数据再次提交到服务器。除了浏览器关闭的情况,还有用户在编辑过程中网络状况变化而导致的网络中断,这种时候我们也需要将用户的操作离线到本地,当网络恢复的时候继续上传。</p><h2 id="房间管理"><a href="#房间管理" class="headerlink" title="房间管理"></a>房间管理</h2><p>由于多人协同的需要,相比普通的 Web 页面,还多了房间和用户的管理。在同一个文档中的用户,可视作在同一个房间。除了能看到哪些人在同一个房间以外,我们能收到相互之间的消息,在文档的场景中,用户的每一个操作,都可以作为是一个消息。</p><p>但文档和一般的房间聊天不一样的地方在于,用户的操作不可丢失,同时还需要有严格的版本顺序的保证。用户的操作内容可能会很大,例如用户复制粘贴了一个10W、20W的表格内容,这样的消息显然无法一次性传输完。在这种情况下,除了考虑像 Websocket 这种需要自行进行数据压缩(HTTP 本身支持压缩)以外,我们还需要实现自己的分片逻辑。当涉及数据分片之后,紧接而来的还有如何分片、分片数据丢失的一些情况处理。</p><h2 id="多种通信方式"><a href="#多种通信方式" class="headerlink" title="多种通信方式"></a>多种通信方式</h2><p>前后端通信方式有很多种,常见的包括 HTTP 短轮询(polling)、Websocket、HTTP 长轮询(long-polling)、SSE(Server-Sent Events)等。</p><p>我们也能看到,不同的在线文档团队选用的通信方式并不一致。例如谷歌文档上行数据使用 Ajax、下行数据使用 HTTP 长轮询推送;石墨文档上行数据使用 Ajax、下行数据使用 SSE 推送;金山文档、飞书文档、腾讯文档则都使用了 Websocket 传输。</p><p>而每种通信方式都有各自的优缺点,包括兼容性、资源消耗、实时性等,也有可能跟业务团队自身的后台架构有关系。因此我们在设计连接层的时候,考虑接口拓展性,应该预留对各种方式的支持。</p><h1 id="每个格子都是一个富文本编辑器"><a href="#每个格子都是一个富文本编辑器" class="headerlink" title="每个格子都是一个富文本编辑器"></a>每个格子都是一个富文本编辑器</h1><p>其实除了实时协同编辑相关,Excel 项目还面临着很多其他的挑战。大家都知道富文本编辑器很坑,但在 Excel 中,每个格子都是富文本编辑器。</p><h2 id="富文本"><a href="#富文本" class="headerlink" title="富文本"></a>富文本</h2><p>富文本的编辑,一般有几种处理方式:</p><ul><li>一个简单的 div 增加<code>contenteditable</code>属性,用浏览器原生的<code>execCommand</code>执行</li><li>div + 事件监听来维护一套编辑器状态(包括光标状态)</li><li>textarea + 事件监听维护一套编辑器状态</li></ul><p>对于<code>contenteditable</code>属性,要对选中的文本进行操作(如斜体、颜色),需要先判断光标的位置,用 Range 判断选中的文本在哪里,然后判断这段文本是不是已经被处理过,需要覆盖、去掉还是保留原效果,这里的坑比较多,也常常出现兼容性问题。<br>一般来说,像 Atom、VSCode 这些复杂的编辑器都是自己实现类似 contenteditable 功能的,使用 div+事件监听的方式。而 Ace editor、金山文档等则是使用隐藏的 textarea 接收输入,并渲染到 div 中来实现编辑效果。</p><h2 id="复制粘贴"><a href="#复制粘贴" class="headerlink" title="复制粘贴"></a>复制粘贴</h2><p>一般来说单个单元格或是多个单元格选中复制的时候,我们能拿到的是格子的原始数据,因此需要进行两步操作:<strong>将数据转换成富文本</strong>(拼接 table/tr/td 等元素),然后<strong>写入剪切板</strong>。</p><p>粘贴的过程,同样需要:<strong>从剪切板获取内容</strong>,再将这些内容<strong>转换成单元格数据</strong>,并<strong>提交操作数据</strong>。这里还可能涉及图片的上传、各种富文本的解析,每个单元格都可能由于设置的一些属性(包括合并单元格、行高列宽、筛选、函数等)而使得解析过程的复杂度直线上升。</p><p>复制粘贴相关功能模块复制粘贴根据使用场景可以分成两种:</p><ol><li><strong>内部复制粘贴</strong>。</li><li><strong>外部复制粘贴</strong>。</li></ol><p>内部复制粘贴指的是在自己产品内的复制粘贴,由于一个复制粘贴过程涉及的计算和解析都很多,内部复制粘贴可以考虑是否直接将单元格数据写入剪切板,粘贴的时候就可以直接获得数据,省去了将数据转换成富文本、将富文本解析成单元格数据等这些计算耗时较大、资源占用较多的步骤。</p><p>外部复制粘贴更多则是涉及到各种同类 Excel 编辑产品的兼容、系统剪切板内容格式的兼容,代码实现特别复杂。</p><h1 id="表格渲染有多复杂"><a href="#表格渲染有多复杂" class="headerlink" title="表格渲染有多复杂"></a>表格渲染有多复杂</h1><p>表格的绘制一般来说也有两种实现方案:</p><ol><li><strong>DOM 绘制</strong>。</li><li><strong>canvas 绘制</strong>。</li></ol><p>业界比较出名的 handsontable 开源库就是基于 DOM 实现绘制,但显而易见十万、百万单元格的 DOM 渲染会产生较大的性能问题。因此,如今很多 Web 版的电子表格实现都是基于 canvas + 叠加 DOM 来实现的,使用 canvas 实现同样需要考虑可视区域、滚动操作、画布层级关系,也有 canvas 自身面临的一些性能问题,包括 canvas 如何进行直出等。</p><p>表格渲染涉及合并单元格、选区、缩放、冻结、富文本与自动换行等各种各样的场景,我们来看看其中到底有多复杂。</p><h2 id="自动换行"><a href="#自动换行" class="headerlink" title="自动换行"></a>自动换行</h2><p>一般来说,一个单元格自动换行体现在数据存储上,只包括:单元格内容+换行属性。但这样一个数据需要渲染出来的时候,则面临着自动换行的一些计算:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/why-spreadsheet-app-excited-1.jpg" alt></p><p>我们需要找到该列的列宽,然后根据该单元格内容情况来进行渲染层的分行。如图,这样一串文本会根据分行逻辑的计算分成了三行。而自动换行之后,还可能涉及该单元格所在行的行高被撑起导致的调整,行高的调整可能还会影响该行其他单元格一些居中属性的渲染结果,需要重新计算。</p><p>因此,当我们对一列格子设置了自动换行,可能会导致大规模的重新计算和渲染,同样会涉及较大的性能消耗。</p><h2 id="冻结区域"><a href="#冻结区域" class="headerlink" title="冻结区域"></a>冻结区域</h2><p>冻结功能可以将我们的表格分成四个区域,左右和上下划分了冻结和非冻结区域。冻结区域的复杂度主要在于边界的一些特殊情况处理,包括区域的选择、图片的切割等。我们来看一个图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/why-spreadsheet-app-excited-2.png" alt></p><p>如图,对于一个图片来说,虽然它是直接放在整个表格上,但落到数据层中的时候,它其实只属于某一个格子。在冻结区域的编辑上,我们需要对它进行切分,但不管是哪个区域中选中它,我们依然需要展示它的原图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/why-spreadsheet-app-excited-3.jpg" alt></p><p>这意味着在 canvas 中,我们获取到鼠标点击的位置时,还需要计算出对应点击的格子是否属于图片覆盖范围内。</p><h2 id="对齐与单元格溢出"><a href="#对齐与单元格溢出" class="headerlink" title="对齐与单元格溢出"></a>对齐与单元格溢出</h2><p>一个单元格的水平对齐方式一般分为三种:左对齐、居中对齐、右对齐。当单元格没有设置自动换行,其内容又超出了该格子的宽度时,会出现覆盖到其他格子的情况:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/why-spreadsheet-app-excited-4.jpg" alt></p><p>也就是说,我们在绘制某个格子的时候,同样需要计算附近的格子有没有溢出到当前格子的情况,如果有溢出则需要在这个格子里进行绘制。除此之外,当某列格子被隐藏的时候,溢出的逻辑可能还需要进行调整和更新。</p><p>以上列出的,都只是某一些比较细节的点,而表格的渲染还涉及单元格和行列的隐藏、拖拽、缩放、选区等各种逻辑,还有单元格边框的一些复杂计算。除此之外,由于 canvas 渲染是一屏的内容,涉及页面的滚动、协同数据的更新等会同样可能导致画布频繁更新绘制。</p><h1 id="数据管理的难题"><a href="#数据管理的难题" class="headerlink" title="数据管理的难题"></a>数据管理的难题</h1><p>当每个格子都支持富文本内容,在十万、百万单元格的场景下,对落盘数据的存储、用户操作的数据变更也提出了不小的挑战。</p><h2 id="原子操作"><a href="#原子操作" class="headerlink" title="原子操作"></a>原子操作</h2><p>和数据库的事务相类似,对于电子表格来说,我们可以将用户的操作拆分成不可分割的原子操作。为什么要这么做呢?其实主要是方便进行 OT 算法的冲突处理,可针对每个不可拆分的原子操作进行特定逻辑的冲突计算和转换,最终落盘到存储中。</p><p>例如,我们插入一个子表这样一个操作,除了插入自身的操作,可能需要对其他子表进行移动操作。那么,对于一个子表来说,我们的操作可能会包括:</p><ul><li>插入</li><li>重命名</li><li>移动</li><li>删除</li><li>更新内容</li><li>…</li></ul><p>只要拆分得足够仔细,对于子表的所有用户行为,都可以由这些操作来组合成最终的效果,这些不再可拆分的操作便是最终的原子操作。例如,复制粘贴一张子表,可以拆分为<code>插入-重命名-更新内容</code>;剪切一张子表,可以拆分为<code>插入-更新内容-删除-移动其他子表</code>。通过分析用户行为,我们可以提取出这些基本操作,来看个具体的例子:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/sheet_ot.png" alt></p><p>如图,对于服务端来说,最终就是新增了两个子表,一个是张三的“工作表 2”,另一个是李四的“工作表 2(自动重命名)”。</p><p>在实现上,一般使用 tranform 函数来处理并发操作,该函数接受已应用于同一文档状态(但在不同客户端上)的两个操作,并计算可以在第二个操作之后应用并保留第一个操作的新操作操作的预期更改。</p><p>在不同的 OT 系统中使用的 OT 函数的名称可能有所不同,但是可以将其分为两类:</p><ul><li>inclusion transformation/forward transformation:表示为<code>IT(opA, opB)</code>,<code>opA</code>以一种有效地包含<code>opB</code>的影响的方式,将操作转换为另一个操作<code>opB'</code>。</li><li>exclusion transformation/backward transformation:表示为<code>ET(opA, opB)</code>,<code>opA</code>以一种有效排除<code>opB</code>影响的方式,将操作转换为另一操作<code>opB''</code>。</li></ul><p>一些 OT 系统同时使用 IT 和 ET 功能,而某些仅使用 IT 功能。OT 功能设计的复杂性取决于多种因素:OT 系统是否支持一致性维护、是否支持 Undo/Redo、要满足哪些转换属性、是否使用 ET、OT 操作模型是否通用、每个操作中的数据是按字符(单个对象)还是按字符串(对象序列)、分层还是其他结构等。</p><p>除了客户端收到服务器的协同消息之后需要进行本地的冲突处理,服务器也可能存在先后接收到两个基于同一版本的消息之后进行冲突处理。在本地和服务器都有一套一致的冲突处理逻辑,才能保证算法的最终一致性。</p><h2 id="版本回退-重做"><a href="#版本回退-重做" class="headerlink" title="版本回退/重做"></a>版本回退/重做</h2><p>对于大多数编辑器来说,Undo/Redo 是最基础的能力,文档编辑也不例外。前面我们提到实时协同有版本的概念,同时用户的每一个操作可能会被拆分成多个原子操作。</p><p>在这样的场景下,Undo/Redo 既涉及到落盘数据的恢复,还涉及到用户操作的还原时遇到冲突的一些处理。在多人协同的场景下,如果在编辑过程中接收到了其他人的一些操作数据,那么 Undo 的时候是否又会撤回别人的操作呢?</p><p>基于 OT 算法的 Undo 其实思路相对简单,通常是针对每个原子操作实现对应的<code>invert()</code>方法,进行该原子操作的逆运算,生成一个新的原子操作并应用。</p><p>前面我们介绍 transform 函数可以分为 IT 和 ET 两类,而 Undo 的实现有两种方式:</p><ul><li>Inv & IT: invert + inclusion transformation</li><li>ET & IT: exclusion transformation + inclusion transformation</li></ul><p>不管是哪种算法,OT 用于撤消的基本思想是根据操作之后执行的那些操作的效果,将操作的逆操作(待撤消的操作)转换为新形式,从而使转换后的逆操作可以实现正确的 Undo 影响。但如果用户在编辑的时候接收到了新的协同操作,当该用户在进行 Undo 的时候,通过逆运算生成的原子操作同样需要和这些新来的协同消息进行冲突处理,才能保证最终一致性。</p><h2 id="数据"><a href="#数据" class="headerlink" title="数据"></a>数据</h2><p>对于支持富文本的单元格来说,每个单元格除了自身的一些属性设置,包括数据格式验证、函数计算、宽高、边框、填充色等,还需要维护该单元格内富文本格式、关联图片的一些数据。这些数据在面临十万甚至百万单元格的时候,对数据传输和存储也带来了不小的挑战。</p><p>修订记录的版本和还原、如何优化内存、如何优化数据大小、如何高效利用数据、如何降低计算时空复杂度等都成为了数据层面临的一些难题。</p><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2><p>以上列举的,只占整个Excel项目的一小部分,而除此之外还有Worker、菜单栏、各种各样的feature功能,像数据格式、函数、图片、图表、筛选、排序、智能拖拽、导入导出、区域权限、搜索替换,每一个功能都会因为项目的复杂性而面临各式各样的挑战。</p><p>除此以外,各个模块之间功能解耦、100W+的代码怎么进行组织和架构设计、代码加载流程如何优化、多人协作导致的问题、项目的维护性/可读性、性能优化等都是我们经常需要思考的问题。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>参与这样的项目,最大的感受是不需要再抓破脑袋去想某个项目还可以做出哪些亮点,因为可以做的事情实在是太多了。对于很多业务来说,代码质量、维护性和可读性也常常不受重视。我们常常因为项目本身的局限性(相对简单)而无法找到自己可以深挖的点,因此最后都是只能通过自动化、配置化的方式去尽可能地提升效能,但可以做的其实也很局限,自身的成长也因此受限。</p><p>大家经常调侃说前端的天花板太低,又说自己面临35岁被淘汰。抛去个人兴趣、热情和自身瓶颈这些原因,很多时候也是因为条件不允许、业务场景较简单,因此没有场景可以发挥自己的能力。以前我也觉得下班之后学习也是可以的,但如果上班就做着自己喜欢的工作,岂不是一举两得?</p><p>最后,欢迎大家各式各样的讨论和交流~</p><p>PS:我们腾讯文档团队还在招人噢~~</p><blockquote><p>感兴趣的可以联系我,QQ: 1780096742,也可以投递简历到 <a href="mailto:wangbeishan@163.com" target="_blank" rel="noopener">wangbeishan@163.com</a>(邮件可能回复不及时)</p></blockquote>]]></content>
<summary type="html">
<p>加入腾讯文档 Excel 开发团队已经有好几个月了,刚开始代码下载下来 100+W 行,代码量很大但模块设计和代码质量比我想象中好好多了,今天跟大家分享下一个 Excel 项目到底可以有多好玩。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>前端监控体系搭建</title>
<link href="https://godbasin.github.io/2020/10/07/monitor-and-report/"/>
<id>https://godbasin.github.io/2020/10/07/monitor-and-report/</id>
<published>2020-10-07T12:45:31.000Z</published>
<updated>2020-10-07T12:46:07.879Z</updated>
<content type="html"><![CDATA[<p>整理了下前端监控的一些项目经验,结合自己的想法输出了这篇文章,跟大家分享下。</p><a id="more"></a><h1 id="前端监控体系搭建"><a href="#前端监控体系搭建" class="headerlink" title="前端监控体系搭建"></a>前端监控体系搭建</h1><p>通常前端建立搭建监控体系,主要是为了解决两个问题:<strong>如何及时发现问题</strong>、<strong>如何快速定位并解决问题</strong>。</p><p>一般来说,结合开发和产品的角度来看,前端监控体系需要做的事情包括:</p><ol><li>页面的整体访问情况,包括常见的 PV、UV、用户行为上报。</li><li>页面的性能情况,包括加载耗时、接口耗时统计。</li><li>灰度发布与有效的监控能力,方便及时发现问题。</li><li>用户反馈问题,需要足够的日志定位问题。</li></ol><p>这些问题可以从两个角度来解决:<strong>数据收集</strong>、<strong>数据上报</strong>。</p><h2 id="数据收集"><a href="#数据收集" class="headerlink" title="数据收集"></a>数据收集</h2><p>要进行有效地监控,首先我们需要将监控数据进行上报。传统的页面开发过程中,系统的质量通常从三方面来评估,针对页面的监控和数据采集也分别从这些方面来进行:</p><ul><li>页面访问速度</li><li>页面稳定性/异常</li><li>外部服务调用情况</li></ul><h3 id="异常收集"><a href="#异常收集" class="headerlink" title="异常收集"></a>异常收集</h3><p>首先,我们需要收集项目运行过程中的一些错误,因为一般来说脚本执行异常很可能会直接导致功能不可用。当 HTML 文档执行异常时,我们可以通过<code>window.onerror</code>、<code>document.addEventlistener(error)</code>、<code>XMLHttpRequest status</code>等方法拦截错误异常。例如,通过监听<code>window.onerror</code>事件,我们可以获取项目中的错误和分析堆栈,将错误信息自动上报到后台服务中。</p><p>常见的前端异常包括:</p><ul><li>逻辑错误:开发实现功能的时候,逻辑梳理不符合预期<ul><li>业务逻辑判断条件错误</li><li>事件绑定顺序错误</li><li>调用栈时序错误</li><li>错误的操作 js 对象</li></ul></li><li>代码健壮性:代码边界情况考虑不周,异常逻辑执行出错<ul><li>将 null 视作对象读取 property</li><li>将 undefined 视作数组进行遍历</li><li>将字符串形式的数字直接用于加运算</li><li>函数参数未传</li></ul></li><li>网络错误:用户网络情况异常、后台服务异常等错误<ul><li>服务端未返回数据但仍 200,前端按正常进行数据遍历</li><li>提交数据时网络中断</li><li>服务端 500 错误时前端未做任何错误处理</li></ul></li><li>系统错误:代码运行环境兼容性问题、内存不够用等问题导致出错</li><li>页面内容异常:缺少内容、绑定事件异常、样式异常</li></ul><h3 id="生命周期数据"><a href="#生命周期数据" class="headerlink" title="生命周期数据"></a>生命周期数据</h3><p>生命周期包括页面加载的关键时间点,常常包括页面打开、更新、关闭等耗时数据。</p><p>一般来说,我们可以通过 PerformanceTiming 属性获取到一些生命周期相关的数据,包括:</p><ul><li><code>PerformanceTiming.navigationStart</code>:当前浏览器窗口的前一个网页关闭,发生 unload 事件时的时间戳</li><li><code>PerformanceTiming.domLoading</code>:返回当前网页 DOM 结构开始解析时(即<code>Document.readyState</code>属性变为“loading”、相应的<code>readystatechange</code>事件触发时)的时间戳</li><li><code>PerformanceTiming.domInteractive</code>:返回当前网页 DOM 结构结束解析、开始加载内嵌资源时(即<code>Document.readyState</code>属性变为“interactive”、相应的<code>readystatechange</code>事件触发时)的时间戳</li><li><code>PerformanceTiming.domComplete</code>:返回当前文档解析完成(即<code>Document.readyState</code>变为”complete”且相对应的<code>readystatechange</code>)被触发时的时间戳</li><li><code>PerformanceTiming.loadEventStart</code>:返回该文档下,load 事件被发送时的时间戳</li><li><code>PerformanceTiming.loadEventEnd</code>:返回当 load 事件结束,即加载事件完成时的时间戳</li></ul><p>除此之外,当初始的 HTML 文档被完全加载和解析完成之后,<code>DOMContentLoaded</code>事件被触发,而无需等待样式表、图像和子框架的完全加载。由于前端框架的出现,很多时候页面的渲染交给框架来控制,因此<code>DOMContentLoaded</code>事件已经失去了原本的作用,很多时候我们会在框架本身提供的生命周期函数中进行数据的收集。</p><p>我们还可以使用<code>MutationObserver</code>接口,该提供了监听页面 DOM 树变化的能力,结合<code>performance</code>获取到具体的时间:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 注册监听函数</span></span><br><span class="line"><span class="keyword">const</span> observer = <span class="keyword">new</span> MutationObserver(<span class="function">(<span class="params">mutations</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">`时间:<span class="subst">${performance.now()}</span>,DOM树发生了变化!有以下变化类型:`</span>);</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i < mutations.length; i++) {</span><br><span class="line"> <span class="built_in">console</span>.log(mutations[<span class="number">0</span>].type);</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line"><span class="comment">// 开始监听document的节点变化</span></span><br><span class="line">observer.observe(<span class="built_in">document</span>, {</span><br><span class="line"> childList: <span class="literal">true</span>,</span><br><span class="line"> subtree: <span class="literal">true</span>,</span><br><span class="line">});</span><br></pre></td></tr></table></figure><h3 id="HTTP-测速数据"><a href="#HTTP-测速数据" class="headerlink" title="HTTP 测速数据"></a>HTTP 测速数据</h3><p>请求相关的数据,我们同样可以通过 PerformanceTiming 属性获取:</p><ul><li><code>PerformanceTiming.redirectStart</code>:返回第一个 HTTP 跳转开始时的时间戳</li><li><code>PerformanceTiming.redirectEnd</code>:返回最后一个 HTTP 跳转结束时(即跳转回应的最后一个字节接受完成时)的时间戳</li><li><code>PerformanceTiming.fetchStart</code>:返回浏览器准备使用 HTTP 请求读取文档时的时间戳,该事件在网页查询本地缓存之前发生</li><li><code>PerformanceTiming.domainLookupStart</code>/<code>PerformanceTiming.domainLookupEnd</code>:返回域名查询开始/结束时的时间戳</li><li><code>PerformanceTiming.connectStart</code>:返回 HTTP 请求开始向服务器发送时的时间戳</li><li><code>PerformanceTiming.connectEnd</code>:返回浏览器与服务器之间的连接建立时的时间戳,连接建立指的是所有握手和认证过程全部结束</li><li><code>PerformanceTiming.secureConnectionStart</code>:返回浏览器与服务器开始安全链接的握手时的时间戳</li><li><code>PerformanceTiming.requestStart</code>:返回浏览器向服务器发出 HTTP 请求时(或开始读取本地缓存时)的时间戳</li><li><code>PerformanceTiming.responseStart</code>:返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳</li><li><code>PerformanceTiming.responseEnd</code>:返回浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前 HTTP 连接已经关闭,则返回关闭时)的时间戳</li></ul><p>通过这些数据,我们可以观察后端服务是否稳定、是否还有优化空间。</p><h3 id="用户行为数据"><a href="#用户行为数据" class="headerlink" title="用户行为数据"></a>用户行为数据</h3><p>除了常见的前端页面加载、请求耗时数据,我们还可以关注用户的一些行为数据,包括页面浏览量或点击量、用户在每一个页面的停留时间、用户通过什么入口来访问该页面、用户在相应的页面中触发的行为。用户行为数据可以通过一些 DOM 元素的操作事件来获取。</p><p>这些数据通常用来统计分析用户行为,来针对性调整页面功能、更好地发挥页面的作用。同时,我们还可以通过一些用户交互数据,来观测系统功能是否正常。</p><h3 id="用户日志"><a href="#用户日志" class="headerlink" title="用户日志"></a>用户日志</h3><p>系统出现异常的时候,通常使用日志进行定位。日志的存储通常包括两种方案:</p><ol><li>上报到服务器。由于日志内容很多,如果全量上报到服务器会导致存储成本过大,同时频繁的上报也会增加接口的维护成本。除此之外,由于网络原因等还可能导致部分或全部的日志丢失等问题。</li><li>本地存储。该方案需要引导用户手动操作提交本地日志,才可以定位到具体异常出现的位置。如果无法联系到用户,则可能由于异常无法重现而无法修复。</li></ol><p>日志通常用户定位用户问题的时候使用,但我们常常需要提前在代码中打印日志。否则,当我们需要定位问题的时候,才发现自己并没有输出相关的日志,有些问题由于复现困难,再补上日志发布后也未必能复现,这样就会比较被动。</p><p>可以通过全局挟持关键模块和函数等方式来进行日志的自动打印,举个例子:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/monitor-and-report-3.jpg" alt></p><p>在每个功能模块运行时,通过使用约定的格式来打印输入参数、执行信息、输出参数,则可以通过解析日志的方式,梳理本次操作的完整调用关系、功能模块执行信息:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/monitor-and-report-4.jpg" alt></p><h2 id="埋点方案"><a href="#埋点方案" class="headerlink" title="埋点方案"></a>埋点方案</h2><p>前端常见的埋点方案包括三种:</p><table><thead><tr><th>-</th><th>代码埋点</th><th>可视化埋点</th><th>无痕埋点</th></tr></thead><tbody><tr><td>使用方式</td><td>手动编码</td><td>可视化圈选</td><td>嵌入 SDK</td></tr><tr><td>自定义数据</td><td>可自定义</td><td>较难自定义</td><td>难以自定义</td></tr><tr><td>业界成熟产品</td><td>友盟、百度统计等第三方数据统计服务商</td><td>Mixpanel</td><td>GrowingIO</td></tr><tr><td>更新代价</td><td>需要版本更新</td><td>需要下发配置</td><td>不需要</td></tr><tr><td>使用成本</td><td>高</td><td>中</td><td>低</td></tr></tbody></table><p>无痕埋点一般是通过上述数据采集中使用的一些 API 来进行数据的采集,但由于无痕埋点的自定义能力很弱,通常我们可以配合代码埋点的方式进行。</p><h3 id="标准化埋点数据"><a href="#标准化埋点数据" class="headerlink" title="标准化埋点数据"></a>标准化埋点数据</h3><p>不管是哪种埋点方式,我们都需要对它们进行标准化处理。一般来说,通过和后台约定好具体的参数,然后前端在埋点采集的时候,自动转换成接口需要的一些数据格式进行本地存储。</p><p>通过这些行为信息,可以实时计算出每个用户在时间轴上的操作顺序,以及每个步骤的操作时间、操作内容等,通过可视化系统直观地展示用户的链路情况,包括系统的入口来源、打开或关闭的页面、每个功能点的点击和操作时间、功能异常的情况等。</p><p>使用标准化的方式获取用户点击流以及页面使用情况,将页面和每个功能的操作行为上报到服务器,实时对操作时间、操作名称等信息来分析得到用户的操作链路、每个页面和功能操作步骤间的耗时和转化率,并进行有效监控。通过该方式,可以高效直观地观察产品的使用情况、分析用户的行为习惯,然后确定产品方向、完善产品功能。</p><h2 id="数据上报"><a href="#数据上报" class="headerlink" title="数据上报"></a>数据上报</h2><p>数据采集完成后,我们需要将这些数据上报到后台服务:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/monitor-and-report-2.jpg" alt></p><p>如图,当页面打开、更新、关闭等生命周期、用户在页面中的操作行为、系统异常等触发时,系统底层通过埋点监听这些事件,获取相关数据数据并进行标准化处理后,进行本地收集然后上报到实时数据分析系统。</p><p>相关的数据信息包括时间、名称、会话标记、版本号等信息,通过这些数据,可以实时计算出每个埋点的使用数量、埋点间的执行时间、埋点间的转换率等,通过可视化系统直观地展示完整的页面使用情况,包括每个页面打开、更新、关闭情况、每个功能点的点击和加载情况、功能异常的情况等。</p><h3 id="上报方式"><a href="#上报方式" class="headerlink" title="上报方式"></a>上报方式</h3><p>一般来说,我们埋点的数据、运行的日志都需要通过上报发送到后台服务再进行转换、存储和监控。</p><h4 id="批量上报"><a href="#批量上报" class="headerlink" title="批量上报"></a>批量上报</h4><p>对于前端来说,过于频繁的请求可能会影响到用户其他正常请求的体验,因此通常我们需要将收集到的数据存储在本地。当收集到一定数量之后再打包一次性上报,或者按照一定的频率(时间间隔)打包上传,打包上传将多次数据合并为一次,可以减轻服务器的压力。</p><h4 id="关键生命周期上报"><a href="#关键生命周期上报" class="headerlink" title="关键生命周期上报"></a>关键生命周期上报</h4><p>由于用户可能在使用过程中遇到异常,或者在使用过程中退出,因此我们还需要在异常触发的时候、用户退出程序前进行上传,以避免问题没能及时发现和定位。</p><h4 id="用户主动提交"><a href="#用户主动提交" class="headerlink" title="用户主动提交"></a>用户主动提交</h4><p>一些异常和使用体验问题,我们会给用户提供主动上传的选项。当用户经过引导后进行上传的时候,我们则可以将本地的数据和日志一并进行提交。</p><h2 id="数据监控"><a href="#数据监控" class="headerlink" title="数据监控"></a>数据监控</h2><p>数据上报完成后,我们需要搭建管理端对这些数据进行有效的监控,主要包括三部分的数据:</p><ul><li>性能监控<ul><li>网页加载性能</li><li>网络请求性能</li></ul></li><li>异常监控<ul><li>JS Error</li></ul></li><li>数据监控<ul><li>页面 PV/UV</li><li>页面来源</li></ul></li></ul><p>日常监控中,我们可以通过对这些监控数据配置告警阈值等方式,结合邮件、机器人等方式推送到相关的人员,来及时发现并解决问题。</p><h3 id="发布过程监控"><a href="#发布过程监控" class="headerlink" title="发布过程监控"></a>发布过程监控</h3><p>多人协作的项目,由于每次发版都会把好几个小伙伴开发的功能一起合并发布,人工保证功能的正确是很低效的,人工测试也不一定能覆盖到很完整的功能、自动化测试也常常因为性价比等问题无法做得很完善。所以除了自动化测试、改动相关的功能自测之外,我们上报过程会带上每次的版本号,同时可以根据版本来观察新版本的曲线情况,在灰度过程也需要小心注意观察:</p><ul><li>小程序错误告警是否有新增错误,可通过错误内容找到报错位置修复</li><li>全版本监控观察:整体的功能点覆盖曲线是否正常,是否有异常涨跌</li><li>分版本监控观察:功能是否覆盖完整、灰度占比是否正常、新旧版本的转化率是否一致</li></ul><p>在灰度发布过程中,我们就能通过上报数据功能曲线是否正常、异常是否在预期范围、曲线突变跟灰度时间点是否吻合等,来确认是否有异常、哪里可能有异常。当出现数据异常的时候,可配合相应的告警渠道来及时通知相应的负责人,及时修复功能异常。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/monitor-and-report-1.jpg" alt></p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>很多时候,前端项目中都会进行一些异常、耗时测速等监控,也会进行一些用户行为的数据上报。其实我们还可以思考将这些过程更加自动化地实现,同时数据在上报之后还可以进行筛选、统计、转换,计算出产品各种维度的使用情况,甚至还可以做全链路监控、或是给到一些实用的产品方向引导。</p>]]></content>
<summary type="html">
<p>整理了下前端监控的一些项目经验,结合自己的想法输出了这篇文章,跟大家分享下。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>前端这几年--10.我的工作历险记</title>
<link href="https://godbasin.github.io/2020/08/30/about-front-end-10/"/>
<id>https://godbasin.github.io/2020/08/30/about-front-end-10/</id>
<published>2020-08-30T08:20:01.000Z</published>
<updated>2020-08-30T08:49:29.075Z</updated>
<content type="html"><![CDATA[<p>这几年前端的竞争本来就很大,而今年的疫情更加是雪上加霜。虽然现在的我工作也算相对稳定一点,但这些年的职场经历也特别丰富,如果写下来,能给大家一些能量,继续坚持自己想做的一些事情,也算是很足够了。</p><a id="more"></a><h2 id="为什么写这篇文章"><a href="#为什么写这篇文章" class="headerlink" title="为什么写这篇文章"></a>为什么写这篇文章</h2><p>之前有收到一个赞赏让我很心酸,对方给我打赏的同时,附了一段话:</p><blockquote><p>“抱歉只有这点钱,今年没有找到工作,但是很喜欢您的文章”</p></blockquote><p>很感谢,很感动,却又很难过。</p><p>我很少去推广自己的一些文章,很多关注我博客的小伙伴,都是通过搜索的时候找到的一些解答(我博客在谷歌搜索下面还是挺靠前的哈哈哈哈)。偶尔会收到一些打赏,不管多少钱都好,大家的留言都很能激励我,也让我更有继续写下去的动力,更是我决定想要开源书的缘由。因为,有人需要。</p><p>我想,从校园刚出来的大家,多多少少都会不适应社会的节奏。运气好的,可以遇到好的导师、团队,在大家的帮助和自身的努力之下,快速适应而且开始施展拳脚。运气差的,可能会被各种甩锅冲晕了头,在职场PUA中开始怀疑自我,最后被这个行业排斥和抛弃。</p><p>我想,我的经历也算是比较曲折,虽然卖惨不是我的本意(我也不觉得自己惨,相反我觉得自己能走到现在,很幸运)。但如果说,这样的经历能给到不管是谁也好,一些坚持下去的动力。</p><p>那么,我来了,我带着我的故事来了。</p><h2 id="踏入互联网"><a href="#踏入互联网" class="headerlink" title="踏入互联网"></a>踏入互联网</h2><p>是的,我是非科班的程序员。</p><p>大学的时候学的物理,毕业的时候去了华为,在华为做的是交换机路由器防火墙这类型的技术支持。在那边的培训也好、转正前的实践也好,成绩也都不错。</p><p>在校的时候,我是个特别爱玩的人。会自己省着零花钱、去打工存点钱,然后到处玩,云南、新疆、青海、甘肃、海南、浙江、江苏、山东、北京、黑龙江,基本都逛过了。那时候自己还是个小朋友,喜欢流浪的感觉,喜欢自由的感觉,心野的不行。</p><p>我一直以为,自己会很喜欢技术支持这种全国到处跑的工作的。万万没想到的是,我竟然喜欢不起来这份工作。一个人,日日夜夜地独自住在酒店里,移动、联通、各处的机房和酒店两点一线的生活,除了机器甚至没有个说话的对象。</p><p>而这份工作又有特殊的地方,就是基本上是夜里12点之后才开始干活。于是我日夜颠倒,有天夜里出去打车的时候,遇到了一直超粘人的小猫,它跟了我一路,但住在酒店里的我却没法收留它。</p><p>于是,我裸辞了。</p><p>辞职之后,还跟一些校园里的小孩玩过不到几个月的所谓创业,后来由于创业内容过于不靠谱、各自又解散了。我重新面临找工作,而由于毕业还不到一年,没有多少的工作经历,又不想继续之前的工作内容。我翻了下自己手上的筹码,唯一能扯上关系的,剩下了在大学里因为觉得好玩而跟教授做的一个项目–Web物理引擎。</p><p>其实这个项目我并没有什么参与,当时都在跟着导师、师兄师姐们吃吃喝喝,也只是简单看过w3c上的一些入门资料。而因为这个契机,我的毕业项目也是用这样的web引擎写了个游戏(看着好厉害的样子,其实也是简单得不行的那种)。</p><p>然后,我借住在同学租的房子里,硬生生地学习了几天的前端知识,然后各种投简历找工作。</p><p>那时候前端还比较简单,就写写样式、写写简单的页面交互,所以后来我被一个小公司录用了。录用我的是三个后台大哥,老板甚至看我学历、长得还行,还发了一个秘书的工作,待遇比前端的多多了。</p><p>最后,由于种种的原因,还是选了做前端,加入了互联网。</p><h2 id="底层人民的挣扎"><a href="#底层人民的挣扎" class="headerlink" title="底层人民的挣扎"></a>底层人民的挣扎</h2><p>之前看到知乎上有个提问:收入断崖式下降是怎样的体验?我觉得我是很有资格去回答的。</p><p>从华为出来,加入一个外包小公司做前端,我的工资掉剩下了30%。那是挺苦的一段日子,跟16个女生住在一个三室的出租屋里,只有一个洗手间,我每天都早早跑去公司里上厕所。下班后,也是排队跟大家错开时间洗澡。因为没有多少钱,每天就啃啃面包和泡面,偶尔能蹭上公司一两顿饭。</p><p>但那段时间,每天都能学到特别多的知识,后台的几位大哥也都给我很多的指导,例如要掌握哪些工具、可以去哪些网站里学。即使是下班后,也依然躲在上铺里开着台灯,一直学到半夜。</p><p>小公司的问题很多,没有社保、没有福利、也不提供住宿和三餐,老板也是那种暴发户类型的。因为工资实在是少得可怜,转正的时候我提出需要涨点工资,然后被老板一顿“不知好歹、不懂感恩”的批,最后不了了之。我自己知道,这样下去也不是办法,后台几位大哥也支持我出去看看。</p><p>然后这些年来,最艰难的时候来了。</p><h2 id="现实它特么的骨感"><a href="#现实它特么的骨感" class="headerlink" title="现实它特么的骨感"></a>现实它特么的骨感</h2><p>我找好了一个工资稍微好些的工作,然后跟老板提离职。</p><p>刚开始老板温和地劝说,后来发现我很坚决,立马翻脸,一边骂我狼心狗肺,一边威胁我说:深圳就这么大,你以为你可以去哪。</p><p>说实话,那时候的我,真的被吓坏了。我跟下个公司的负责人说起这件事,他安慰我说不用管,都是吓唬小朋友的。在犹豫很久之后,我还是离开了。</p><p>那段日子,其实家人因为种种原因,也不大支持,一直想让我离开深圳回家。后来因为一些事情吵翻了,家人甚至发话说如果我要坚持就不认这个女儿了。那时候的我也特别叛逆和倔强,于是我们断了联系好几个月了。甚至有段时间,会有很多不好的念头,我每天都需要跟这样的念头斗争,生活它真是太**难了。</p><p>新的公司是一个实体到互联网转型中的公司,刚去不到几天,可能因为身体熬不住了,就开始不间断地肚子疼、发烧、发冷。但是由于刚换的工作,也不好请假,就那样熬了差不多两三周。直到有一天,走路都成为了困扰,就打车去医院检查。</p><p>因为疼得厉害,我躺在医院过道边上的一个床上,吐得脑袋都不大清醒,来来往往的人都看着我,脸上各种疑惑、犹豫、欲言又止的表情,却没有一个人问问我。</p><p>然后,我收到了入院通知。</p><p>至今我还记得那天,我跟公司的负责人说了这事,需要请几天假。对方在回了我一句“女生就是矫情”之后,不到一分钟,我被移除出所有相关的群,然后被辞退了,甚至这几周的工作、一丁点工资都没有给我。</p><p>医院的床位满了,医生给我在过道上加了一张床,告诉我第二天要做多少多少的检查,然后安排手术。我看着天花板,一切都显得那么不真实。后来微信咨询了一位学医的朋友,他一个电话过来,告诉我病情特别紧急,让我挂了电话之后马上联系家人,语气非常坚决,我乖乖地打了电话。</p><h2 id="所以说身体健康比任何事情都重要"><a href="#所以说身体健康比任何事情都重要" class="headerlink" title="所以说身体健康比任何事情都重要"></a>所以说身体健康比任何事情都重要</h2><p>后面的事情,大概就是我哥把我接回家,离开医院的时候医生给了警告、还签了个出院概不负责的协议。</p><p>也正因为这个病,跟家里闹僵的关系有所缓和。后面的一两个月,基本上都是各种打针吃药、尝试把烧退下来之后,安排起了手术。那段时间身上全都是针口,到后面护士都找不到可以扎针的地方了。手术后因为并发症,住了好几天的ICU,我也深刻记得当时为了降温,医生在我身边放满了冰块,我跟医生说好冷,医生让我忍忍。</p><p>后来出院后,我回家称了下,自己的体重回到了初中的时候,瘦的睡觉的时候都会被骨头磕着。然后开始了长达半年的恢复,状态好点的时候,就买了特别多的技术书籍,在家除了休息、缓慢的锻炼,就看书、写代码。</p><p>生病的那段日子,我是没有勇气重来一遍的。</p><p>其实我特别不愿意提及这段经历,因为实在是太难了,也算是比较隐私。甚至也有可能以后哪天,我的公司说考虑到我身体的因素、建议我换个低强度的地方,因为之前的经历,我相信这是绝对可能发生的事情。</p><p>但我之所以写出来,是因为看到现在互联网行业里,所谓的福报、996、007现象特别严重。而让我担忧又毫无办法的是,很多年轻人没日没夜地通宵熬夜,因为内卷太严重了、大家都没有更多的精力去考虑健康这件事。有句老话叫“不见棺材不落泪”,真心希望大家在拥有的时候,就知道要珍惜。</p><p>如果我的经历,可以让哪怕一个人,能正视自己、珍惜自己的话,那也值得了。</p><h2 id="职场里真的什么事情都会发生"><a href="#职场里真的什么事情都会发生" class="headerlink" title="职场里真的什么事情都会发生"></a>职场里真的什么事情都会发生</h2><p>身体恢复之后,我又回来了深圳。</p><p>在社会摸爬滚打,你会遇到各式各样的人。这六年的工作里,我的三观被无数次刷新。</p><p>有那种已经结婚有娃的,突然喜欢上你。各种明示暗示划清界限都被解读为“是因为喜欢才生气、因为喜欢才这样”,甚至在周末的时候跟我说在我家附近,吓得不敢出门。而因为工作上有直接的接触没办法完全杜绝联系,跟上级反馈了,上级也找对方隐晦地谈过,依然没有任何效果。甚至对方在知道我投诉他之后,特别生气疯狂指责我。这事直到我离职后,他突然跑过来各种约我挽回等,直接把我在公司里吓哭了,最后其他人找他谈,以及断绝了联系之后,才清净了不少。</p><p>还有各种奇葩领导喜欢软硬兼施、威胁谈判、PUA 打压。像有段时间不舒服,回家比较早,就被上级喊去聊天,说领导都没走你怎么好意思走。尝试解释说最近不大舒服,还被反复追问是不是怀孕,在我坚决否定之后依然锲而不舍地喊我去做个检查。像有导师和上级安排方向不一致,又不知道听谁的,最后被盖上了“挑活干、不尊师重道”等各式各样的帽子,还是从其他人口中得到的消息,他们在认真劝我不要太有主见。</p><p>也有一些24小时不间断微信、大半夜突然让出个方案、回答各种不紧急问题的。甚至有次我家人出事请假、还守在抢救室外、生死不明的时候,对方在我明确告知当时情况,依然在当天晚上给我发“今年要淘汰20%的人,你又面临要晋级、又面临年纪大了可能要生娃,要加倍努力”等消息。在我和家人24小时倒班照顾的时候,各种反复因为一个不重要不紧急的项目让我远程工作。</p><p>是的,可能对很多人说,这些事情它们都很正常。这个社会就是很现实、生活就是那么难,难到大家对人与人之间的冷漠、自私都习以为常。而这也是我至今对深圳喜欢不起来的一个原因,社畜这词真的特别贴切。</p><p>唯一庆幸的是,自己到现在三观还算正常,甚至正常得有点不正常。在觉察到“这样不对”之后,我可以当机立断地进入下一段旅程。而正是经历过各式各样的“神仙”和妖魔鬼怪之后,更是越发的珍惜一些人一些事。</p><p>所以我特别喜欢现在的团队,也特别珍惜上一个团队。如果可以的话,希望这样的美好能一直保持下去。竞争它的确残酷,但这不意味着我们不能拥有友情、不能互相帮助。</p><p>金钱它的确有很大很多的魅力,而我偏爱人性的温度。</p><h2 id="所以,请不要放弃"><a href="#所以,请不要放弃" class="headerlink" title="所以,请不要放弃"></a>所以,请不要放弃</h2><p>我说了很多,很多很多。我想要说这些,并不是卖惨、也不是要把大家吓跑。</p><p>人生它总是不如意,生活也不总是那么轻松。如果我们要去向远方,要明白这一路上肯定会很坎坷。这其中有些坑是一定会踩到的,但还有些不一定要掉进去的。</p><p>我想说的是,你要有这样的心理准备。在碰到一些奇奇怪怪的事情的时候,不要陷入自我怀疑,不要丢失前进的勇气。虽然我离想去的地方依然很远,但现在我稍微站在你们前面一点点,向你们招手:看,这路上陷阱再多,我也走过来了。</p><p>我想说的是,如果路真的很难行,结伴同行可以减轻一些包袱。即使遭遇不好的事情,在学会如何保护自己的时候,希望你们还能保留一些对身边人的信任。如果说我们改变不了一些事情,那么不被改变也是一种胜利。</p><p>我们总在觉得很难的时候,承受的压力很大,赖在地上发脾气:我不走了。</p><p>我想说的是,在情绪高涨、愤怒和不甘占据了所有情绪的时候,请不要匆忙做下决定。时间它是往前走的,我们唯一无法挽回的是每一次的选择。我希望你们在做一些重要决定的时候,可以先冷静思考,认真想清楚它是不是你想要的,你是否又会后悔呢。</p><p>所以,即使再难,也不要轻言放弃。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>这个世界它真的不大友好,单单是我自己的经历,都不是用文字可以描述完的。可怕的是,我的人生才刚刚开始向前迈进,距离目的地还特别特别远。但正因为它就是这么难,我希望可以的话,能给在夜里行走的你们,点上一盏灯。即使灯光再微弱,你依然可以看得到自己。</p><p>不要被前路的黑暗吓退了,远处的风景它真的很美好。</p>]]></content>
<summary type="html">
<p>这几年前端的竞争本来就很大,而今年的疫情更加是雪上加霜。虽然现在的我工作也算相对稳定一点,但这些年的职场经历也特别丰富,如果写下来,能给大家一些能量,继续坚持自己想做的一些事情,也算是很足够了。</p>
</summary>
<category term="工作这杯茶" scheme="https://godbasin.github.io/categories/%E5%B7%A5%E4%BD%9C%E8%BF%99%E6%9D%AF%E8%8C%B6/"/>
<category term="心态" scheme="https://godbasin.github.io/tags/%E5%BF%83%E6%80%81/"/>
</entry>
<entry>
<title>前端这几年--9.提升工作效率</title>
<link href="https://godbasin.github.io/2020/08/30/about-front-end-9/"/>
<id>https://godbasin.github.io/2020/08/30/about-front-end-9/</id>
<published>2020-08-30T01:03:21.000Z</published>
<updated>2020-08-30T08:48:30.462Z</updated>
<content type="html"><![CDATA[<p>效率二字在工作中已经是老生常谈了,但反观我们的日常工作里,其实依然有很多可以改进的地方。效率提升了,我们可以把时间花在自己想花的地方了。</p><a id="more"></a><h1 id="自我管理"><a href="#自我管理" class="headerlink" title="自我管理"></a>自我管理</h1><p>效率提升的大部分都是关于自我管理的,这里分享几个对我自己来说比较实用的技巧。</p><h2 id="时间管理"><a href="#时间管理" class="headerlink" title="时间管理"></a>时间管理</h2><p>相信每个人都有自己效率较高的工作时间,这里我们可以将自己每天的工作时间分成几个部分,包括:零碎的时间段(30 分钟内)、连续较短的时间段(1-2 个小时)、连续较长的时间段(2 个小时以上)。</p><p>我们用空瓶子填石头的方法,先把长的时间段划出来,用来专注写代码实现功能。接下来是较短的连续时间段,可以用来做代码测试、问题定位和修复等事情。最后是零碎的时间段,可以用来规划工作内容、与协作方(产品、设计、其他开发)沟通、总结复盘等内容。</p><p>来举个栗子说明一下。</p><p>例如我们程序员一般每天 9 点多才到公司,上午的工作时间大概 2 个小时左右。可用于准备今日的 Todo List,梳理今日工作内容并进行分块(根据工作量大小划分),梳理需要的资源并推动相应的依赖方,这个过程大概 0.5-1 小时。上午还剩下 1 个多小时,可以挑某个预估时间差不多的活来干。</p><p>然后就是开心的下午时间了,一般下午有 4-5 小时的时间。除去可能进行的会议,可以进行较大块的工作内容。可以以小时为单位来划分,每完成一个任务就稍作休息,上个厕所、接杯水喝等。这个过程可能会被各种人打断,来问某个功能是否能实现的产品、来咨询某些问题怎么处理的甲方,不过我们的任务是以小时为单位划分的话,影响也不会很大。</p><p>然后是晚上。一般晚上也有 2、3 个小时以上的时间可以干活,而且这个时候开会比较少、被人打扰的情况也较少,所以很多程序员的最高效开发时间在晚上。同样的可以按照以小时为单位的任务来进行,晚上还有比较重要的一件事,就是根据早上梳理的工作内容,回顾今天的事情是否顺利完成,如果有一些心得体会,可以简单地记录下来,等空闲或者周末的时候再集中整理。</p><p>大家可以根据自己的个人情况来进行调整,例如我个人习惯是 10 点以后尽量不写代码,因为会越写越精神晚上睡不着,当然也有很多人喜欢 10 点以后开始写代码。</p><h2 id="Todo-List"><a href="#Todo-List" class="headerlink" title="Todo List"></a>Todo List</h2><p>Todo List 的梳理其实是高效工作里最重要的一个步骤。前面我们说到通过时间管理的方式来提升工作效率,而我们可以将时间划分为时间段的前提,则是梳理好我们的 Todo List。</p><h3 id="养成备忘的习惯"><a href="#养成备忘的习惯" class="headerlink" title="养成备忘的习惯"></a>养成备忘的习惯</h3><p>要怎么写好一个 Todo List 呢?其实关键是养成备忘的习惯。</p><p>我们工作中除了代码开发以外,涉及许多其他的零碎的事情,例如开会、问题响应、临时问题定位、内容整理和输出等等。当事情很多、又突然被打断或者打乱的时候,我们常常会抓耳挠腮。那么是什么导致了事情太多太乱呢?是因为我们记不住。</p><p>所以我们可以在每次突然接到一个新的任务的时候,或者突然想起某个事情要做的时候,就可以先简单地在一个习惯的地方记录下来。尽量在一个统一的地方记录,免得想不起来记在哪里或是遗漏了,就达不到我们的目的了。可以是手边备一个笔记本、手机备忘录、微信文件传输助手等等。</p><p>记录的时候可以根据完成或者进行的日期来维护,例如“今天”、“本周”、“周末”、“XX月”,这样我们每次有新的内容补充,都可以继续填在原有的内容后面,而查阅起来也比较方便。</p><h3 id="优先级排序"><a href="#优先级排序" class="headerlink" title="优先级排序"></a>优先级排序</h3><p>除了维护这样一个 Tode List 备忘列表,我们还需要对这个列表进行优先级排序。</p><p>我们待完成的任务中,都会有重要程度、紧急程度的区分。每次更新备忘的时候,都可以重新确认一下优先级。这样,当我们拿到这么一个 Todo List 的时候,就可以直接按照上面的顺序来先后完成,不用再因为“时间不够、事情太多”而烦躁了。</p><p>除此之外,我们还需要对任务的耗时做一个预估,因为根据任务所需要的时间,我们很可能需要匹配自身拥有的时间来调整优先级顺序。</p><p>例如,我这周末打算写一篇文章、看某一本书的两章内容,还需要做饭、打扫卫生,剩下的时间去动物之森集合。那么我需要评估下每个任务的时间,然后根据上下午、晚上的时间段来对应划分,最终根据合适的方式来排序,得到我的周末 Todo List。</p><p>我只需要在特定的时间段完成这个任务,多出来的时间就可以去玩游戏啦!</p><h1 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h1><p>除了与自身时间管理、任务管理相关的事情以外,我们工作中效率不高的原因常常还因为低效的沟通、重复性的工作等,这些也是需要提升效率的地方。</p><h2 id="协作与沟通"><a href="#协作与沟通" class="headerlink" title="协作与沟通"></a>协作与沟通</h2><p>首先,我们来看看常常“浪费时间”的协作和沟通问题。如果你仔细观察,你会发现我们的工作中会经常出现这样的情况,两个人争了半天才发现讲的不是一个事情,或者关注的重点不一致。那么,我们要怎么避免这种情况呢?</p><p>首先是换位思考。我们需要知道对方在想什么,怎么知道呢?问。在对方说了一堆之后,你可以尝试通过上下文找到他的疑惑点,然后用反问的方式梳理一下,“你想知道的是这个吗”、“我这样理解对吗”,这样对方会停止急于表达自己的行为,从而来尝试理解你说的是否正确。通过这样一个小技巧,你们双方都可以进行换位思考,只要问题达成一致,剩余的讨论就不会偏离方向导致浪费时间了。</p><p>不要陷入情绪。我们在讨论问题的时候容易带入情绪,方案被否决会觉得自己被否定,从而觉得丢脸导致不能理性分析。同样的,我们在提出对方的问题的时候,也需要关注对方的情绪,尽量对事不对人。如果对方是个敏感骄傲的人,可以尝试私下进行二次对话,避免公众场合的争执。</p><p>然后最重要的一点是准确地表达。即使工作很多年了,你依然会发现很多人甚至不能好好地描述问题。如果连问题都讲不清楚,对方又如何能提供帮助呢?表达的时候可以先写下来,自己尝试阅读几遍或者念出来,看看能否很好地理解、是否完整地说明了问题,然后才给到对方这个信息。</p><h2 id="重复性工作"><a href="#重复性工作" class="headerlink" title="重复性工作"></a>重复性工作</h2><p>重复性的工作,常常让我们兴趣黯然又越做越烦。</p><p>很多人会抱怨,为什么要给我安排这种没有技术含量的活。而这种没有技术含量、重复性的工作为什么占用你这么多时间,有仔细分析过吗?瓶颈在哪呢?</p><p>我们可以对手上这些“总是反复出现”、“低级”的工作进行分析,是做了某个工具、对方使用的时候总是问很基础的问题?那么这样的问题是否可以沉淀到文档中,给到详细的指引说明、参考链接呢?又或者是因为手上的工作总是要复制粘贴,那么身为程序员的我们是否可以写个脚本或者工具去自动化完成或者局部完成呢?</p><p>如何对待重复性、没有技术含量的工作,才是最能体现我们解决问题能力的地方。不要急躁,好好思考和分析,一步一步去解决吧。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>不要让自己埋没在工作中喘不过气,解决掉低效的工作内容,解放自己,你可以走得更远,也更开心。</p>]]></content>
<summary type="html">
<p>效率二字在工作中已经是老生常谈了,但反观我们的日常工作里,其实依然有很多可以改进的地方。效率提升了,我们可以把时间花在自己想花的地方了。</p>
</summary>
<category term="工作这杯茶" scheme="https://godbasin.github.io/categories/%E5%B7%A5%E4%BD%9C%E8%BF%99%E6%9D%AF%E8%8C%B6/"/>
<category term="心态" scheme="https://godbasin.github.io/tags/%E5%BF%83%E6%80%81/"/>
</entry>
<entry>
<title>补齐Web前端性能分析的工具盲点</title>
<link href="https://godbasin.github.io/2020/08/29/front-end-performance-analyze/"/>
<id>https://godbasin.github.io/2020/08/29/front-end-performance-analyze/</id>
<published>2020-08-29T01:55:23.000Z</published>
<updated>2020-08-29T07:08:26.160Z</updated>
<content type="html"><![CDATA[<p>最近依然在研究大型项目,而大型项目最容易遇到的问题便是性能问题。一般来说,当我们遇到性能瓶颈的时候,才会开始去进行相应的分析。分析的方向除了业务本身的特点相关之外,常见的我们还可以借助一些工具来发现问题。本文一起来研究下,前端性能分析可以怎么走~</p><a id="more"></a><h1 id="前端性能分析工具(Chrome-DevTools)"><a href="#前端性能分析工具(Chrome-DevTools)" class="headerlink" title="前端性能分析工具(Chrome DevTools)"></a>前端性能分析工具(Chrome DevTools)</h1><p>一般来说,前端的性能分析通常可以从<strong>时间</strong>和<strong>空间</strong>两个角度来进行:</p><ul><li><strong>时间</strong>:常见耗时,如页面加载耗时、渲染耗时、网络耗时、脚本执行耗时等</li><li><strong>空间</strong>:资源占用,包括 CPU 占用、内存占用、本地缓存占用等</li></ul><p>那么,下面来看看有哪些常见的工具可以借来用用。由于我们的网页基本上跑在浏览器中,所以基本上大多数的工具都来源于浏览器自身提供,首当其冲的当然是 <a href="https://developers.google.com/web/tools/chrome-devtools" target="_blank" rel="noopener">Chrome DevTools</a>。本文我们也主要围绕 Chrome DevTools 来进行说明。</p><h2 id="Lighthouse"><a href="#Lighthouse" class="headerlink" title="Lighthouse"></a>Lighthouse</h2><p><a href="https://github.com/GoogleChrome/lighthouse" target="_blank" rel="noopener">Lighthouse</a> 的前身是 Chrome DevTools 面板中的 Audits。在 Chrome 60 之前的版本中, 这个面板只包含网络使用率和页面性能两个测量类别,从 Chrome 60 版本开始, Audits 面板已经被 Lighthouse 的集成版取代。而在最新版本的 Chrome 中,则需要单独安装 Lighthouse 拓展程序来使用,也可以通过脚本来使用。</p><h3 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h3><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_7.png" alt="Lighthouse 架构"></p><p>下面是 Lighthouse 的组成部分:</p><ul><li>驱动(Driver):和 <a href="https://chromedevtools.github.io/devtools-protocol/" target="_blank" rel="noopener">Chrome Debugging Protocol</a> 进行交互的接口</li><li>收集器(Gatherers):使用驱动程序收集页面的信息,收集器的输出结果被称为 Artifact</li><li>审查器(Audits):将 Artifact 作为输入,审查器会对其运行测试,然后分配通过/失败/得分的结果</li><li>报告(Report):将审查的结果分组到面向用户的报告中(如最佳实践),对该部分应用加权和总体然后得出评分</li></ul><h3 id="主要功能"><a href="#主要功能" class="headerlink" title="主要功能"></a>主要功能</h3><p>Lighthouse 会在一系列的测试下运行网页,比如不同尺寸的设备和不同的网络速度。它还会检查页面对辅助功能指南的一致性,例如颜色对比度和 ARIA 最佳实践。</p><p>在比较短的时间内,Lighthouse 可以给出这样一份报告(可将报告生成为 JSON 或 HTML):</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_2.png" alt="Lighthouse 架构"></p><p>这份报告从 5 个方面来分析页面: <strong>性能</strong>、<strong>辅助功能</strong>、<strong>最佳实践</strong>、<strong>搜索引擎优化</strong>和 <strong>PWA</strong>。像性能方面,会给出一些常见的耗时统计。除此以外,还会给到一些详细的优化方向。</p><p>如果你希望短时间内对你的网站进行较全面的评估,可以使用 Lighthouse 来跑一下分数,确定大致的优化方向。</p><h2 id="Performance-面板"><a href="#Performance-面板" class="headerlink" title="Performance 面板"></a>Performance 面板</h2><p><a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference" target="_blank" rel="noopener">Performance</a> 面板同样有个前身,叫 <a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/timeline-tool?hl=zh-cn" target="_blank" rel="noopener">Timeline</a>。该面板用于记录和分析<strong>运行时性能</strong>,运行时性能是页面运行时(而不是加载)的性能。</p><h3 id="使用步骤"><a href="#使用步骤" class="headerlink" title="使用步骤"></a>使用步骤</h3><p>Performance 面板功能特别多,具体的分析也可以单独讲一篇了。这里我们简单说一下使用的步骤:</p><ol><li>在隐身模式下打开 Chrome。隐身模式可确保 Chrome 以干净状态运行,例如浏览器的扩展可能会在性能评估中产生影响。</li><li>在 DevTools 中,单击“Performance”选项卡,并进行一些基础配置(更多参考<a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance" target="_blank" rel="noopener">官方说明</a>)。</li><li>按照提示单击记录,开始记录。进行完相应的操作之后,点击停止。</li><li>当页面运行时,DevTools 捕获性能指标。停止记录后,DevTools 处理数据,然后在 Performance 面板上显示结果。</li></ol><h3 id="主要功能-1"><a href="#主要功能-1" class="headerlink" title="主要功能"></a>主要功能</h3><p>关于 Performance 怎么使用的文章特别多,大家网上随便搜一下就能搜到。一般来说,主要使用以下功能:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_5.jpg" alt></p><ul><li><strong>查看 FPS 图表</strong>:当在 FPS 上方看到红色条形时,表示帧速率下降得太低,以至于可能损害用户体验。通常,绿色条越高,FPS 越高</li><li><strong>查看 CPU 图表</strong>:CPU 图表在 FPS 图表下方。CPU 图表的颜色对应于性能板的底部的 Summary 选项卡</li><li><strong>查看 火焰图</strong>:火焰图直观地表示出了内部的 CPU 分析,横轴是时间,纵轴是调用指针,调用栈最顶端的函数在最下方。启用 JS 分析器后,火焰图会显示调用的每个 JavaScript 函数,可用于分析具体函数</li><li><strong>查看 Buttom-up</strong>:此视图可以看到某些函数对性能影响最大,并能够检查这些函数的调用路径</li></ul><p>具体要怎么定位某些性能瓶颈,可以参考<a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference" target="_blank" rel="noopener">官方文档系列文章</a>,这里就不详细介绍啦。</p><h3 id="Performance-Monitor"><a href="#Performance-Monitor" class="headerlink" title="Performance Monitor"></a>Performance Monitor</h3><p>打开 Chrome 控制台后,按组合键<code>ctrl + p</code>(Mac 快捷键为<code>command + p</code>),输入<code>> Show Performance Monitor</code>,就可以打开 Performance Monitor 性能监视器。主要的监控指标包括:</p><ul><li>CPU usage:CPU 占用率</li><li>JS head size:JS 内存使用大小</li><li>DOM Nodes:内存中挂载的 DOM 节点个数</li><li>JS event listeners:事件监听数</li><li>…:其他等等</li></ul><p>大多数情况下,我们在进行性能优化的时候,使用上面一些工具也足以确定大致的优化方向。更多的细节和案例,就不在这里详述了。</p><h1 id="前端性能监控"><a href="#前端性能监控" class="headerlink" title="前端性能监控"></a>前端性能监控</h1><p>除了具体的性能分析和定位,我们也经常需要对业务进行性能监控。前端性能监控包括两种方式:合成监控(Synthetic Monitoring,SYN)、真实用户监控(Real User Monitoring,RUM)。</p><h2 id="合成监控"><a href="#合成监控" class="headerlink" title="合成监控"></a>合成监控</h2><p>合成监控就是在一个模拟场景里,去提交一个需要做性能审计的页面,通过一系列的工具、规则去运行你的页面,提取一些性能指标,得出一个审计报告。例如上面介绍的 Lighthouse 就是合成监控。</p><p>合成监控的使用场景不多,一般可能出现在开发和测试的过程中,例如结合流水线跑性能报告、定位性能问题时本地跑的一些简单任务分析等。该方式的优点显而易见:</p><ul><li>可采集更丰富的数据指标,例如结合 <a href="https://chromedevtools.github.io/devtools-protocol/" target="_blank" rel="noopener">Chrome Debugging Protocol</a> 获取到的数据</li><li>较成熟的解决方案和工具,实现成本低</li><li>不影响真实用户的性能体验</li></ul><h2 id="真实用户监控"><a href="#真实用户监控" class="headerlink" title="真实用户监控"></a>真实用户监控</h2><p>真实用户监控,就是用户在我们的页面上访问,访问之后就会产生各种各样的性能指标。我们在用户访问结束的时候,把这些性能指标上传到我们的日志服务器上,进行数据的提取清洗加工,最后在我们的监控平台上进行展示的一个过程。</p><p>我们提及前端监控的时候,大多数都包括了真实用户监控。常见的一些性能监控包括加载耗时、DOM 渲染耗时、接口耗时统计等,而对于页面加载过程,可以看到它被定义成了很多个阶段:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_6.png" alt="RUM 性能模型"></p><p>而我们要做的,则是在力所能及的地方进行打点、计算、采集、上报,该过程常常需要借助 Performance Timeline API。将需要的数据发送到服务端,然后再对这些数据进行处理,最终通过可视化等方式进行监控。因此,真实用户监控往往需要结合业务本身的前后端架构设计来建设,其优点也比较容易理解:</p><ul><li>完全还原真实场景,减去模拟成本</li><li>数据样本足够抹平个体的差异</li><li>采集数据可用于更多场景的分析和优化</li></ul><p>对比合成监控,真实用户监控在有些场景下无法拿到更多的性能分析数据(例如具体哪里 CPU 占用、内存占用高),因此更多情况下作为优化效果来参考。这些情况下,具体的分析和定位可能还是得依赖合成监控。</p><p>但真实用户监控也有自身的优势,例如 TCP、DNS 连接耗时过高,在各种环境下的一些运行耗时问题,合成监控是很难发现的。</p><h1 id="性能分析自动化"><a href="#性能分析自动化" class="headerlink" title="性能分析自动化"></a>性能分析自动化</h1><p>我们在开发过程中,也常常需要进行性能分析。而前端的性能分析上手成本也不低,除了基本的页面加载耗时、网络耗时,更具体的定位往往需要结合前面介绍的 Performance 面板、FPS、CPU、火焰图等一点点来分析。</p><p>如果这一块想要往自动化方向发展,我们可以怎么做呢?</p><h2 id="使用-Lighthouse"><a href="#使用-Lighthouse" class="headerlink" title="使用 Lighthouse"></a>使用 Lighthouse</h2><p>前面也有介绍 Lighthouse,它提供了脚本的方式使用。因此,我们可以通过自动化任务跑脚本的方式,使用 Lighthouse 跑分析报告,通过对比以往的数据来进行功能变更、性能优化等场景的性能回归。</p><p>使用 Lighthouse 的优势在于开发成本低,只需要按照<a href="https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md" target="_blank" rel="noopener">官方提供的配置</a>来调整、获取自己需要的一些数据,就可以快速接入较全面的 Lighthouse 拥有的性能分析能力。</p><p>不过由于 Lighthouse 同样基于 CDP(Chrome DevTools Protocol),因此除了实现成本降低了,CDP 缺失的一些能力它也一样会缺失。</p><h2 id="Chrome-DevTools-Protocol"><a href="#Chrome-DevTools-Protocol" class="headerlink" title="Chrome DevTools Protocol"></a>Chrome DevTools Protocol</h2><p><a href="https://chromedevtools.github.io/devtools-protocol/" target="_blank" rel="noopener">Chrome DevTools Protocol</a> 允许第三方对基于 Chrome 的 Web 应用程序进行检测、检查、调试、分析等。有了这个协议,我们就可以自己开发工具获取 Chrome 的数据了。</p><h3 id="认识-Chrome-DevTools-协议"><a href="#认识-Chrome-DevTools-协议" class="headerlink" title="认识 Chrome DevTools 协议"></a>认识 Chrome DevTools 协议</h3><p>Chrome DevTools 协议基于 WebSocket,利用 WebSocket 建立连接 DevTools 和浏览器内核的快速数据通道。</p><p>我们使用的 Chrome DevTools 其实也是一个 Web 应用。我们使用 DevTools 的时候,浏览器内核 Chromium 本身会作为一个服务端,我们看到的浏览器调试工具界面,通过 Websocket 和 Chromium 进行通信。建立过程如下:</p><ol><li>DevTools 将作为 Web 应用程序,Chromium 作为服务端提供连接。</li><li>通过 HTTP 提取 HTML、JavaScript 和 CSS 文件。</li><li>资源加载后,DevTools 会建立与浏览器的 Websocket 连接,并开始交换 JSON 消息。</li></ol><p>同样的,当我们通过 DevTools 从 Windows、Mac 或 Linux 计算机远程调试 Android 设备上的实时内容时,使用的也是该协议。当 Chromium 以一个<code>--remote-debugging-port=0</code>标志启动时,它将启动 Chrome DevTools 协议服务器。</p><h3 id="Chrome-DevTools-协议域划分"><a href="#Chrome-DevTools-协议域划分" class="headerlink" title="Chrome DevTools 协议域划分"></a>Chrome DevTools 协议域划分</h3><p>Chrome DevTools协议具有与浏览器的许多不同部分(例如页面、Service Worker 和扩展程序)进行交互的 API。该协议把不同的操作划分为了不同的域(domain),每个域负责不同的功能模块。比如<code>DOM</code>、<code>Debugger</code>、<code>Network</code>、<code>Console</code>和<code>Performance</code>等,可以理解为 DevTools 中的不同功能模块。</p><p>使用该协议我们可以:</p><ul><li>获取 JS 的 Runtime 数据,常用的如<code>window.performance</code>和<code>window.chrome.loadTimes()</code>等</li><li>获取<code>Network</code>及<code>Performance</code>数据,进行自动性能分析</li><li>使用 <a href="https://github.com/GoogleChrome/lighthouse/blob/master/docs/puppeteer.md" target="_blank" rel="noopener">Puppeteer</a> 的 <a href="https://pptr.dev/#?product=Puppeteer&version=v1.13.0&show=api-class-cdpsession" target="_blank" rel="noopener">CDPSession</a>,与浏览器的协议通信会变得更加简单</li></ul><h3 id="与性能相关的域"><a href="#与性能相关的域" class="headerlink" title="与性能相关的域"></a>与性能相关的域</h3><p>本文讲性能分析相关,因此这里我们只关注和性能相关的域。</p><p><strong>1. Performance。</strong><br>从<code>Performance</code>域中<code>Performance.getMetrics()</code>可以拿到获取运行时性能指标包括:</p><ul><li><code>Timestamp</code>: 采取度量样本的时间戳</li><li><code>Documents</code>: 页面中的文档数</li><li><code>Frames</code>: 页面中的帧数</li><li><code>JSEventListeners</code>: 页面中的事件数</li><li><code>Nodes</code>: 页面中的 DOM 节点数</li><li><code>LayoutCount</code>: 全部或部分页面布局的总数</li><li><code>RecalcStyleCount</code>: 页面样式重新计算的总数</li><li><code>LayoutDuration</code>: 所有页面布局的合并持续时间</li><li><code>RecalcStyleDuration</code>: 所有页面样式重新计算的总持续时间</li><li><code>ScriptDuration</code>: JavaScript 执行的持续时间</li><li><code>TaskDuration</code>: 浏览器执行的所有任务的合并持续时间</li><li><code>JSHeapUsedSize</code>: 使用的 JavaScript 栈大小</li><li><code>JSHeapTotalSize</code>: JavaScript 栈总大小</li></ul><p><strong>2. Tracing。</strong><br><code>Tracing</code>域可获取页面加载的 DevTools 性能跟踪。可以使用<code>Tracing.start</code>和<code>Tracing.stop</code>创建可在 Chrome DevTools 或时间轴查看器中打开的跟踪文件。</p><p>我们能看到生成的 JSON 文件长这样:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_8.png" alt></p><p>这样的 JSON 文件,我们可以丢到 <a href="https://chromedevtools.github.io/timeline-viewer/" target="_blank" rel="noopener">DevTools Timeline Viewer</a> 中,可以看到对应的时间轴和火焰图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_9.jpg" alt></p><p><strong>3. Runtime。</strong><br><code>Runtime</code>域通过远程评估和镜像对象暴露 JavaScript 的运行时。可以通过<code>Runtime.getHeapUsage</code>获取 JavaScript 栈的使用情况,通过<code>Runtime.evaluate</code>计算全局对象的表达式,通过<code>Runtime.queryObjects</code>迭代 JavaScript 栈并查找具有给定原型的所有对象(可用于计算原型链中某处具有相同原型的所有对象,衡量 JavaScript 内存泄漏)。</p><p>除了上面介绍的这些,还有<code>Network</code>可以分析网络相关的性能,以及其他可能涉及 DOM 节点、JS 执行等各种各样的数据分析,更多的可能需要大家自己去研究了。</p><h3 id="自动化性能分析"><a href="#自动化性能分析" class="headerlink" title="自动化性能分析"></a>自动化性能分析</h3><p>通过使用 Chrome DevTools 协议,我们可以获取 DevTools 提供的很多数据,包括网络数据、性能数据、运行时数据。</p><p>对于如何使用该协议,其实已经有很多大神针对这个协议封装出不同语言的库,包括 Node.js、Python、Java等,可以根据需要在 <a href="https://github.com/ChromeDevTools/awesome-chrome-devtools#chrome-devtools-protocol" target="_blank" rel="noopener">awesome-chrome-devtools</a> 这个项目中找到。</p><p>至于我们到底能拿到怎样的数据,可以做到怎样的自动化程度,就不在本文里讲述啦,后面有机会再开篇文章详细讲讲。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://www.zcfy.cc/article/the-new-chrome-devtool-feature-you-want-to-know-about-3318.html" target="_blank" rel="noopener">你一定要知道的 Chrome DevTool 新功能</a></li><li><a href="https://juejin.im/post/6844904045774110733" target="_blank" rel="noopener">前端性能分析利器-Chrome性能分析&性能监视器</a></li><li><a href="https://www.infoq.cn/article/Dxa8aM44oz*Lukk5Ufhy" target="_blank" rel="noopener">蚂蚁金服如何把前端性能监控做到极致?</a></li><li><a href="https://testerhome.com/topics/15817" target="_blank" rel="noopener">chrome devtools protocol——Web 性能自动化实践介绍</a></li><li><a href="https://chromedevtools.github.io/devtools-protocol/" target="_blank" rel="noopener">Chrome DevTools Protocol</a></li><li><a href="https://addyosmani.com/blog/puppeteer-recipes/#measuring-memory-leaks" target="_blank" rel="noopener">Web Performance Recipes With Puppeteer</a></li></ul><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>前端性能分析相关的文章不算多,而由于性能分析本身的场景就跟业务特性结合比较紧密,可以用来借鉴的内容、较统一的解决方案也不多。而性能的监控、自动化等方向的介绍比较少,也希望这篇文章能给到你们一些方向吧~</p>]]></content>
<summary type="html">
<p>最近依然在研究大型项目,而大型项目最容易遇到的问题便是性能问题。一般来说,当我们遇到性能瓶颈的时候,才会开始去进行相应的分析。分析的方向除了业务本身的特点相关之外,常见的我们还可以借助一些工具来发现问题。本文一起来研究下,前端性能分析可以怎么走~</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>在线文档的网络层设计思考</title>
<link href="https://godbasin.github.io/2020/08/23/online-doc-network/"/>
<id>https://godbasin.github.io/2020/08/23/online-doc-network/</id>
<published>2020-08-23T05:25:12.000Z</published>
<updated>2020-08-23T05:31:19.440Z</updated>
<content type="html"><![CDATA[<p>像在线文档这样大型的项目,不管是从功能职责方面,还是从代码维护方面,分层和分模块都是必然的趋势。而网络层作为与服务端直接连接的一层,有多少是我们可以做到更好的呢?</p><a id="more"></a><h1 id="认识网络层"><a href="#认识网络层" class="headerlink" title="认识网络层"></a>认识网络层</h1><p>首先,涉及多人在线协作的场景,从用户交互到服务端存储都会特别复杂。对于前端来说,从后台获取的数据到展示,分别需要经过网络层、数据层和渲染层。除此之外,多人在线同样涉及房间管理等,简单来说,我们大致可以这么进行分层(图1):</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_0.png" alt="图1"></p><h2 id="网络层职责"><a href="#网络层职责" class="headerlink" title="网络层职责"></a>网络层职责</h2><p>一般来说,网络层无非就是做一些与服务端通信的工作,例如发起请求、异常处理、自动重试、登录态续期等。如果说,除了 HTTP 请求,可能还涉及 socket、长连接、请求数据缓存等各种功能。</p><p>在多人协作的场景下,为了保证用户体验,一般会采用 OT 算法来进行冲突处理。而为了保证每次的用户操作都可以按照正确的时序来更新,我们会维护一个自增的版本号,每次有新的修改,都会更新版本号。因此,在这样的场景下,网络层的职责大概包括:</p><ul><li>校验数据合法性</li><li>本地数据准确的提交给后台(涉及队列和版本控制)</li><li>协同数据正确处理后分发给数据层(涉及冲突处理)</li></ul><p>我们能看到,与网络层有交接的主要包括服务端和数据层。</p><p>那么,我们可以考虑将网络层拆分模块:<br><strong>1. 连接层:管理与服务端的连接(Socket、长连接等)。</strong><br><strong>2. 接入层:管理数据版本、冲突处理、与数据层的连接等。</strong></p><p>这样,我们的分层结构调整为图2:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_1.jpg" alt="图2"></p><h2 id="网络层设计"><a href="#网络层设计" class="headerlink" title="网络层设计"></a>网络层设计</h2><p>既然对网络层进行了模块的拆分,那么相关的设计我们也来分模块进行吧。</p><h3 id="连接层"><a href="#连接层" class="headerlink" title="连接层"></a>连接层</h3><p>连接层作为直接与服务端连接的一层,需要包括以下能力(图3):</p><ul><li><strong>多种通信方式支持</strong>(长连接、socket、短连接、SSE等)</li><li><strong>房间管理</strong>(心跳管理、用户管理、消息管理)</li></ul><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_2.png" alt="图3"></p><p><strong>1. 多种通信方式支持。</strong></p><p>前后端通信方式有很多种,常见的包括 HTTP 短轮询(pollinf)、Websocket、HTTP 长轮询(long-polling)、SSE(Server-Sent Events)等。</p><p>我们也能看到,不同的在线文档团队选用的通信方式并不一致。例如谷歌文档上行数据使用 Ajax、下行数据使用 HTTP 长轮询推送;石墨文档上行数据使用 Ajax、下行数据使用 SSE 推送;金山文档、飞书文档、腾讯文档则都使用了 Websocket 传输。</p><p>而每种通信方式都有各自的优缺点,包括兼容性、资源消耗、实时性等,也有可能跟业务团队自身的后台架构有关系。因此我们在设计连接层的时候,考虑接口拓展性,应该预留对各种方式的支持。</p><p><strong>2. 房间管理。</strong></p><p>由于多人协同的需要,相比普通的 Web 页面,还多了房间和用户的管理。在同一个文档中的用户,可视作在同一个房间。除了能看到哪些人在同一个房间以外,我们能收到相互之间的消息,在文档的场景中,用户的每一个操作,都可以作为是一个消息。</p><p>但文档和一般的房间聊天不一样的地方在于,用户的操作内容可能会很大,例如用户复制粘贴了一个10W、20W的表格内容,这样的消息显然无法一次性传输完。在这种情况下,除了考虑像 Websocket 这种需要自行进行数据压缩(HTTP 本身支持压缩)以外,我们还需要实现自己的分片逻辑。当涉及数据分片之后,紧接而来的还有如何分片、分片数据丢失的一些情况处理。</p><p>这样,我们的连接层则演化成图4的架构:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_3.jpg" alt="图4"></p><h2 id="接入层"><a href="#接入层" class="headerlink" title="接入层"></a>接入层</h2><p>接入层主要负责与业务比较相关的一些内容,例如协同数据的版本管理、冲突处理、用户操作的任务队列。我们可以从接收和发送两个方向来进行拆分(图5):</p><ul><li><strong>接收数据</strong>(服务端 -> 数据层):管理来自服务端的数据</li><li><strong>发送数据</strong>(数据层 -> 服务端):管理需要提交给服务端的数据</li></ul><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_4.png" alt="图5"></p><p>接入层的职责主要包括两个部分:<br><strong>1. 维护数据任务队列。</strong></p><ul><li>用户操作数据正常进入队列</li><li>队列任务正常提交到接入层</li><li>队列任务提交异常后进行重试</li><li>队列任务确认提交成功后移除</li></ul><p><strong>2. 数据版本有序递增。</strong></p><ul><li>协同数据版本更新</li><li>丢失数据版本补拉</li><li>提交数据版本递增</li></ul><p>也就是说,来自数据层的数据,将会添加到任务队列中。任务队列中的数据在提交数据之前,需要检查本地的版本和服务端的版本是否一致,如果有版本缺失的话,则要先从服务端拉取缺失的版本,确认本地版本最新后,则可以提交到服务端。</p><p>而来自服务端的数据,则需要先和任务队列中的数据进行冲突处理。冲突处理完成之后,则会同步到数据层。</p><p>这里面其实还有更多的细节,包括队列中任务的状态、任务的合并、数据版本的合并、版本断层的处理、重试失败的处理,队列中任务与协同消息的冲突处理、撤销重做的反向冲突处理,甚至还可能涉及断网离线的操作、本地缓存的任务队列、离线数据与在线数据的同步等等。本文我们就不继续讨论这些细节,还是回归到整体的设计上。</p><p>到这,我们的网络层架构大概出来了:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/tencent-doc-network_5.png" alt="图6"></p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>相比其他常见前端项目,在线文档光是网络层的设计都要复杂很多。而网络层只是其中一小部分,我们还有数据层、渲染层、各个组件和 feature 模块,依赖管理、通信管理、流程控制、性能优化等各种功能模块,每一个都有特别多的挑战。对于前端来说,能参与这样一个项目也是一件很幸福的事情了。</p>]]></content>
<summary type="html">
<p>像在线文档这样大型的项目,不管是从功能职责方面,还是从代码维护方面,分层和分模块都是必然的趋势。而网络层作为与服务端直接连接的一层,有多少是我们可以做到更好的呢?</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>VSCode 源码解读:IPC通信机制</title>
<link href="https://godbasin.github.io/2020/08/15/vscode-ipc/"/>
<id>https://godbasin.github.io/2020/08/15/vscode-ipc/</id>
<published>2020-08-15T08:51:30.000Z</published>
<updated>2020-08-16T04:18:21.904Z</updated>
<content type="html"><![CDATA[<p>最近在研究前端大型项目中要怎么管理模块间通信,本文记录研究 VSCode 中通信机制的过程,主要包括 IPC 部分吧。</p><a id="more"></a><h1 id="Electron-的-通信机制"><a href="#Electron-的-通信机制" class="headerlink" title="Electron 的 通信机制"></a>Electron 的 通信机制</h1><p>我们知道 Electron 是基于 Chromium + Node.js 的架构。同样基于 Chromium + Node.js 的,还有 NW.js,我们先来看看它们之间有什么不一样吧。</p><h1 id="Electron-与-NW-js"><a href="#Electron-与-NW-js" class="headerlink" title="Electron 与 NW.js"></a>Electron 与 NW.js</h1><p>说到 Node.js 的桌面应用,基本上大家都会知道 Electron 和 NW.js。例如 VsCode 就是基于 Electron 写的,而小程序开发工具则是基于 NW.js 来开发的。</p><p>我们知道,Node.js 和 Chromium 的运行环境不一样,它们的 JavaScript 上下文都有一些特有的全局对象和函数。在 Node.js 中包括 module、process、require等,在 Chromium 中会有 window、 documnet等。</p><p>那么,Electron 和 NW.js 都分别是怎样管理 Node.js 和 Chromium 的呢?</p><h3 id="NW-js-内部架构"><a href="#NW-js-内部架构" class="headerlink" title="NW.js 内部架构"></a>NW.js 内部架构</h3><p>NW.js 是最早的 Node.js 桌面应用框架,架构如图1。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_1.jpg" alt="图1"></p><p>在 NW.js 中,将 Node.js 和 Chromium 整合在一起使用,其中做了几件事情,我们来分别看下。</p><ol><li><p>Node.js 和 Chromium 都使用 V8 来处理执行的 JavaScript,因此在 NW.js 中它们使用相同的 V8 实例。</p></li><li><p>Node.js 和 Chromium 都使用事件循环编程模式,但它们用不同的软件库(Node.js 使用 libuv,Chromium 使用 MessageLoop/Message-Pump)。NW.js 通过使 Chromium 使用构建在 libuv 之上的定制版本的 MessagePump 来集成 Node.js 和 Chromium 的事件循环(如图2)。</p></li></ol><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_3.jpg" alt="图2"></p><ol start="3"><li>整合 Node.js 的上下文到 Chromium 中,使 Node.js 可用(如图3)。</li></ol><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_2.jpg" alt="图3"></p><p>因此,虽然 NW.js 整合了 Node.js 和 Chromium,但它更接近一个前端的应用开发方式(如图4),它的入口是 index.html。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_0.jpg" alt="图4"></p><h3 id="Electron-内部架构"><a href="#Electron-内部架构" class="headerlink" title="Electron 内部架构"></a>Electron 内部架构</h3><p>Electron 强调 Chromium 源代码和应用程序进行分离,因此并没有将 Node.js 和 Chromium 整合在一起。</p><p>在 Electron 中,分为主进程(main process)和渲染器进程(renderer processes):</p><ul><li>主进程:一个 Electron 应用中,有且只有一个主进程(package.json 的 main 脚本)</li><li>渲染进程:Electron 里的每个页面都有它自己的进程,叫作渲染进程。由于 Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到</li></ul><p>那么,不在一个进程当然涉及跨进程通信。于是,在 Electron 中,可以通过以下方式来进行主进程和渲染器进程的通信:</p><ol><li>利用<code>ipcMain</code>和<code>ipcRenderer</code>模块进行 IPC 方式通信,它们是处理应用程序后端(<code>ipcMain</code>)和前端应用窗口(<code>ipcRenderer</code>)之间的进程间通信的事件触发。</li><li>利用<code>remote</code>模块进行 RPC 方式通信。</li></ol><blockquote><p><code>remote</code>模块返回的每个对象(包括函数),表示主进程中的一个对象(称为远程对象或远程函数)。当调用远程对象的方法时,调用远程函数、或者使用远程构造函数 (函数) 创建新对象时,实际上是在发送同步进程消息。</p></blockquote><p>如图5,Electron 中从应用程序的后端部分到前端部分的任何状态共享(反之亦然),均通过<code>ipcMain</code>和<code>ipcRenderer</code>模块进行。这样,主进程和渲染器进程的 JavaScript 上下文将保持独立,但是可以在进程之间以显式方式传输数据。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_4.jpg" alt="图5"></p><h1 id="VSCode-的通信机制"><a href="#VSCode-的通信机制" class="headerlink" title="VSCode 的通信机制"></a>VSCode 的通信机制</h1><p>VSCode 基于 Electron 进行开发的,那么我们来看看 VSCode 里的相关设计吧。</p><h2 id="VSCode-多进程架构"><a href="#VSCode-多进程架构" class="headerlink" title="VSCode 多进程架构"></a>VSCode 多进程架构</h2><p>VSCode 采用多进程架构,VSCode 启动后主要有下面的几个进程:</p><ul><li>主进程</li><li>渲染进程,多个,包括 Activitybar、Sidebar、Panel、Editor 等等</li><li>插件宿主进程</li><li>Debug 进程</li><li>Search 进程</li></ul><p>这些进程间的关系如图6:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_5.png" alt="图6"></p><p>而在 VSCode 中,这些进程的通信方式同样包括 IPC 和 RPC 两种,如图7:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_6.jpg" alt="图7"></p><h2 id="IPC-通信"><a href="#IPC-通信" class="headerlink" title="IPC 通信"></a>IPC 通信</h2><p>我们能看到,主进程和渲染进程的通信基础还是 Electron 的<code>webContents.send</code>、<code>ipcRender.send</code>、<code>ipcMain.on</code>。</p><p>我们来看看 VSCode 中具体的 IPC 通信机制设计,包括:协议、频道、连接等。</p><h3 id="协议"><a href="#协议" class="headerlink" title="协议"></a>协议</h3><p>IPC 通信中,协议是最基础的。就像我们人和人之间的交流,需要使用约定的方式(语言、手语),在 IPC 中协议可看做是约定。</p><p>作为通信能力,最基本的协议范围包括发送和接受消息:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> interface IMessagePassingProtocol {</span><br><span class="line"> send(buffer: VSBuffer): <span class="keyword">void</span>;</span><br><span class="line"> onMessage: Event<VSBuffer>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>至于具体协议内容,可能包括连接、断开、事件等等:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Protocol</span> <span class="title">implements</span> <span class="title">IMessagePassingProtocol</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span>(private sender: Sender, readonly onMessage: Event<VSBuffer>) { }</span><br><span class="line"> <span class="comment">// 发送消息</span></span><br><span class="line"> send(message: VSBuffer): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">this</span>.sender.send(<span class="string">'vscode:message'</span>, message.buffer);</span><br><span class="line"> } <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="comment">// systems are going down</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 断开连接</span></span><br><span class="line"> dispose(): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">this</span>.sender.send(<span class="string">'vscode:disconnect'</span>, <span class="literal">null</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们能看到,IPC 的通信也使用了 VSCode 中的<code>Event</code>/<code>Emitter</code>事件机制,关于事件的更多可以参考<a href="https://godbasin.github.io/front-end-playground/front-end-basic/deep-learning/vscode-event.html">《VSCode源码解读:事件系统设计》</a>。</p><p>IPC 实际上就是发送和接收信息的能力,而要能准确地进行通信,客户端和服务端需要在同一个频道上。</p><h3 id="频道"><a href="#频道" class="headerlink" title="频道"></a>频道</h3><p>作为一个频道而言,它会有两个功能,一个是点播<code>call</code>,一个是收听,即<code>listen</code>。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * IChannel是对命令集合的抽象</span></span><br><span class="line"><span class="comment"> * call 总是返回一个至多带有单个返回值的 Promise</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> interface IChannel {</span><br><span class="line"> call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): <span class="built_in">Promise</span><T>;</span><br><span class="line"> listen<T>(event: string, arg?: any): Event<T>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="客户端与服务端"><a href="#客户端与服务端" class="headerlink" title="客户端与服务端"></a>客户端与服务端</h3><p>一般来说,客户端和服务端的区分主要是:发起连接的一端为客户端,被连接的一端为服务端。在 VSCode 中,主进程是服务端,提供各种频道和服务供订阅;渲染进程是客户端,收听服务端提供的各种频道/服务,也可以给服务端发送一些消息(接入、订阅/收听、离开等)。</p><p>不管是客户端和服务端,它们都会需要发送和接收消息的能力,才能进行正常的通信。</p><p>在 VSCode 中,客户端包括<code>ChannelClient</code>和<code>IPCClient</code>,<code>ChannelClient</code>只处理最基础的频道相关的功能,包括:</p><ol><li>获得频道<code>getChannel</code>。</li><li>发送频道请求<code>sendRequest</code>。</li><li>接收请求结果,并处理<code>onResponse/onBuffer</code>。</li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 客户端</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">ChannelClient</span> <span class="title">implements</span> <span class="title">IChannelClient</span>, <span class="title">IDisposable</span> </span>{</span><br><span class="line"> getChannel<T extends IChannel>(channelName: string): T {</span><br><span class="line"> <span class="keyword">const</span> that = <span class="keyword">this</span>;</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> call(command: string, arg?: any, cancellationToken?: CancellationToken) {</span><br><span class="line"> <span class="keyword">return</span> that.requestPromise(channelName, command, arg, cancellationToken);</span><br><span class="line"> },</span><br><span class="line"> listen(event: string, <span class="attr">arg</span>: any) {</span><br><span class="line"> <span class="keyword">return</span> that.requestEvent(channelName, event, arg);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">as</span> T;</span><br><span class="line"> }</span><br><span class="line"> private requestPromise(channelName: string, <span class="attr">name</span>: string, arg?: any, cancellationToken = CancellationToken.None): <span class="built_in">Promise</span><any> {}</span><br><span class="line"> private requestEvent(channelName: string, <span class="attr">name</span>: string, arg?: any): Event<any> {}</span><br><span class="line"> private sendRequest(request: IRawRequest): <span class="keyword">void</span> {}</span><br><span class="line"> private send(header: any, <span class="attr">body</span>: any = <span class="literal">undefined</span>): <span class="keyword">void</span> {}</span><br><span class="line"> private sendBuffer(message: VSBuffer): <span class="keyword">void</span> {}</span><br><span class="line"> private onBuffer(message: VSBuffer): <span class="keyword">void</span> {}</span><br><span class="line"> private onResponse(response: IRawResponse): <span class="keyword">void</span> {}</span><br><span class="line"> private whenInitialized(): <span class="built_in">Promise</span><<span class="keyword">void</span>> {}</span><br><span class="line"> dispose(): <span class="keyword">void</span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同样的,服务端包括<code>ChannelServer</code>和<code>IPCServer</code>,<code>ChannelServer</code>也只处理与频道直接相关的功能,包括:</p><ol><li>注册频道<code>registerChannel</code>。</li><li>监听客户端消息<code>onRawMessage/onPromise/onEventListen</code>。</li><li>处理客户端消息并返回请求结果<code>sendResponse</code>。</li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 服务端</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">ChannelServer</span><<span class="title">TContext</span> </span>= string> implements IChannelServer<TContext>, IDisposable {</span><br><span class="line"> registerChannel(channelName: string, <span class="attr">channel</span>: IServerChannel<TContext>): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">this</span>.channels.set(channelName, channel);</span><br><span class="line"> }</span><br><span class="line"> private sendResponse(response: IRawResponse): <span class="keyword">void</span> {}</span><br><span class="line"> private send(header: any, <span class="attr">body</span>: any = <span class="literal">undefined</span>): <span class="keyword">void</span> {}</span><br><span class="line"> private sendBuffer(message: VSBuffer): <span class="keyword">void</span> {}</span><br><span class="line"> private onRawMessage(message: VSBuffer): <span class="keyword">void</span> {}</span><br><span class="line"> private onPromise(request: IRawPromiseRequest): <span class="keyword">void</span> {}</span><br><span class="line"> private onEventListen(request: IRawEventListenRequest): <span class="keyword">void</span> {}</span><br><span class="line"> private disposeActiveRequest(request: IRawRequest): <span class="keyword">void</span> {}</span><br><span class="line"> private collectPendingRequest(request: IRawPromiseRequest | IRawEventListenRequest): <span class="keyword">void</span> {}</span><br><span class="line"> public dispose(): <span class="keyword">void</span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们能看到,作为频道的直接连接对象,<code>ChannelClient</code>和<code>ChannelServer</code>的发送和接收基本上是一一对应的,像<code>sendRequest</code>和<code>sendResponse</code>等等。但<code>ChannelClient</code>只能发送请求,而<code>ChannelServer</code>只能响应请求,在阅读完整篇文章后,可以思考一下:如果我们想要实现双向通信的话,可以怎么做呢?</p><p>我们还发现,对消息的发送和接受,<code>ChannelClient</code>和<code>ChannelServer</code>会进行序列化(<code>serialize</code>)和反序列化(<code>deserialize</code>):</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 此处篇幅关系,以 deserialize 举例:</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">deserialize</span>(<span class="params">reader: IReader</span>): <span class="title">any</span> </span>{</span><br><span class="line"> <span class="keyword">const</span> type = reader.read(<span class="number">1</span>).readUInt8(<span class="number">0</span>);</span><br><span class="line"> <span class="keyword">switch</span> (type) {</span><br><span class="line"> <span class="keyword">case</span> DataType.Undefined: <span class="keyword">return</span> <span class="literal">undefined</span>;</span><br><span class="line"> <span class="keyword">case</span> DataType.String: <span class="keyword">return</span> reader.read(readSizeBuffer(reader)).toString();</span><br><span class="line"> <span class="keyword">case</span> DataType.Buffer: <span class="keyword">return</span> reader.read(readSizeBuffer(reader)).buffer;</span><br><span class="line"> <span class="keyword">case</span> DataType.VSBuffer: <span class="keyword">return</span> reader.read(readSizeBuffer(reader));</span><br><span class="line"> <span class="keyword">case</span> DataType.Array: {</span><br><span class="line"> <span class="keyword">const</span> length = readSizeBuffer(reader);</span><br><span class="line"> <span class="keyword">const</span> result: any[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i < length; i++) {</span><br><span class="line"> result.push(deserialize(reader));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">case</span> DataType.Object: <span class="keyword">return</span> <span class="built_in">JSON</span>.parse(reader.read(readSizeBuffer(reader)).toString());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么,<code>IPCClient</code>和<code>IPCServer</code>又起到什么作用呢?</p><h3 id="连接"><a href="#连接" class="headerlink" title="连接"></a>连接</h3><p>现在有了频道直接相关的客户端部分<code>ChannelClient</code>和服务端部分<code>ChannelServer</code>,但是它们之间需要连接起来才能进行通信。一个连接(<code>Connection</code>)由<code>ChannelClient</code>和<code>ChannelServer</code>组成。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">interface Connection<TContext> extends Client<TContext> {</span><br><span class="line"> readonly channelServer: ChannelServer<TContext>; <span class="comment">// 服务端</span></span><br><span class="line"> readonly channelClient: ChannelClient; <span class="comment">// 客户端</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>而连接的建立,则由<code>IPCServer</code>和<code>IPCClient</code>负责。其中:</p><ul><li><code>IPCClient</code>基于<code>ChannelClient</code>,负责简单的客户端到服务端一对一连接</li><li><code>IPCServer</code>基于<code>channelServer</code>,负责服务端到客户端的连接,由于一个服务端可提供多个服务,因此会有多个连接</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 客户端</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">IPCClient</span><<span class="title">TContext</span> </span>= string> implements IChannelClient, IChannelServer<TContext>, IDisposable {</span><br><span class="line"> private channelClient: ChannelClient;</span><br><span class="line"> private channelServer: ChannelServer<TContext>;</span><br><span class="line"> getChannel<T extends IChannel>(channelName: string): T {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.channelClient.getChannel(channelName) <span class="keyword">as</span> T;</span><br><span class="line"> }</span><br><span class="line"> registerChannel(channelName: string, <span class="attr">channel</span>: IServerChannel<TContext>): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">this</span>.channelServer.registerChannel(channelName, channel);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 由于服务端有多个服务,因此可能存在多个连接</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">IPCServer</span><<span class="title">TContext</span> </span>= string> implements IChannelServer<TContext>, IRoutingChannelClient<TContext>, IConnectionHub<TContext>, IDisposable {</span><br><span class="line"> private channels = <span class="keyword">new</span> <span class="built_in">Map</span><string, IServerChannel<TContext>>();</span><br><span class="line"> private _connections = <span class="keyword">new</span> <span class="built_in">Set</span><Connection<TContext>>();</span><br><span class="line"> <span class="comment">// 获取连接信息</span></span><br><span class="line"> <span class="keyword">get</span> connections(): Connection<TContext>[] {}</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 从远程客户端获取频道。</span></span><br><span class="line"><span class="comment"> * 通过路由器后,可以指定它要呼叫和监听/从哪个客户端。</span></span><br><span class="line"><span class="comment"> * 否则,当在没有路由器的情况下进行呼叫时,将选择一个随机客户端,而在没有路由器的情况下进行侦听时,将监听每个客户端。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> getChannel<T extends IChannel>(channelName: string, <span class="attr">router</span>: IClientRouter<TContext>): T;</span><br><span class="line"> getChannel<T extends IChannel><span class="function">(<span class="params">channelName: string, clientFilter: (client: Client<TContext></span>) =></span> boolean): T;</span><br><span class="line"> getChannel<T extends IChannel><span class="function">(<span class="params">channelName: string, routerOrClientFilter: IClientRouter<TContext> | ((client: Client<TContext></span>) =></span> boolean)): T {}</span><br><span class="line"> <span class="comment">// 注册频道</span></span><br><span class="line"> registerChannel(channelName: string, <span class="attr">channel</span>: IServerChannel<TContext>): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">this</span>.channels.set(channelName, channel);</span><br><span class="line"> <span class="comment">// 添加到连接中</span></span><br><span class="line"> <span class="keyword">this</span>._connections.forEach(<span class="function"><span class="params">connection</span> =></span> {</span><br><span class="line"> connection.channelServer.registerChannel(channelName, channel);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>前面也说过,客户端可理解为渲染进程,服务端可理解为主进程。</p><p>而连接的详细建立过程,可以参考<a href="https://juejin.im/post/6844904050052300814" target="_blank" rel="noopener">《vscode-通信机制设计解读(Electron)》</a>一文。这里借用里面的一张图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/electron_ipc_7.jpg" alt="图8"></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://zhaomenghuan.js.org/blog/vscode-workbench-source-code-interpretation.html" target="_blank" rel="noopener">vscode 定制开发 —— Workbench 源码解读及实战前言</a></li><li><a href="https://zhaomenghuan.js.org/blog/vscode-custom-development-basic-preparation.html" target="_blank" rel="noopener">vscode 定制开发 —— 基础准备</a></li><li><a href="https://livebook.manning.com/book/cross-platform-desktop-applications/chapter-6/59" target="_blank" rel="noopener">【译】探索NW.js和Electron的内部</a></li><li><a href="https://juejin.im/post/6844904050052300814" target="_blank" rel="noopener">vscode-通信机制设计解读(Electron)</a></li><li><a href="https://imweb.io/topic/5b3b72ab4d378e703a4f4435" target="_blank" rel="noopener">你不知道的 Electron (一):神奇的 remote 模块</a></li></ul><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>IPC 和 RPC 通信是由于 Electron 的跨进程通信出现的。那么,我们还可以思考下,在一般的前端开发场景下,除了跨进程以外是否还有其他场景可以参考呢?</p><p>至于 RPC 的部分,由于目前也没有强业务相关,有空我们下次再约。</p>]]></content>
<summary type="html">
<p>最近在研究前端大型项目中要怎么管理模块间通信,本文记录研究 VSCode 中通信机制的过程,主要包括 IPC 部分吧。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>前端面试这件事--6.Javascript相关</title>
<link href="https://godbasin.github.io/2020/07/26/interview-6-javascript/"/>
<id>https://godbasin.github.io/2020/07/26/interview-6-javascript/</id>
<published>2020-07-26T08:43:17.000Z</published>
<updated>2021-03-13T12:36:55.242Z</updated>
<content type="html"><![CDATA[<p>这些年也有不少的面试别人和面试自己的经历,也有好些人来咨询一些前端的面试题目和准备,所以整理一下记录下来。本文针对面试中 Javascript 相关的内容,进行详细的描述。<br><a id="more"></a></p><p>不管是小前端还是大前端,我们的开发都离不开 Javascript。关于 Javascript 有太多内容可以讲了,每一个点都可以讲很久很久。本文主要是围绕知识点,来讲述面试可能出现的一些题目。 </p><h2 id="单线程的-Javascript"><a href="#单线程的-Javascript" class="headerlink" title="单线程的 Javascript"></a>单线程的 Javascript</h2><p>要怎么理解 Javascript 是单线程这个概念呢?大概可以从浏览器来说起。</p><p><strong>Q:为什么 Javascript 是单线程?</strong><br>A:作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。如果 Javascript 是多线程,当页面更新内容的时候、用户又触发了交互,这时候线程间的同步问题会变得很复杂,为了避免复杂性,Javascript 被设计为单线程。</p><p>因此我们的 Javascript 是单线程的,这是大前提。</p><h3 id="异步任务"><a href="#异步任务" class="headerlink" title="异步任务"></a>异步任务</h3><p>那么这样一个单线程的 Javascript,要如何高效地进行页面的交互和渲染处理呢?Javascript 只有一个线程,意味着任务需要一个接一个地进行,如果这时候我们有一个任务是等待用户输入,那在用户进行操作前,所有其他任务都会等待,页面处于假死状态,体验糟糕。因此,异步任务出现了。</p><p><strong>Q: 为什么说 Javascript 是异步的?</strong><br>A: I/O 类型的任务会有较长的等待时间。使用异步任务的方式,只要异步任务有了运行结果,再进行处理。这个过程中浏览器就不用处于等待状态,CPU 也可以处理其他任务。</p><p>(该问题其实不完全准确,因为 Javascript 中分为同步任务和异步任务,因此我们可以深入回答。)</p><p>A: 在浏览器中,任务可以分为同步任务和异步任务两种。同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行后一个任务。异步任务进入”任务队列”的任务,当该任务完成后,”任务队列”通知主线程,该任务才会进入主线程排队执行。</p><p><strong>Q: 在实际使用中异步任务可能会存在什么问题?</strong><br>A: <code>setTimeout</code>、<code>setInterval</code>的时间精确性。该类方法设置一个定时器,当定时器计时完成,需要执行回调函数,此时才把回调函数放入事件队列中。如果当回调函数放入队列时,假设队列中还有大量的事件没执行,此时就会造成任务执行时间不精确。</p><p>(那要怎么提升精确度呢?)</p><p>A: 可以使用系统时钟来补偿计时器不准确性。如果你的定时器是一系列的,可以在每次回调任务结束的时候,根据最初的系统时间和该任务的执行时间进行差值比较,来修正后续的定时器时间。</p><h3 id="EventLoop"><a href="#EventLoop" class="headerlink" title="EventLoop"></a>EventLoop</h3><p>前面我们提到异步任务机制,而 EventLoop 的设计解决了异步任务的同步问题。</p><p><strong>Q: 介绍浏览器的 EventLoop。</strong><br>A: JavaScript 有一个基于事件循环的并发模型,称为 EventLoop。<br>Javascript 有一个主线程和调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。</p><p><strong>Q: 宏任务(MacroTask)和微任务(MicroTask)的区别。</strong></p><ul><li>宏任务(MacroTask)包括:script全部代码、<code>setTimeout</code>、<code>setInterval</code>、<code>setImmediate</code>(Node独有)、<code>requestAnimationFrame</code>(浏览器独有)、I/O、UI rendering(浏览器独有)。</li><li>微任务(MicroTask)包括:<code>process.nextTick</code>(Node独有)、<code>Promise</code>、<code>Object.observe</code>(废弃)、<code>MutationObserver</code>。</li></ul><ol><li>宏任务(MacroTask)队列一次只从队列中取一个任务执行,执行完后就去执行微任务(MicroTask)队列中的任务。</li><li>微任务(MicroTask)队列中所有的任务都会被依次取出来执行,直到微任务队列为空;</li><li>执行 UI rendering 的节点是在执行完所有的微任务(MicroTask)之后,下一个宏任务(MacroTask)之前,紧跟着执行 UI rendering。</li></ol><blockquote><p>篇幅关系,大家可以从以下文章中获取更详细的内容说明:</p><ul><li><a href="http://www.ruanyifeng.com/blog/2013/10/event_loop.html" target="_blank" rel="noopener">什么是 Event Loop?</a></li><li><a href="http://www.ruanyifeng.com/blog/2014/10/event-loop.html" target="_blank" rel="noopener">JavaScript 运行机制详解:再谈Event Loop</a></li><li><a href="https://juejin.im/post/5c3d8956e51d4511dc72c200" target="_blank" rel="noopener">一次弄懂Event Loop(彻底解决此类面试问题)</a></li><li><a href="https://juejin.im/post/5b8f76675188255c7c653811" target="_blank" rel="noopener">带你彻底弄懂Event Loop</a></li></ul></blockquote><p>这里可能会出各式各样的题目,考察包括:</p><ul><li>浏览器中,<code>setTimeout</code>、<code>Promise</code>和<code>async</code>/<code>await</code>的执行顺序</li><li>Node.js 中,<code>setTimeout</code>、<code>setImmediate</code>和<code>process.nextTick</code>的执行顺序<blockquote><p>这里就不写具体题目了,大家可以参考上面的文章,或者自行搜一下题目看看,理解原理了比死记硬背要好得多。</p></blockquote></li></ul><h3 id="Web-Workers-Service-Workers"><a href="#Web-Workers-Service-Workers" class="headerlink" title="Web Workers/Service Workers"></a>Web Workers/Service Workers</h3><p>现在硬件的性能也越来越强了,为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准。</p><p><strong>Q: 介绍一下 Web Workers。</strong><br>A: Web Workers 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。</p><p><strong>Q: Service workers 有什么用途。</strong><br>A: Service workers 可拦截并修改访问和资源请求,细粒度地缓存资源,本质上可充当 Web 应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。</p><blockquote><p>参考内容:</p><ul><li><a href="http://www.ruanyifeng.com/blog/2018/07/web-worker.html" target="_blank" rel="noopener">Web Worker 使用教程</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API" target="_blank" rel="noopener">Service Worker API</a></li></ul></blockquote><h2 id="Javascript-的设计"><a href="#Javascript-的设计" class="headerlink" title="Javascript 的设计"></a>Javascript 的设计</h2><p>理解 Javascript 的一些基本设计,日常工作中也有很多帮助,例如可以更优雅地实现一些逻辑,也可以在遇到一些问题的时候快速定位。</p><h3 id="原型和继承"><a href="#原型和继承" class="headerlink" title="原型和继承"></a>原型和继承</h3><p><strong>Q: 如何理解 Javascript 中的“一切皆对象”?</strong><br>A: 当谈到继承时,JavaScript 只有一种结构:对象。<br>每个实例对象<code>object</code>都有一个私有属性<code>__proto__</code>指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象<code>__proto__</code>,层层向上直到一个对象的原型对象为 null。null 没有原型,并作为这个原型链中的最后一个环节。</p><p><strong>Q: 如何理解 Javascript 的原型继承?</strong><br>A: JavaScript 对象有一个指向一个原型对象的链。<br>当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。<br>函数的继承与其他的属性继承没有差别。需要注意的是,当继承的函数被调用时,<code>this</code>指向的是当前继承的对象,而不是继承的函数所在的原型对象。</p><p><strong>Q: 如何创建一个对象?</strong><br>A: 可以使用以下方法:</p><ul><li>使用语法结构创建的对象,即定义一个数组、函数、对象等</li><li>使用构造器创建的对象: <code>new XXX()</code></li><li>使用<code>Object.create</code>创建对象</li><li>使用<code>class</code>关键字创建对象</li></ul><p><strong>Q: <strong>proto</strong> 与 prototype 有什么区别?</strong><br>A: JavaScript 可以通过<code>prototype</code>和<code>__proto__</code>在两个对象之间创建一个关联,使得一个对象就可以通过委托访问另一个对象的属性和函数。当我们创建函数时,Javascript 会为这个函数自动添加<code>prototype</code>属性,值是一个有<code>constructor</code>属性的对象。一旦我们通过<code>new</code>关键字调用,那么 Javascript 就会帮你创建该构造函数的实例,实例通过将<code>__proto__</code>指向承构造函数的<code>prototype</code>,来继承构造函数<code>prototype</code>的所有属性和方法。<br>对象<code>__proto__</code>属性的值就是它所对应的原型对象,指向自己构造函数的<code>prototype</code>。每个对象都有<code>__proto__</code>属性来标识自己所继承的原型,但只有函数才有<code>prototype</code>属性。</p><blockquote><p><code>__proto__</code>与<code>prototype</code>可以考的点也很多,这里也不详述了,可以参考下:</p><ul><li><a href="https://github.com/creeperyang/blog/issues/9" target="_blank" rel="noopener">从<strong>proto</strong>和prototype来深入理解JS对象和原型链</a></li></ul></blockquote><p><strong>Q: 如何判断对象类型?</strong><br>A: 看情境可结合使用以下三种方法:</p><ol><li><code>typeof</code>运算符。问题:容易混淆<code>object</code>和<code>null</code>,会把<code>Array</code>还有用户自定义函数都返回为<code>object</code>。</li><li><code>instanceof</code>运算符,判断某一个对象是否是所给的构造函数的一个实例。</li><li><code>constructor</code>属性,返回对创建此对象的数组函数的引用。</li></ol><h3 id="作用域与闭包"><a href="#作用域与闭包" class="headerlink" title="作用域与闭包"></a>作用域与闭包</h3><p><strong>Q: 请描述以下代码的执行输出内容(题略,考察作用域)。</strong><br>A: 当代码在一个环境中执行时,会创建变量对象的一个作用域链,来保证对执行环境有权访问的变量和函数的有序访问。</p><blockquote><p>考察内容可包括:全局作用域、函数作用域、块级作用域、词法作用域、动态作用域,可参考:</p><ul><li><a href="https://juejin.im/post/5abb99e9f265da2392366824" target="_blank" rel="noopener">谈谈 JavaScript 的作用域</a></li></ul></blockquote><p><strong>Q: 什么场景需要使用闭包?</strong><br>A: 由于 Javascript 特殊的变量作用域,函数内部可以直接读取全局变量,但在函数外部自然无法读取函数内的局部变量。闭包用途:</p><ul><li>用于从外部读取其他函数内部变量的函数</li><li>可以使用闭包来模拟私有方法</li><li>让这些变量的值始终保持在内存中(涉及垃圾回收机制,可能导致内存泄露问题)</li></ul><blockquote><p>可参考:</p><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures" target="_blank" rel="noopener">闭包</a></li><li><a href="https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html" target="_blank" rel="noopener">学习Javascript闭包(Closure)</a></li></ul></blockquote><h3 id="关于-this"><a href="#关于-this" class="headerlink" title="关于 this"></a>关于 this</h3><p><strong>Q: 简单描述 this 在不同场景下的指向。</strong><br>A: Javascript 中 <code>this</code>指针代表的是执行当前代码的对象的所有者,可简单理解为<code>this</code>永远指向最后调用它的那个对象。<br>根据 JavaScript 中函数的调用方式不同,分为以下情况:</p><ol><li>函数作为对象的方法调用:此时<code>this</code>被自然绑定到该对象。</li><li>作为函数调用: 函数可以直接被调用,此时<code>this</code>绑定到全局对象(在浏览器中为 window)。</li><li>作为构造函数调用:如果不使用<code>new</code>调用,则和普通函数一样。</li><li>使用<code>apply</code>、<code>call</code>、<code>bind</code>等方式调用:根据API不同,可切换函数执行的上下文环境,即<code>this</code>绑定的对象。</li></ol><p><strong>Q: 手写代码实现 call/apply/bind。</strong><br>A: 此处略,请自行谷歌。</p><p><strong>Q: 箭头函数与普通函数的区别。</strong><br>箭头函数的<code>this</code>始终指向函数定义时的<code>this</code>,而非执行时。</p><blockquote><p>关于 Javascript 的一些设计,可以参考:</p><ul><li><a href="https://github.com/mqyqingfeng/Blog/issues/17" target="_blank" rel="noopener">JavaScript深入系列15篇正式完结!</a></li></ul></blockquote><h3 id="ES6"><a href="#ES6" class="headerlink" title="ES6+"></a>ES6+</h3><p>ES6/ES7/…/ESn这些都是不断发展的语法糖,虽然可能很多人都没有大量用到,但是一些基本的还是需要掌握的。<br>推荐阮大神的<a href="https://es6.ruanyifeng.com/" target="_blank" rel="noopener">ES6 入门教程</a>,里面讲的特别详细和清晰。</p><p><strong>Q: 为什么要使用 Promise?</strong><br>Javascript 的单线程设计,导致网络请求、事件监听等都需要异步执行。异步执行通常用回调函数实现,多层的回调会导致回调金字塔的出现。<br>Promise 允许我们为异步操作的成功和失败分别绑定相应的处理方法(handlers),同时可以通过<code>Promise.all()</code>、<code>Promise.race()</code>等方法来同时处理多个异步操作的结果。</p><p><strong>Q: 手写代码 or 讲解 Promise、async/await 的实现。</strong><br>对于<code>Promise</code>、<code>async</code>/<code>await</code>理解的考察,最简单的就是让描述或者手写它们的实现方式。具体可以参考 MDN 上的内容,也可以找一些 polyfill 来看,也可以看其他人的文章分享,这里就不多说啦。</p><p>除此之外,关于已下内容也可以去了解一下,这里也不多说了:</p><ul><li>解构的使用</li><li>Class 的使用</li><li>Set 和 Map 数据结构</li><li>浏览器兼容性与 Babel</li></ul><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>除了以上问题以外,Javascript 还有一个典型的浮点数运算精确度的问题,大家可参考<a href="https://github.com/camsong/blog/issues/9" target="_blank" rel="noopener">JavaScript 浮点数陷阱及解法</a>。<br>Javascript 这块的内容太多了,大家平时可以多关注一下自己对这些内容的理解和掌握程度,平时工作中也可以多进行思考,基础性的内容不建议临时抱佛脚哦。</p>]]></content>
<summary type="html">
<p>这些年也有不少的面试别人和面试自己的经历,也有好些人来咨询一些前端的面试题目和准备,所以整理一下记录下来。本文针对面试中 Javascript 相关的内容,进行详细的描述。<br>
</summary>
<category term="工作这杯茶" scheme="https://godbasin.github.io/categories/%E5%B7%A5%E4%BD%9C%E8%BF%99%E6%9D%AF%E8%8C%B6/"/>
<category term="心态" scheme="https://godbasin.github.io/tags/%E5%BF%83%E6%80%81/"/>
</entry>
<entry>
<title>VSCode 源码解读:事件系统设计</title>
<link href="https://godbasin.github.io/2020/07/05/vscode-event/"/>
<id>https://godbasin.github.io/2020/07/05/vscode-event/</id>
<published>2020-07-05T01:55:29.000Z</published>
<updated>2020-07-05T03:13:05.687Z</updated>
<content type="html"><![CDATA[<p>最近在研究前端大型项目中要怎么管理满天飞的事件、模块间各种显示和隐式调用的问题,本文结合相应的源码分析,记录 VS Code 中的事件管理系统设计。</p><a id="more"></a><h1 id="VS-Code-事件"><a href="#VS-Code-事件" class="headerlink" title="VS Code 事件"></a>VS Code 事件</h1><p>看源码的方式有很多种,带着疑问有目的性地看,会简单很多。</p><h2 id="Q1-VS-Code-中的事件管理代码在哪?"><a href="#Q1-VS-Code-中的事件管理代码在哪?" class="headerlink" title="Q1: VS Code 中的事件管理代码在哪?"></a>Q1: VS Code 中的事件管理代码在哪?</h2><p>一般来说,说到事件,肯定是跟<code>event</code>关键字相关,因此我们直接全局搜一下文件名(VS Code 下快捷键<code>ctrl+p</code>):<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vscode-event-file-name.png" alt></p><p>一下子就出来了,这个路径是<code>base\common\event.ts</code>的肯定是比较关键的。打开一看,里面比较关键的有两个:<strong>Event</strong>和<strong>Emitter</strong>。</p><h2 id="Q2-VS-Code-中的事件都包括了哪些能力?"><a href="#Q2-VS-Code-中的事件都包括了哪些能力?" class="headerlink" title="Q2: VS Code 中的事件都包括了哪些能力?"></a>Q2: VS Code 中的事件都包括了哪些能力?</h2><p>先来看看<code>Event</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 主要定义了一些接口协议,以及相关方法</span></span><br><span class="line"><span class="comment">// 使用 namespace 的方式将相关内容包裹起来</span></span><br><span class="line"><span class="keyword">export</span> namespace Event {</span><br><span class="line"> <span class="comment">// 来看看里面比较关键的一些方法</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 给定一个事件,返回另一个仅触发一次的事件</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">once</span><<span class="title">T</span>>(<span class="params">event: Event<T></span>): <span class="title">Event</span><<span class="title">T</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 给定一连串的事件处理功能(过滤器,映射等),每个事件和每个侦听器都将调用每个函数</span></span><br><span class="line"> <span class="comment">// 对事件链进行快照可以使每个事件每个事件仅被调用一次</span></span><br><span class="line"> <span class="comment">// 以此衍生了 map、forEach、filter、any 等方法此处省略</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">snapshot</span><<span class="title">T</span>>(<span class="params">event: Event<T></span>): <span class="title">Event</span><<span class="title">T</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 给事件增加防抖</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">debounce</span><<span class="title">T</span>>(<span class="params">event: Event<T>, merge: (last: T | undefined, event: T</span>) => <span class="title">T</span>, <span class="title">delay</span>?: <span class="title">number</span>, <span class="title">leading</span>?: <span class="title">boolean</span>, <span class="title">leakWarningThreshold</span>?: <span class="title">number</span>): <span class="title">Event</span><<span class="title">T</span>>;</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">// 触发一次的事件,同时包括触发时间</span></span><br><span class="line"><span class="function"><span class="title">export</span> <span class="title">function</span> <span class="title">stopwatch</span><<span class="title">T</span>>(<span class="params">event: Event<T></span>): <span class="title">Event</span><<span class="title">number</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 仅在 event 元素更改时才触发的事件</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">latch</span><<span class="title">T</span>>(<span class="params">event: Event<T></span>): <span class="title">Event</span><<span class="title">T</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 缓冲提供的事件,直到出现第一个 listener,这时立即触发所有事件,然后从头开始传输事件</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">buffer</span><<span class="title">T</span>>(<span class="params">event: Event<T>, nextTick = false, _buffer: T[] = []</span>): <span class="title">Event</span><<span class="title">T</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 可链式处理的事件,支持以下方法</span></span><br><span class="line"> <span class="keyword">export</span> interface IChainableEvent<T> {</span><br><span class="line"> event: Event<T>;</span><br><span class="line"> map<O><span class="function">(<span class="params">fn: (i: T</span>) =></span> O): IChainableEvent<O>;</span><br><span class="line"> forEach(fn: <span class="function">(<span class="params">i: T</span>) =></span> <span class="keyword">void</span>): IChainableEvent<T>;</span><br><span class="line"> filter(fn: <span class="function">(<span class="params">e: T</span>) =></span> boolean): IChainableEvent<T>;</span><br><span class="line"> filter<R><span class="function">(<span class="params">fn: (e: T | R</span>) =></span> e is R): IChainableEvent<R>;</span><br><span class="line"> reduce<R><span class="function">(<span class="params">merge: (last: R | <span class="literal">undefined</span>, event: T</span>) =></span> R, initial?: R): IChainableEvent<R>;</span><br><span class="line"> latch(): IChainableEvent<T>;</span><br><span class="line"> debounce(merge: <span class="function">(<span class="params">last: T | <span class="literal">undefined</span>, event: T</span>) =></span> T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<T>;</span><br><span class="line"> debounce<R><span class="function">(<span class="params">merge: (last: R | <span class="literal">undefined</span>, event: T</span>) =></span> R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<R>;</span><br><span class="line"> on(listener: <span class="function">(<span class="params">e: T</span>) =></span> any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;</span><br><span class="line"> once(listener: <span class="function">(<span class="params">e: T</span>) =></span> any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;</span><br><span class="line"> }</span><br><span class="line"> <span class="class"><span class="keyword">class</span> <span class="title">ChainableEvent</span><<span class="title">T</span>> <span class="title">implements</span> <span class="title">IChainableEvent</span><<span class="title">T</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将事件转为可链式处理的事件</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">chain</span><<span class="title">T</span>>(<span class="params">event: Event<T></span>): <span class="title">IChainableEvent</span><<span class="title">T</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 来自 DOM 事件的事件</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">fromDOMEventEmitter</span><<span class="title">T</span>>(<span class="params">emitter: DOMEventEmitter, eventName: string, map: (...args: any[]</span>) => <span class="title">T</span> = <span class="title">id</span> => <span class="title">id</span>): <span class="title">Event</span><<span class="title">T</span>> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 来自 Promise 的事件</span></span><br><span class="line"> <span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">fromPromise</span><<span class="title">T</span> = <span class="title">any</span>>(<span class="params">promise: Promise<T></span>): <span class="title">Event</span><<span class="title">undefined</span>> </span>{}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们能看到,<code>Event</code>中主要是一些对事件的处理和某种类型事件的生成。其中,除了常见的<code>once</code>和 DOM 事件等兼容,还提供了比较丰富的事件能力:</p><ul><li>防抖动</li><li>可链式调用</li><li>缓存</li><li>Promise 转事件</li></ul><h2 id="Q3-VS-Code-中的事件的触发和监听是怎么实现的?"><a href="#Q3-VS-Code-中的事件的触发和监听是怎么实现的?" class="headerlink" title="Q3: VS Code 中的事件的触发和监听是怎么实现的?"></a>Q3: VS Code 中的事件的触发和监听是怎么实现的?</h2><p>到这里,我们只看到了关于事件的一些功能(参考<code>Event</code>),而事件的触发和监听又是怎么进行的呢?</p><p>我们可以继续来看<code>Emitter</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 这是事件发射器的一些生命周期和设置</span></span><br><span class="line"><span class="keyword">export</span> interface EmitterOptions {</span><br><span class="line"> onFirstListenerAdd?: <span class="built_in">Function</span>;</span><br><span class="line"> onFirstListenerDidAdd?: <span class="built_in">Function</span>;</span><br><span class="line"> onListenerDidAdd?: <span class="built_in">Function</span>;</span><br><span class="line"> onLastListenerRemove?: <span class="built_in">Function</span>;</span><br><span class="line"> leakWarningThreshold?: number;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Emitter</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="comment">// 可传入生命周期方法和设置</span></span><br><span class="line"> <span class="keyword">constructor</span>(options?: EmitterOptions) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 允许大家订阅此发射器的事件</span></span><br><span class="line"> <span class="keyword">get</span> event(): Event<T> {</span><br><span class="line"> <span class="comment">// 此处会根据传入的生命周期相关设置,在对应的场景下调用相关的生命周期方法</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 向订阅者触发事件</span></span><br><span class="line"> fire(event: T): <span class="keyword">void</span> {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 清理相关的 listener 和队列等</span></span><br><span class="line"> dispose() {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,<code>Emitter</code>以<code>Event</code>为对象,以简洁的方式提供了事件的订阅、触发、清理等能力。</p><h2 id="Q4-项目中的事件是怎么管理的?"><a href="#Q4-项目中的事件是怎么管理的?" class="headerlink" title="Q4: 项目中的事件是怎么管理的?"></a>Q4: 项目中的事件是怎么管理的?</h2><p><code>Emitter</code>似乎有些简单了,我们只能看到单个事件发射器的使用。那各个模块之间的事件订阅和触发又是怎么实现的呢?</p><p>我们来全局搜一下关键字<code>Emitter</code>:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vscode-event-global-emitter.jpg" alt></p><p>搜出来很多地方都有使用,我们来看一下第一个:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 这里我们只摘录相关的代码</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">WindowManager</span> </span>{</span><br><span class="line"> public <span class="keyword">static</span> readonly INSTANCE = <span class="keyword">new</span> WindowManager();</span><br><span class="line"> <span class="comment">// 注册一个事件发射器</span></span><br><span class="line"> private readonly _onDidChangeZoomLevel = <span class="keyword">new</span> Emitter<number>();</span><br><span class="line"> <span class="comment">// 将该发射器允许大家订阅的事件取出来</span></span><br><span class="line"> public readonly onDidChangeZoomLevel: Event<number> = <span class="keyword">this</span>._onDidChangeZoomLevel.event;</span><br><span class="line"></span><br><span class="line"> public setZoomLevel(zoomLevel: number, <span class="attr">isTrusted</span>: boolean): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>._zoomLevel === zoomLevel) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">this</span>._zoomLevel = zoomLevel;</span><br><span class="line"> <span class="comment">// 当 zoomLevel 有变更时,触发该事件</span></span><br><span class="line"> <span class="keyword">this</span>._onDidChangeZoomLevel.fire(<span class="keyword">this</span>._zoomLevel);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>显然,在 VS Code 里,事件的使用方式主要包括:</p><ul><li>注册事件发射器</li><li>对外提供定义的事件</li><li>在特定时机向订阅者触发事件</li></ul><p>那么,其他地方又是怎样订阅这么一个事件呢?在这个例子中,由于浏览器实例唯一,可以通过挂载全局对象的方式来提供使用:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 对外提供一个调用全局实例的方法</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">onDidChangeZoomLevel</span>(<span class="params">callback: (zoomLevel: number</span>) => <span class="title">void</span>): <span class="title">IDisposable</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> WindowManager.INSTANCE.onDidChangeZoomLevel(callback);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 其他地方的调用方式</span></span><br><span class="line"><span class="keyword">import</span> { onDidChangeZoomLevel } <span class="keyword">from</span> <span class="string">'vs/base/browser/browser'</span>;</span><br><span class="line"><span class="keyword">let</span> zoomListener = onDidChangeZoomLevel(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="comment">// 该干啥干啥</span></span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>除此之外,我们也可以通过创建实例调用来直接监听相关事件:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> instance = <span class="keyword">new</span> WindowManager(opts);</span><br><span class="line">instance.onDidChangeZoomLevel(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="comment">// 该干啥干啥</span></span><br><span class="line">});</span><br></pre></td></tr></table></figure><h2 id="Q5-事件满天飞,不会导致性能问题吗?"><a href="#Q5-事件满天飞,不会导致性能问题吗?" class="headerlink" title="Q5: 事件满天飞,不会导致性能问题吗?"></a>Q5: 事件满天飞,不会导致性能问题吗?</h2><p>习惯使用一些前端框架的小伙伴们肯定比较有经验,我们如果在某个组件里做了事件订阅这样的操作,当组件销毁的时候是需要取消事件订阅的。否则该订阅内容会在内存中一直存在,除了一些异常问题,还可能引起内存泄露。</p><p>那么,VS Code 里又是怎么处理这样的问题呢?</p><p>其实我们在全局搜<code>Emitter</code>的时候,也能看到一些地方的使用方式是:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 只选取局部关键代码摘要</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Scrollable</span> <span class="keyword">extends</span> <span class="title">Disposable</span> </span>{</span><br><span class="line"> private _onScroll = <span class="keyword">this</span>._register(<span class="keyword">new</span> Emitter<ScrollEvent>());</span><br><span class="line"> public readonly onScroll: Event<ScrollEvent> = <span class="keyword">this</span>._onScroll.event;</span><br><span class="line"></span><br><span class="line"> private _setState(newState: ScrollState): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">const</span> oldState = <span class="keyword">this</span>._state;</span><br><span class="line"> <span class="keyword">if</span> (oldState.equals(newState)) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">this</span>._state = newState;</span><br><span class="line"> <span class="comment">// 状态变更的时候,触发事件</span></span><br><span class="line"> <span class="keyword">this</span>._onScroll.fire(<span class="keyword">this</span>._state.createScrollEvent(oldState));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里使用了<code>this._register(new Emitter<T>())</code>这样的方式注册事件发射器,我们能看到该方法继承自<code>Disposable</code>。而<code>Disposable</code>的实现也很简洁:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> abstract <span class="class"><span class="keyword">class</span> <span class="title">Disposable</span> <span class="title">implements</span> <span class="title">IDisposable</span> </span>{</span><br><span class="line"> <span class="comment">// 用一个 Set 来存储注册的事件发射器</span></span><br><span class="line"> private readonly _store = <span class="keyword">new</span> DisposableStore();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span>() {</span><br><span class="line"> trackDisposable(<span class="keyword">this</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 处理事件发射器</span></span><br><span class="line"> public dispose(): <span class="keyword">void</span> {</span><br><span class="line"> markTracked(<span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>._store.dispose();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 注册一个事件发射器</span></span><br><span class="line"> protected _register<T extends IDisposable>(t: T): T {</span><br><span class="line"> <span class="keyword">if</span> ((t <span class="keyword">as</span> unknown <span class="keyword">as</span> Disposable) === <span class="keyword">this</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'Cannot register a disposable on itself!'</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>._store.add(t);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>也就是说,每个继承<code>Disposable</code>类都会有管理事件发射器的相关方法,包括添加、销毁处理等。其实我们仔细看看,这个<code>Disposable</code>并不只是服务于事件发射器,它适用于所有支持<code>dispose()</code>方法的对象:</p><blockquote><p>Dispose 模式主要用来资源管理,资源比如内存被对象占用,则会通过调用方法来释放。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> interface IDisposable {</span><br><span class="line"> dispose(): <span class="keyword">void</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">DisposableStore</span> <span class="title">implements</span> <span class="title">IDisposable</span> </span>{</span><br><span class="line"> private _toDispose = <span class="keyword">new</span> <span class="built_in">Set</span><IDisposable>();</span><br><span class="line"> private _isDisposed = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 处置所有注册的 Disposable,并将其标记为已处置</span></span><br><span class="line"> <span class="comment">// 将来添加到此对象的所有 Disposable 都将在 add 中处置。</span></span><br><span class="line"> public dispose(): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>._isDisposed) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> markTracked(<span class="keyword">this</span>);</span><br><span class="line"> <span class="keyword">this</span>._isDisposed = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">this</span>.clear();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 丢弃所有已登记的 Disposable,但不要将其标记为已处置</span></span><br><span class="line"> public clear(): <span class="keyword">void</span> {</span><br><span class="line"> <span class="keyword">this</span>._toDispose.forEach(<span class="function"><span class="params">item</span> =></span> item.dispose());</span><br><span class="line"> <span class="keyword">this</span>._toDispose.clear();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 添加一个 Disposable</span></span><br><span class="line"> public add<T extends IDisposable>(t: T): T {</span><br><span class="line"> markTracked(t);</span><br><span class="line"> <span class="comment">// 如果已处置,则不添加</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>._isDisposed) {</span><br><span class="line"> <span class="comment">// 报错提示之类的</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 未处置,则可添加</span></span><br><span class="line"> <span class="keyword">this</span>._toDispose.add(t);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> t;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因此,我们可以看到,在 VS Code 中是这样管理事件的:</p><ol><li>抹平 DOM 事件等差异,提供标准化的<code>Event</code>和<code>Emitter</code>能力。</li><li>通过注册<code>Emitter</code>,并对外提供类似生命周期的方法<code>onXxxxx</code>的方式,来进行事件的订阅和监听。</li><li>通过提供通用类<code>Disposable</code>,统一管理多个事件发射器(或其他资源)的注册、销毁。</li></ol><h2 id="Q6-上面只销毁了事件触发器本身的资源,那对于订阅者来说,要怎么销毁订阅的-Listener-呢?"><a href="#Q6-上面只销毁了事件触发器本身的资源,那对于订阅者来说,要怎么销毁订阅的-Listener-呢?" class="headerlink" title="Q6: 上面只销毁了事件触发器本身的资源,那对于订阅者来说,要怎么销毁订阅的 Listener 呢?"></a>Q6: 上面只销毁了事件触发器本身的资源,那对于订阅者来说,要怎么销毁订阅的 Listener 呢?</h2><p>或许读到这里的时候,你依然有点懵。看上去 VS Code 的<code>Emitter</code>和<code>Event</code>似乎跟常见的实现方式很相似,只是使用的方式有点不一样而已,到底有什么特别的呢?</p><p>不知道大家注意到了没,在 VS Code 中,注册一个事件发射器、订阅某个事件,都是通过<code>this._register()</code>这样的方式来实现:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1. 注册事件发射器</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Button</span> <span class="keyword">extends</span> <span class="title">Disposable</span> </span>{</span><br><span class="line"> <span class="comment">// 注册一个事件发射器,可使用 this._onDidClick.fire(xxx) 来触发事件</span></span><br><span class="line"> private _onDidClick = <span class="keyword">this</span>._register(<span class="keyword">new</span> Emitter<Event>());</span><br><span class="line"> <span class="keyword">get</span> onDidClick(): BaseEvent<Event> { <span class="keyword">return</span> <span class="keyword">this</span>._onDidClick.event; }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 订阅某个事件</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">QuickInputController</span> <span class="keyword">extends</span> <span class="title">Disposable</span> </span>{</span><br><span class="line"> <span class="comment">// 省略很多其他非关键代码</span></span><br><span class="line"> private getUI() {</span><br><span class="line"> <span class="keyword">const</span> ok = <span class="keyword">new</span> Button(okContainer);</span><br><span class="line"> ok.label = localize(<span class="string">'ok'</span>, <span class="string">"OK"</span>);</span><br><span class="line"> <span class="comment">// 注册一个 Disposable,用来订阅某个事件</span></span><br><span class="line"> <span class="keyword">this</span>._register(ok.onDidClick(<span class="function"><span class="params">e</span> =></span> {</span><br><span class="line"> <span class="keyword">this</span>.onDidAcceptEmitter.fire();</span><br><span class="line"> }));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>也就是说当某个类被销毁时,会发生以下事情:</p><ol><li>它所注册的事件发射器会被销毁,而事件发射器中的 Listener、队列等都会被清空。</li><li>它所订阅的一些事件会被销毁,订阅中的 Listener 同样会被移除。</li></ol><p>至于订阅事件的 Listener 是如何被移除的,可参考以下代码:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Emitter</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="keyword">get</span> event(): Event<T> {</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">this</span>._event) {</span><br><span class="line"> <span class="keyword">this</span>._event = <span class="function">(<span class="params">listener: (e: T</span>) =></span> any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {</span><br><span class="line"> <span class="comment">// 若无队列,则新建一个</span></span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">this</span>._listeners) {</span><br><span class="line"> <span class="keyword">this</span>._listeners = <span class="keyword">new</span> LinkedList();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 往队列中添加该 Listener,同时返回一个移除该 Listener 的方法</span></span><br><span class="line"> <span class="keyword">const</span> remove = <span class="keyword">this</span>._listeners.push(!thisArgs ? listener : [listener, thisArgs]);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> result: IDisposable;</span><br><span class="line"> <span class="comment">// 返回一个带 dispose 方法的结果,dispose 执行时会移除该 Listener</span></span><br><span class="line"> result = {</span><br><span class="line"> dispose: <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> result.dispose = Emitter._noop;</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">this</span>._disposed) {</span><br><span class="line"> remove();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"> <span class="keyword">if</span> (disposables <span class="keyword">instanceof</span> DisposableStore) {</span><br><span class="line"> disposables.add(result);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (<span class="built_in">Array</span>.isArray(disposables)) {</span><br><span class="line"> disposables.push(result);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>._event;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>到这里,VS Code 中事件相关的管理的设计也都呈现出来了,包括:</p><ul><li>提供标准化的<code>Event</code>和<code>Emitter</code>能力</li><li>通过注册<code>Emitter</code>,并对外提供类似生命周期的方法<code>onXxxxx</code>的方式,来进行事件的订阅和监听</li><li>通过提供通用类<code>Disposable</code>,统一管理相关资源的注册和销毁</li><li>通过使用同样的方式<code>this._register()</code>注册事件和订阅事件,将事件相关资源的处理统一挂载到<code>dispose()</code>方法中</li></ul><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>VS Code 中除了事件的管理,Dispose 模式还体现在各种其他资源的管理,包括插件等。</p><p>当我们遇到一些问题不知道该怎么解决的时候,可以试着站到巨人的肩膀上,说不定可以看到更多。</p>]]></content>
<summary type="html">
<p>最近在研究前端大型项目中要怎么管理满天飞的事件、模块间各种显示和隐式调用的问题,本文结合相应的源码分析,记录 VS Code 中的事件管理系统设计。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>响应式编程在前端领域的应用</title>
<link href="https://godbasin.github.io/2020/07/04/reactive-programing/"/>
<id>https://godbasin.github.io/2020/07/04/reactive-programing/</id>
<published>2020-07-04T04:03:21.000Z</published>
<updated>2020-07-05T03:11:17.250Z</updated>
<content type="html"><![CDATA[<p>其实在几年前因为 Angular 的原因接触过响应式编程,而这些年的一些项目经验,让我在再次回顾响应式编程的时候又有了新的理解。</p><a id="more"></a><h1 id="什么是响应式编程"><a href="#什么是响应式编程" class="headerlink" title="什么是响应式编程"></a>什么是响应式编程</h1><p>响应式编程基于观察者模式,是一种面向数据流和变化传播的声明式编程方式。</p><h2 id="异步数据流"><a href="#异步数据流" class="headerlink" title="异步数据流"></a>异步数据流</h2><p>响应式编程常常用在异步数据流,通过订阅某个数据流,可以对数据进行一系列流式处理,例如过滤、计算、转换、合流等,配合函数式编程可以实现很多优秀的场景。</p><p>除了天然异步的前端、客户端等 GUI 开发以外,响应式编程在大数据处理中也同样拥有高并发、分布式、依赖解耦等优势,在这种同步阻塞转异步的并发场景下会有较大的性能提升,淘宝业务架构就是使用响应式的架构。</p><h2 id="响应式编程在前端领域"><a href="#响应式编程在前端领域" class="headerlink" title="响应式编程在前端领域"></a>响应式编程在前端领域</h2><p>在前端领域,常见的异步编程场景包括事件处理、用户输入、HTTP 响应等。对于这类型的数据流,可以使用响应式编程的方式来进行设计。</p><p>不少开发者基于响应式编程设计了一些工具库,包括 Rxjs、Mobx、Cycle.js 等。其中,Rxjs 提供了基于可观察对象(Observable)的 functional reactive programming 服务,Mobx 提供了基于状态管理的 transparent functional reactive programming 服务,而 Cycle.js 则是一个响应式前端框架。</p><p>我们可以结合具体场景来介绍下使用,这里会以 Rxjs 来说明。</p><h3 id="HTTP-请求与重试"><a href="#HTTP-请求与重试" class="headerlink" title="HTTP 请求与重试"></a>HTTP 请求与重试</h3><p>基于响应式编程,我们可以很简单地实现一个请求的获取和自动重试:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { ajax } <span class="keyword">from</span> <span class="string">"rxjs/ajax"</span>;</span><br><span class="line"><span class="keyword">import</span> { map, retry, catchError } <span class="keyword">from</span> <span class="string">"rxjs/operators"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> apiData = ajax(<span class="string">"/api/data"</span>).pipe(</span><br><span class="line"> <span class="comment">// 可以在 catchError 之前使用 retry 操作符。它会订阅到原始的来源可观察对象,此处为重新发起 HTTP 请求</span></span><br><span class="line"> retry(<span class="number">3</span>), <span class="comment">// 失败前会重试最多 3 次</span></span><br><span class="line"> map(<span class="function">(<span class="params">res</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!res.response) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">"Value expected!"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> res.response;</span><br><span class="line"> }),</span><br><span class="line"> catchError(<span class="function">(<span class="params">err</span>) =></span> <span class="keyword">of</span>([]))</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">apiData.subscribe({</span><br><span class="line"> next(x) {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"data: "</span>, x);</span><br><span class="line"> },</span><br><span class="line"> error(err) {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">"errors already caught... will not run"</span>);</span><br><span class="line"> },</span><br><span class="line">});</span><br></pre></td></tr></table></figure><h3 id="用户输入"><a href="#用户输入" class="headerlink" title="用户输入"></a>用户输入</h3><p>对应用户的一些交互,也可以通过订阅的方式来获取需要的信息:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> observable = Rx.Observable.fromEvent(input, <span class="string">"input"</span>) <span class="comment">// 监听 input 元素的 input 事件</span></span><br><span class="line"> .map(<span class="function">(<span class="params">e</span>) =></span> e.target.value) <span class="comment">// 一旦发生,把事件对象 e 映射成 input 元素的值</span></span><br><span class="line"> .filter(<span class="function">(<span class="params">value</span>) =></span> value.length >= <span class="number">1</span>) <span class="comment">// 接着过滤掉值长度小于 1 的</span></span><br><span class="line"> .distinctUntilChanged() <span class="comment">// 如果该值和过去最新的值相等,则忽略</span></span><br><span class="line"> .subscribe(</span><br><span class="line"> <span class="comment">// subscribe 拿到数据</span></span><br><span class="line"> (x) => <span class="built_in">console</span>.log(x),</span><br><span class="line"> (err) => <span class="built_in">console</span>.error(err)</span><br><span class="line"> );</span><br><span class="line"><span class="comment">// 订阅</span></span><br><span class="line">observable.subscribe(<span class="function">(<span class="params">x</span>) =></span> <span class="built_in">console</span>.log(x));</span><br></pre></td></tr></table></figure><p>在用户频繁交互的场景,数据的流式处理可以让我们很方便地进行节流和防抖。除此之外,模块间的调用和事件通信同样可以通过这种方式来进行处理。</p><h2 id="比较其他技术"><a href="#比较其他技术" class="headerlink" title="比较其他技术"></a>比较其他技术</h2><p>接触响应式编程这个概念的时候,大多数人都会对它产生困惑,也比较容易与 Promise、事件订阅这些设计混淆。我们来一起看看。</p><h3 id="Promise"><a href="#Promise" class="headerlink" title="Promise"></a>Promise</h3><p>Promise 相信大家也都很熟悉,在这里拿出来比较,其实更多是将 Rxjs 中的 Observable 与之比较。这两个其实很不一样:</p><ul><li>Promise 会发生状态扭转,状态扭转不可逆;而 Observable 是无状态的,数据流可以源源不断,可用于随着时间的推移获取多个值</li><li>Promise 在定义时就会被执行;而 Observable 只有在被订阅时才会执行</li><li>Promise 不支持取消;而 Observable 可通过取消订阅取消正在进行的工作</li></ul><h3 id="事件"><a href="#事件" class="headerlink" title="事件"></a>事件</h3><p>同样是基于观察者模式,相信很多人都对事件和响应式编程之间的关系比较迷惑。而根据具体的设计实现,事件和响应式编程模式可以达到高度相似。</p><p>一个比较显著的区别在于,由于响应式编程是面向数据流和变化传播的模式,意味着我们可以对数据流进行配置处理,使其在把事件传给事件处理器之前先进行转换。同样由于流式处理,响应式编程可以把包含一堆异步/事件的组合从开头到结尾用流的操作符清晰表示,而原始事件回调只能表示一堆相邻节点的关系,对于数据流动方向和过程都可以进一步掌握。</p><p>同时,结合响应式编程的合流、缓存等能力,我们可以收获更多。</p><h1 id="响应式编程提供了怎样的服务"><a href="#响应式编程提供了怎样的服务" class="headerlink" title="响应式编程提供了怎样的服务"></a>响应式编程提供了怎样的服务</h1><p>前面说了很多,相信大家对响应式编程的概念和使用有一定的理解了。现在,我们一起来看看它还能给我们带来怎样的服务。</p><h2 id="热观察与冷观察"><a href="#热观察与冷观察" class="headerlink" title="热观察与冷观察"></a>热观察与冷观察</h2><p>在 Rxjs 中,有热观察和冷观察的概念。其中的区别:</p><ul><li>Hot Observable,可以理解为现场直播,我们进场的时候只能看到即时的内容</li><li>Cold Observable,可以理解为点播(电影),我们打开的时候会从头播放</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> liveStreaming$ = Rx.Observable.interval(<span class="number">1000</span>).take(<span class="number">5</span>);</span><br><span class="line"></span><br><span class="line">liveStreaming$.subscribe(</span><br><span class="line"> data => <span class="built_in">console</span>.log(<span class="string">'subscriber from first second'</span>)</span><br><span class="line"> err => <span class="built_in">console</span>.log(err),</span><br><span class="line"> () => <span class="built_in">console</span>.log(<span class="string">'completed'</span>)</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">setTimeout(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> liveStreaming$.subscribe(</span><br><span class="line"> data => <span class="built_in">console</span>.log(<span class="string">'subscriber from 2nd second'</span>)</span><br><span class="line"> err => <span class="built_in">console</span>.log(err),</span><br><span class="line"> () => <span class="built_in">console</span>.log(<span class="string">'completed'</span>)</span><br><span class="line"> )</span><br><span class="line">}, <span class="number">2000</span>)</span><br><span class="line"><span class="comment">// 事实上两个订阅者接收到的值都是 0,1,2,3,4,此处为冷观察</span></span><br></pre></td></tr></table></figure><p>Rxjs 中 Observable 默认为冷观察,而通过<code>publish()</code>和<code>connect()</code>可以将冷的 Observable 转变成热的:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> publisher$ = Rx.Observable.interval(<span class="number">1000</span>).take(<span class="number">5</span>).publish();</span><br><span class="line"></span><br><span class="line">publisher$.subscribe(</span><br><span class="line"> data => <span class="built_in">console</span>.log(<span class="string">'subscriber from first minute'</span>,data),</span><br><span class="line"> err => <span class="built_in">console</span>.log(err),</span><br><span class="line"> () => <span class="built_in">console</span>.log(<span class="string">'completed'</span>)</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">setTimeout(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> publisher$.subscribe(</span><br><span class="line"> data => <span class="built_in">console</span>.log(<span class="string">'subscriber from 2nd minute'</span>, data),</span><br><span class="line"> err => <span class="built_in">console</span>.log(err),</span><br><span class="line"> () => <span class="built_in">console</span>.log(<span class="string">'completed'</span>)</span><br><span class="line"> )</span><br><span class="line">}, <span class="number">3000</span>)</span><br><span class="line"></span><br><span class="line">publisher$.connect();</span><br><span class="line"><span class="comment">// 第一个订阅者输出的是0,1,2,3,4,而第二个输出的是3,4,此处为热观察</span></span><br></pre></td></tr></table></figure><p>热观察和冷观察根据具体的场景可能会有不同的需要,而 Observable 提供的缓存能力也能解决不少业务场景。例如,如果我们想要在拉群后,自动同步之前的聊天记录,通过冷观察就可以做到。同样的,热观察的用途也很广泛。</p><h2 id="合流"><a href="#合流" class="headerlink" title="合流"></a>合流</h2><p>流的处理大概是响应式编程中最有意思的部分了。一般来说,合流有两种方式:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. merge</span></span><br><span class="line">--1----2-----3--------4---</span><br><span class="line">----a-----b----c---d------</span><br><span class="line"> merge</span><br><span class="line">--1<span class="_">-a</span>--2--b--3-c---d--4---</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. combine</span></span><br><span class="line">--1----2-----3--------4---</span><br><span class="line">----a-----b-----c--d------</span><br><span class="line"> combine</span><br><span class="line">----1a-2a-2b-3b-3c-3d-4d--</span><br></pre></td></tr></table></figure><p>那这样的合流方式,可以具体应用到哪里呢?</p><p>例如,merge 的合流方式可以用在群聊天、聊天室,一些多人协作的场景、公众号订阅的场景就可以通过这样的方式合流,最终按照顺序地展示出对应的操作记录。</p><p>再举个例子,我们在 Excel 中,通过函数计算了 A1 和 B2 两个格子的相加。这种情况下,使用 combine 方式合流符合预期,那么我们可以订阅这么一个流:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> streamA1 = Rx.Observable.fromEvent(inputA1, <span class="string">"input"</span>); <span class="comment">// 监听 A1 单元格的 input 事件</span></span><br><span class="line"><span class="keyword">const</span> streamB2 = Rx.Observable.fromEvent(inputB2, <span class="string">"input"</span>); <span class="comment">// 监听 B2 单元格的 input 事件</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> subscribe = combineLatest(streamA1, streamB2).subscribe(<span class="function">(<span class="params">valueA1, valueB2</span>) =></span> {</span><br><span class="line"> <span class="comment">// 从 streamA1 和 streamB2 中获取最新发出的值</span></span><br><span class="line"> <span class="keyword">return</span> valueA1 + valaueB2;</span><br><span class="line">});</span><br><span class="line"><span class="comment">// 获取函数计算结果</span></span><br><span class="line">observable.subscribe(<span class="function">(<span class="params">x</span>) =></span> <span class="built_in">console</span>.log(x));</span><br></pre></td></tr></table></figure><p>在一个较大型的前端应用中,通常会拆分成渲染层、数据层、网络层、其他服务等多个功能模块。虽然服务按照功能结构进行拆分了,但依然会存在服务间调用导致依赖关系复杂、事件触发和监听满天飞等情况,这种情况下,只能通过全局搜索关键字来找到上下游数据流、信息流,通过一个接一个的节点和关键字搜索才能大概理清楚某个数据来源哪里。</p><p>那么,如果使用了响应式编程,我们可以通过各种合流的方式、订阅分流的方式,来将应用中的数据流动从头到尾串在一起。这样,我们可以很清晰地当前节点上的数据来自于哪里,是用户的操作还是来自网络请求。</p><h2 id="其他使用方式"><a href="#其他使用方式" class="headerlink" title="其他使用方式"></a>其他使用方式</h2><p>除了上面提到的一些 HTTP 请求、用户操作、事件管理等可以使用响应式编程的方式来实现,我们还可以将定时器、数组/可迭代对象变量转换为可观察序列。</p><h3 id="timer"><a href="#timer" class="headerlink" title="timer"></a>timer</h3><p>也就是说,如果我们界面中有个倒计时,就可以以定时器为数据源,订阅该数据流进行响应:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// timerOne 在 0 秒时发出第一个值,然后每 1 秒发送一次</span></span><br><span class="line"><span class="keyword">const</span> timerOne = timer(<span class="number">0</span>, <span class="number">1000</span>).subscribe(<span class="function"><span class="params">x</span> =></span> {</span><br><span class="line"> <span class="comment">// 触发界面更新</span></span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>定时器结合合流的方式,我们还可以玩出更多的花样。例如,界面中有三个倒计时,我们需要在倒计时全部结束之后展示一些内容,这个时候我们就可以通过将三个倒计时 combine 合流,当三个流都处于倒计时终止的状态时,触发相应的逻辑。</p><h3 id="数组-可迭代对象"><a href="#数组-可迭代对象" class="headerlink" title="数组/可迭代对象"></a>数组/可迭代对象</h3><p>我们可以将数组或者可迭代的对象,转换为可观察的序列。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> array = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 打印出每个项目</span></span><br><span class="line"><span class="keyword">const</span> subscription = Rx.Observable.from(array).subscribe(</span><br><span class="line"> x => <span class="built_in">console</span>.log(<span class="string">'onNext: %s'</span>, x),</span><br><span class="line"> e => <span class="built_in">console</span>.log(<span class="string">'onError: %s'</span>, e),</span><br><span class="line"> () => <span class="built_in">console</span>.log(<span class="string">'onCompleted'</span>)</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// => onNext: 1</span></span><br><span class="line"><span class="comment">// => onNext: 2</span></span><br><span class="line"><span class="comment">// => onNext: 3</span></span><br><span class="line"><span class="comment">// => onNext: 4</span></span><br><span class="line"><span class="comment">// => onNext: 5</span></span><br><span class="line"><span class="comment">// => onCompleted</span></span><br></pre></td></tr></table></figure><p>乍一看,似乎只是将遍历换了种写法,其实这样的能力可以用在更多的地方。例如,我们在离线编辑文档的时候,做了很多操作,这些操作在本地会用一个操作记录数组的方式缓存下来。当应用检测到网络状态恢复的时候,可以将这样的操作组转换为有序的一个个操作同步到远程服务器。(当然,更好的设计应该是支持批量有序地上传操作到服务器)</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>对响应式编程的介绍暂告一段落。</p><p>可见对于很多复杂程度较低的前端应用来说,其实入门成本比较高。但在一些复杂应用的场景,合理地使用响应式编程,可以有效地降低各个模块间的依赖,更加容易地进行整体数据流动管理和维护。</p><p>这么有意思的东西,你要不要来试试看?</p>]]></content>
<summary type="html">
<p>其实在几年前因为 Angular 的原因接触过响应式编程,而这些年的一些项目经验,让我在再次回顾响应式编程的时候又有了新的理解。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>谈谈依赖和解耦</title>
<link href="https://godbasin.github.io/2020/06/26/module-seperate/"/>
<id>https://godbasin.github.io/2020/06/26/module-seperate/</id>
<published>2020-06-26T02:55:23.000Z</published>
<updated>2020-06-26T03:10:31.509Z</updated>
<content type="html"><![CDATA[<p>大型项目总避免不了各种模块间相互依赖,如果模块之间耦合严重,随着功能的增加,项目到后期会越来越难以维护。今天我们一起来思考下,大家常说的代码解耦到底要怎么做?<br><a id="more"></a></p><h1 id="依赖是怎么产生的"><a href="#依赖是怎么产生的" class="headerlink" title="依赖是怎么产生的"></a>依赖是怎么产生的</h1><p>既然要研究怎么让模块解耦,那当然要从根源来分析:依赖它到底从何而来?</p><p>依赖其实是在我们想把代码写好的那一刻开始产生的。为什么这么说呢?因为大多数代码都是可以通过像流水线一样写下来,最终变成一个几千行的函数、几万行的单个文件。这个时候甚至没有拆分成模块,也就更谈不上所谓依赖和解耦了。</p><p>忽然有一天,我们发现这种堆屎山的日子实在过的没有意思,开始研究怎么将一座大屎山拆成几个小屎山,然后再一点点清理干净。依赖的产生,就在我们一拆多的这个过程伴随出现的。</p><h2 id="接口管理"><a href="#接口管理" class="headerlink" title="接口管理"></a>接口管理</h2><p>当我们开始进行代码优化的时候,最先想到的就是将某些通用的功能抽象成单独的模块,通过提供接口这样的方式来给到需要的地方使用。</p><p>为了避免过度设计,我们会基于现有和可预见的需要进行设计。但日常的开发中,不可预见的问题定位和调整却占了大部分的时间。</p><p>例如,在做 To B 项目的时候,我们设计了一套完整的 API 给到对方,开始的时候大家都会按照这套接口来配合开发,其乐融融。突然有一天,老板拉了一个大客户,说这个客户的用户量会很大,必须要好好配合。老板一走,大客户马上化身甲方爸爸,说他们的接口已经写好了,友商都是按照他们的格式接入,都上线了。</p><p>遇到这种情况,通常我们会新增一个适配层,专门用于我们的服务和甲方爸爸之间的适配。</p><p>说了那么多,依赖在哪里呢?</p><p>依赖其实在接口设计完成的时候就出来了,虽然这是我们自己设计的接口,但它依赖于上游按照约定来调用。而上游有调整的时候,我们是需要跟随者适配或者调整的。</p><p>或者举个小一点的例子,我们在项目中使用了一个较出名的开源库。某一天该开源库升级版本了,新的版本不兼容旧的版本,同时声明旧的版本不会再继续维护了。这意味着如果我们不升级版本的话,后续旧的版本出现了 bug,我们只能自己啃源码来修复了。</p><p>这是来自于“甲方按照约定接口来调用服务”、“乙方按照约定接口来提供服务”的依赖。</p><h2 id="状态管理"><a href="#状态管理" class="headerlink" title="状态管理"></a>状态管理</h2><p>由接口管理产生的依赖通常来自外部,而应用内部也会有依赖的产生,常见的包括状态管理和事件管理。我们先来看看状态管理。</p><p>一个应用程序能按照预期正常运行,必然无法避免一些状态的管理。最简单的,生命周期就是一种状态。程序是否已经启动、功能是否正常运行、输入输出是否有变化,这些都会影响到程序的运行状态。</p><p>由于程序会有状态变化,因此我们的功能实现必然依赖程序的状态。例如,只有用户登录了才能进行更多的操作、订单产生了才可以进行撤销、界面渲染完成了用户才可以点击,等等。</p><p>从代码可读性和可维护性角度来看,面向对象编程近些年来还是稍胜于函数式编程,面向对象的设计本身就是状态设计的过程,而某个对象的运行结果,也会依赖于该对象的状态。</p><p>这是来自于对某个程序“按照预期运行”进行合理设计而产生的依赖。</p><h2 id="功能管理"><a href="#功能管理" class="headerlink" title="功能管理"></a>功能管理</h2><p>当我们根据功能将代码拆分成一个个模块之后,功能模块的管理也同样会产生一些依赖。</p><p>管理系统中最常见的就是面板的管理,对于每个面板来说,它应该只关心自身的状态。产品设计会要求我们在打开某个新的面板的时候,关闭其他面板;或是在点击面板以外的地方,关闭当前面板。这会涉及到面板与面板以外界面的通信,一般来说我们可以使用事件的方式来管理。每个面板在创建的时候,都需要监听外界的一个点击事件,并判断点击区域落在面板外面的时候,触发关闭。</p><p>某一天,产品提了个需求,所有的这些面板关闭的时候都要有一个动画效果,至于这个关闭动画的持续时间,要根据点击位置与面板的距离来计算。我们需要在点击事件触发的时候,把点击的位置告诉监听对象。</p><p>于是,我们全局搜了所有该类型事件的触发节点和监听节点,一一进行调整。</p><p>这是来自于对某个功能“不会发生变更”而产生的依赖。</p><h2 id="依赖来自于约束"><a href="#依赖来自于约束" class="headerlink" title="依赖来自于约束"></a>依赖来自于约束</h2><p>为了方便管理,我们设计了一些约定,并基于“大家都会遵守约定”的前提来提供更好、更便捷的服务。</p><p>举个例子,前端框架中为了更清晰地管理渲染层、数据层和逻辑处理,常用的设计包括 MVC、MVVM 等。而要使这样的架构设计发挥出效果,我们需要遵守其中的使用规范,不可以在数据层里直接修改界面等。</p><p>可以看到,依赖来自于对代码的设计。</p><h1 id="依赖可以解耦吗"><a href="#依赖可以解耦吗" class="headerlink" title="依赖可以解耦吗"></a>依赖可以解耦吗</h1><p>既然依赖来自于设计,为什么我们又常常说要进行模块间的解耦,降低模块间的依赖呢?</p><h2 id="依赖的划分"><a href="#依赖的划分" class="headerlink" title="依赖的划分"></a>依赖的划分</h2><p>我们先来看看一个问题,所有的依赖都需要解耦吗?</p><p>其实我们能看到,不合理的设计会导致代码间相互依赖,耦合严重。这种情况下,我们可以理解为产生了不合适的依赖。</p><p>而通常我们所说的设计解耦,则是通过合理的设计,恰到好处的职责和边界划分。此时,同样会产生一些约定,但这样的约定可以更好地管理我们的代码,此时可以理解为产生了合理的依赖。</p><p>因此,回到前面的疑问:既然依赖来自于设计,为什么我们要通过设计来降低依赖呢?显然,我们想要减少的,是不合理的依赖。而通过合理的设计,可以进行恰当的解耦。</p><h3 id="无状态的函数式编程?"><a href="#无状态的函数式编程?" class="headerlink" title="无状态的函数式编程?"></a>无状态的函数式编程?</h3><p>每个程序员对函数式编程都曾抱有过幻想,写多了面向对象编程的代码,对一些状态的管理和维护感到心烦。而无状态的函数式仿佛是白月光,可远观不可亵玩。</p><p>但即使是基于函数式编程设计的语言,写出来的功能也无法逃脱状态管理的命运。像 Clojure 编写的 Storm,也需要进行消息队列的管理、重启后服务的恢复等一系列状态管理。</p><p>在前端领域,React 同样基于函数式编程,但框架同样带有生命周期这样的状态。用 React 来实现的应用也依赖状态,因此同样产生了 Redux/Mobx 这样的工具来进行数据状态的管理工具。</p><p>应用程序无法离开状态的管理,是否意味着我们不需要函数式编程呢?并不是这样的。相反,我们需要对功能模块进行划分,划分出有状态和无状态的功能,来将状态管理放置到更小的范围,避免“牵一发而动全身”。</p><p>在这里,我们进行了状态有无的划分。</p><h3 id="单向流的数据管理?"><a href="#单向流的数据管理?" class="headerlink" title="单向流的数据管理?"></a>单向流的数据管理?</h3><p>代码解耦的方式,其中也包括了使用单向数据流这种方式。</p><p>不管是 React 还是 Vue,都提供了单向数据流的管理工具。由于一个应用中,各个功能间都会有一些相互间的数据依赖,为了避免模块间的直接依赖,使用单向流的方式,可以将一些非模块内闭环的数据通过有序、单向的方式进行捆绑。通过这样的方式,模块之间的依赖解除了,调整为模块与数据流模块之间的依赖,代码的耦合程度得到缓解。</p><p>在这里,我们进行了模块内外数据的划分。</p><h3 id="服务化"><a href="#服务化" class="headerlink" title="服务化"></a>服务化</h3><p>服务化,是系统解耦最常用的一种方式。</p><p>通过将功能进行业务领域的拆分,我们得到了不同领域的服务,常见的例如电商系统拆分成订单系统、购物车系统、商品系统、商家系统、支付系统等等。</p><p>而如今打得火热的“微服务”,也都是基于领域建模的一种实现方式。</p><p>在这里,我们进行了业务领域的划分。</p><h3 id="模块化与依赖注入?"><a href="#模块化与依赖注入?" class="headerlink" title="模块化与依赖注入?"></a>模块化与依赖注入?</h3><p>相比于针对系统设计的服务化,同样有针对功能设计的模块化。</p><p>在前端领域,同样可以根据功能拆分为表单功能、列表功能、面板功能等,通过给这些功能设置边界、封装成独立完整的模块,可以将功能与功能之间的依赖降到最低。同样的,根据功能划分的方式,我们还可以将功能拆分成渲染层、数据层、网络层这样的模块。</p><p>而配合依赖注入的方式,我们在使用这些功能的时候不再需要单独对这些功能的状态进行维护,同样实现了功能模块间的解耦。</p><p>在这里,我们进行了功能应用的划分。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>到这里,你会不会有点疑惑,说了半天好像什么都没说?我当然知道要合理设计啊,但什么才是合理的设计呢?</p><p>架构设计没有银弹,系统的复杂度、使用场景、用户群体、机器性能等都会影响决策,“具体场景具体分析”才是最优解。</p><p>而我们能做的,就是多思考、多参考、多分析、多尝试,沉淀下来的经验和思考方式才是最实用的工具。</p>]]></content>
<summary type="html">
<p>大型项目总避免不了各种模块间相互依赖,如果模块之间耦合严重,随着功能的增加,项目到后期会越来越难以维护。今天我们一起来思考下,大家常说的代码解耦到底要怎么做?<br>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>大型前端项目要怎么跟踪和分析函数调用链</title>
<link href="https://godbasin.github.io/2020/06/21/trace-stash/"/>
<id>https://godbasin.github.io/2020/06/21/trace-stash/</id>
<published>2020-06-21T06:41:31.000Z</published>
<updated>2020-06-21T06:42:54.833Z</updated>
<content type="html"><![CDATA[<p>相信不少有大型项目维护经验的小伙伴,都曾经遇到代码量太多、函数调用链路太长、断点断到头都要断等等问题。最近也在研究解决这个问题,本文分享下整体的思路和解决方案。<br><a id="more"></a></p><h2 id="方案设计"><a href="#方案设计" class="headerlink" title="方案设计"></a>方案设计</h2><p>简单做一个函数耗时分析的功能还是比较简单的,写代码也是比较容易的一部分。但如果让一个功能真正发挥它的价值,前期方案的设计和分析也是很重要的。</p><h3 id="现状"><a href="#现状" class="headerlink" title="现状"></a>现状</h3><p>一般来说,对于大型项目或是新人加入,维护过程(熟悉代码、定位问题、性能优化等)比较痛的有以下问题:</p><ul><li><strong>函数执行情况黑盒</strong><ul><li>函数调用链不清晰</li><li>函数耗时不清楚</li><li>代码异常定位困难</li></ul></li><li><strong>用户行为难以复现</strong></li></ul><p>要是遇到代码稍微复杂点,事件比较多、函数调用也特别多的,即使使用断点也能看到眼花,蒸汽眼罩都得多买一些(真的贵啊)。生活本来就比较难了,身为技术人的我们得用技术去提升下自身的生活和工作体验呀。</p><p>回到上面说到的现状,函数执行黑盒这块相信大家都比较容易考虑到,而在日常的问题定位中,很多情况下我们需要查用户的问题,但是用户的反馈常常表达上会和我们理解的不一致。那如果能直接还原用户的操作,那岂不是棒棒哒?</p><h3 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h3><p>那既然现状有了,我们可以根据自己的需要,把目标确定下来。</p><blockquote><p>个人觉得,即使是技术人员,前期的目标和现状分析也是很重要的。我们常常会遇到很多项目进行到一半发现和预期不一致、需要重新返工甚至只能放弃,往往是因为前期做的调研不充分,考虑到的情况还不够多。综上,设计的部分也需要好好去做,至于具体的方式,是手稿、文字、流程图、还是 PPT,可以根据个人喜好去选择。</p></blockquote><p>那么,我先来拆分下自己想要的功能是什么样子的:</p><ul><li>基础能力<ul><li>单个函数执行情况:函数名、入参、出参、耗时</li><li>全局辅助信息:函数调用链、调用次数统计</li></ul></li><li>便捷接入<ul><li>不改动源码</li></ul></li><li>易拓展<ul><li>可重放功能</li><li>可保存到服务器</li></ul></li></ul><p>首先,基本功能必不可少,主要包括函数的一些执行情况,例如调用链、函数名、类名、入参出参,还有性能分析相关的,包括耗时、函数调用次数的统计等。这些在我们分析和定位问题的时候,能派上不少的用场。</p><p>其次,对于这部分功能代码,需要满足易用性,包括易接入、易拓展等。易接入主要考虑不需要改动源代码,这也是代码设计中比较基础的要求了。易拓展则预留给后续想要在现有功能基础上添加新功能的时候,会相对简便。</p><h3 id="整体方案设计"><a href="#整体方案设计" class="headerlink" title="整体方案设计"></a>整体方案设计</h3><p>方案设计也不算复杂,基本上就是结合目标,然后以自己最熟练的方向作为起点,一点点把完整的功能视图补全。最后,再回顾下前面的现状和目标,分析设计出来的方案是否有脱离实际需要(有时候我们的脑补能力很强大,容易飘离本意)。</p><p>说起函数,最简单的就是给每个想要检测的函数包裹一层,除了调用原有的功能以外,新增对函数的一些数据采集,包括上面说到的单个函数执行信息和全局的辅助信息等。</p><p>要怎么方便地使用这些信息呢?我们可以通过堆栈的方式存下来,然后对这些信息进行处理来获取调用链、耗时等。通常来说,可以暴露全局变量的接口,来快速打印输出这些信息。</p><p>我们来看看设计方案:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/function-trace-global.png" alt></p><p>这里,将函数重放和上传服务器的优先级降低,先实现核心功能。工作内容的拆分、工作量的预估这些也都是方案设计中比较重要的部分,将大目标拆分成一个个小目标,这样对整体节奏、实现过程的把控会更有力。</p><h3 id="方案细节设计"><a href="#方案细节设计" class="headerlink" title="方案细节设计"></a>方案细节设计</h3><p>整体方案初步定了,我们需要考虑每个环节的细节方案。以一期的功能为主,流程包括以下:<br><strong>1. 监听函数执行</strong>。<br><strong>2. 采集函数执行情况</strong>:(调用链路、入参出参、耗时)。<br><strong>3. 暴露全局变量或 API</strong>。<br><strong>4. 使用全局变量或 API 打印调用链等</strong>。</p><p>由于这是一个非关键链路的功能,除了怎样的功能更方便使用以外,主要考虑这样的旁路功能不能影响主要功能的性能、不能因为一些异常导致正常功能无法使用。因此我们需要对每个流程进行一些分析和考虑:</p><ul><li>监听函数执行<ul><li>可通过依赖注入的方式,减少对源代码的入侵</li><li>代码实现多基于 Class,可考虑装饰器 Decorator 的方式</li></ul></li><li>采集函数执行情况<ul><li>需要注意性能和存储消耗,保证原有功能健壮性</li><li>考虑使用栈来存储<ul><li>存储考虑链路长度限制、参数长度限制、链路数量上限</li></ul></li><li>设置优先级,根据优先级选择性采集</li><li>旁路功能可考虑丢Worker执行<ul><li>考虑通信对性能的消耗</li></ul></li></ul></li></ul><p>我们能看到方案的一些细节如图:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/function-trace-p0.png" alt></p><p>至此,大致的方案设计已经完成。</p><h2 id="函数调用链的设计和实现"><a href="#函数调用链的设计和实现" class="headerlink" title="函数调用链的设计和实现"></a>函数调用链的设计和实现</h2><p>其实对于函数耗时统计的,网上也有一大堆的代码可以搜到,其中基于装饰器实现的也很多。由于我们项目中代码大多数都是基于 Class 设计的,因此装饰器这种不影响源代码的方式更加适合。</p><h3 id="单次追踪对象"><a href="#单次追踪对象" class="headerlink" title="单次追踪对象"></a>单次追踪对象</h3><p>装饰器的实现其实不难,网上也有很多可以参考的。而我们装饰器里的具体逻辑是怎样的,依赖我们设计的单次追踪对象和调用栈是怎样的。因此,我们可以先设计一下单次追踪对象。</p><p>该对象要实现的功能包括:</p><ol><li><strong>特殊 ID 标记本追踪对象(<code>traceId</code>)</strong>。创建该次对象的时候,自动生成该 ID。</li><li><strong>可更新追踪对象的信息(<code>update</code>方法)</strong>。</li><li><strong>执行该追踪对象(<code>exec</code>方法)</strong>。为重放功能做铺垫,如果我们存储了该函数以及函数入参,理想情况下可认为该函数可重放</li><li><strong>打印该追踪对象相关信息(<code>print</code>方法)</strong>。</li></ol><p>来,直接看大致的代码设计:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">interface IFunctionTraceInfo {</span><br><span class="line"> className?: string; <span class="comment">// 类名</span></span><br><span class="line"> functionName?: string; <span class="comment">// 函数名</span></span><br><span class="line"> inParams?: any[] | <span class="literal">null</span>; <span class="comment">// 入参</span></span><br><span class="line"> outParams?: any[] | <span class="literal">null</span>; <span class="comment">// 出参</span></span><br><span class="line"> timeConsuming?: number; <span class="comment">// 耗时</span></span><br><span class="line"> originFunction?: <span class="built_in">Function</span>; <span class="comment">// 原函数</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FunctionTrace</span> </span>{</span><br><span class="line"> traceId: string; <span class="comment">// 标记本次Trace</span></span><br><span class="line"> traceInfo: IFunctionTraceInfo;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span>(traceInfo: IFunctionTraceInfo) {</span><br><span class="line"> <span class="keyword">this</span>.traceId = <span class="keyword">this</span>.getRandomId();</span><br><span class="line"> <span class="keyword">this</span>.traceInfo = traceInfo;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 随机生成一个 ID 来标记</span></span><br><span class="line"> getRandomId() {</span><br><span class="line"> <span class="comment">// 时间戳(9位) + 随机串(10位)</span></span><br><span class="line"> <span class="keyword">return</span> (<span class="built_in">Date</span>.now()).toString(<span class="number">32</span>) + <span class="built_in">Math</span>.random().toString(<span class="number">32</span>).substring(<span class="number">2</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 更新该函数的一些信息</span></span><br><span class="line"> update(traceInfo: IFunctionTraceInfo) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 执行该函数</span></span><br><span class="line"> exec() {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打印该函数的一些信息</span></span><br><span class="line"> print() {</span><br><span class="line"> <span class="keyword">const</span> { className, functionName, timeConsuming } = <span class="keyword">this</span>.traceInfo;</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<span class="subst">${className}</span> -> <span class="subst">${functionName}</span>(<span class="subst">${<span class="keyword">this</span>.traceId}</span>): <span class="subst">${timeConsuming}</span>`</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="追踪堆栈"><a href="#追踪堆栈" class="headerlink" title="追踪堆栈"></a>追踪堆栈</h3><p>除了单次的追踪堆栈,我们还需要根据函数执行的顺序等维护完整的调用链信息,因此我们需要一个堆栈来维护完整的调用链。</p><p>那么,这个堆栈的功能也应该包括:</p><ol><li><strong>当前的层次(<code>level</code>)</strong>。为调用链做铺垫,可认为函数开始执行的时候<code>level++</code>,函数结束的时候<code>level--</code>。<ul><li>当多个函数交错执行的时候(例如事件触发),该方式可能不准确</li></ul></li><li><strong>堆栈信息(<code>traceList</code>)</strong>。</li><li><strong>开始记录某次追踪(<code>start</code>方法)</strong>。添加该次追踪之后将<code>level++</code>,便于记录当前追踪的层次。</li><li><strong>结束记录某次追踪(<code>end</code>方法)</strong>。<code>level--</code>。</li><li><strong>获取某次追踪对象(<code>getTrace</code>方法)</strong>。可用于单次追踪对象的信息获取和操作。</li><li><strong>打印堆栈信息</strong>。结合当前层次,通过缩放打印对应的调用信息,可包括耗时等。</li><li><strong>打印堆栈中函数的调用次数</strong>。以调用次数该维度打印堆栈中的追踪信息,可用于分析函数调用次数是否符合预期。</li></ol><p>同样的,我们来看看代码:<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">interface StashFunctionTrace {</span><br><span class="line"> traceLevel?: number;</span><br><span class="line"> trace: FunctionTrace;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FunctionTraceStash</span> </span>{</span><br><span class="line"> level: number; <span class="comment">// 当前层级,默认为0</span></span><br><span class="line"> traceList: StashFunctionTrace[]; <span class="comment">// Trace数组</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span>() {</span><br><span class="line"> <span class="keyword">this</span>.level = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">this</span>.traceList = [];</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 开始本次 Trace</span></span><br><span class="line"> <span class="comment">// 添加该 Trace 之后将 level + 1,便于记录当前 Trace 的层次</span></span><br><span class="line"> start(trace: FunctionTrace) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 结束本次 Trace</span></span><br><span class="line"> end() {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 根据 traceId 获取某个 Trace 对象</span></span><br><span class="line"> getTrace(traceId: string): StashFunctionTrace | <span class="literal">null</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.traceList.find(<span class="function">(<span class="params">stashTrace</span>) =></span> stashTrace.trace.traceId === traceId) || <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打印 Trace 堆栈信息</span></span><br><span class="line"> printTraceList(): string {</span><br><span class="line"> <span class="keyword">const</span> traceStringList: string[] = [];</span><br><span class="line"> <span class="keyword">this</span>.traceList.forEach(<span class="function">(<span class="params">stashTrace</span>) =></span> {</span><br><span class="line"> <span class="keyword">let</span> prefix = <span class="string">''</span>;</span><br><span class="line"> <span class="keyword">if</span> (stashTrace.traceLevel && stashTrace.traceLevel > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 根据层次,前置 tab</span></span><br><span class="line"> prefix = <span class="keyword">new</span> <span class="built_in">Array</span>(stashTrace.traceLevel).join(<span class="string">'\t'</span>);</span><br><span class="line"> }</span><br><span class="line"> traceStringList.push(prefix + stashTrace.trace.print());</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> traceStringList.join(<span class="string">'\n'</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打印函数调用次数统计</span></span><br><span class="line"> printTraceCount(className?: string, functionName?: string) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 重放该堆栈</span></span><br><span class="line"> replay() {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 清空该堆栈信息</span></span><br><span class="line"> clear() {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h3 id="装饰器逻辑"><a href="#装饰器逻辑" class="headerlink" title="装饰器逻辑"></a>装饰器逻辑</h3><p>到这里,我们可以确定装饰器需要进行哪些操作:</p><ol><li>生成追踪记录<code>new Trace(执行信息)</code>,包括入参、类名、方法名等。</li><li><code>TraceStash.add(Trace)</code>添加层次。</li><li><code>originFun()</code>包裹原有函数、执行。</li><li><code>Trace.update()</code>更新一些信息,包括函数耗时、出参等。</li><li><code>TraceStash.end()</code>结束本次调用。</li></ol><p>为了方便使用,我们可以设计基于<code>Class</code>的装饰器,以及基于<code>Class.methods</code>方法的装饰器,还可以基于单函数的装饰器。</p><p>我们还可以通过AST分析自动给代码中需要的部分添加上装饰器。至于装饰器具体实现,大家下来可以自己想一下。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>很多人喜欢拿了任务就直接开撸,然后就会在写代码的过程中发现一个又一个问题。幸运的话,最终能做出想要的效果。而运气不好的话,可能得推倒重来了。<br>而在开始写代码之前,稍微进行一些分析、思考和调研,可以得到事半功倍的效果。</p>]]></content>
<summary type="html">
<p>相信不少有大型项目维护经验的小伙伴,都曾经遇到代码量太多、函数调用链路太长、断点断到头都要断等等问题。最近也在研究解决这个问题,本文分享下整体的思路和解决方案。<br>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
<entry>
<title>多人协作如何进行冲突处理</title>
<link href="https://godbasin.github.io/2020/06/14/operation-transform/"/>
<id>https://godbasin.github.io/2020/06/14/operation-transform/</id>
<published>2020-06-14T08:23:31.000Z</published>
<updated>2020-06-14T15:06:03.805Z</updated>
<content type="html"><![CDATA[<p>最近接触到一些针对多人同时操作进行冲突处理的场景,简单介绍下相关的实现方式。<br><a id="more"></a></p><h2 id="Operational-transformation-OT"><a href="#Operational-transformation-OT" class="headerlink" title="Operational transformation(OT)"></a>Operational transformation(OT)</h2><p>OT 算法最初是为在纯文本文档的协作编辑中的一致性维护和并发控制而发明的,在本文中我们也主要掌握一致性维护相关的一些方法。</p><h3 id="协同软件的冲突"><a href="#协同软件的冲突" class="headerlink" title="协同软件的冲突"></a>协同软件的冲突</h3><p>想必大家都知道,在多人协同场景下,必然会出现各种各样的冲突场景。</p><p>举个例子,团队接到一个超大型的项目需要开发,老板说10分钟给出排期和分工。然后瑟瑟发抖的大家二话不说打开腾讯文档,创建了一个表格,让每个模块负责人先针对自己模块来进行工作量拆分和预估。</p><p>PM 创建了表格之后,将表格丢到群里,说前端后台各自创建一个子表来写相应的工作量情况。</p><p>前端张三马上点开了表格,点击添加子表,系统自动生成“工作表2”这样一个子表。与此同时,后台李四也进行了同样的操作。</p><p>那么问题来了,一个表格中原则上并不允许两个同样名字的子表,这个时候冲突就出现了,我们要怎么处理呢?</p><p>虽然是两个同样名字的子表,但我们并不能将它们进行合并,因为对于张三和李四来说,他们就是在自己创建的子表里写工作拆分和排期情况。所以,我们需要使用对用户影响最小的方式,来解决掉这个冲突。</p><h3 id="操作的拆分"><a href="#操作的拆分" class="headerlink" title="操作的拆分"></a>操作的拆分</h3><p>为了处理冲突,我们需要将一些操作进行拆分。例如,我们插入一个子表这样一个操作,除了插入自身的操作,可能需要对其他子表进行移动操作。那么,对于一个子表来说,我们的操作可能会包括:</p><ul><li>插入</li><li>重命名</li><li>移动</li><li>删除</li><li>更新内容</li><li>…</li></ul><p>只要拆分得足够仔细,对于子表的所有用户行为,都可以由这些操作来组合成最终的效果。例如,复制粘贴一张子表,可以拆分为<code>插入-重命名-更新内容</code>;剪切一张子表,可以拆分为<code>插入-更新内容-删除-移动其他子表</code>。通过分析用户行为,我们可以提取出这些基本操作。</p><h3 id="操作间的冲突处理"><a href="#操作间的冲突处理" class="headerlink" title="操作间的冲突处理"></a>操作间的冲突处理</h3><p>基本操作提取出来之后,我们就可以很仔细地梳理和分析操作和操作之间是否会产生冲突,以及要怎么处理了。</p><p>例如,我们上面提取出来的关于子表的操作中,包括<code>插入</code>、<code>重命名</code>、<code>移动</code>、<code>删除</code>、<code>更新内容</code>五种操作,实际上,每种操作都可能和自身、以及其他四种操作都发生冲突,于是我们可能有<code>5*5=25</code>种需要考虑的冲突情况。</p><p>我们先来大致看看这 25 组冲突中,是不是全都需要进行冲突处理的。例如,<code>更新内容</code>一般来说跟其他几个操作都不会发生什么冲突,因为更新内容改变的是表格的内容,而不是位置、名字这些,一个表格内部和另一个表格内部基本上不会发生冲突。但<code>重命名</code>和<code>插入</code>之间,满足一定条件的时候(插入的子表名字和重命名的名字相同)可能就会产生冲突。</p><p>你可能会觉得疑惑,<code>插入-重命名</code>和<code>重命名-插入</code>不是一样的吗?我们先带着这个疑问继续往后看。</p><h3 id="最终一致性的实现"><a href="#最终一致性的实现" class="headerlink" title="最终一致性的实现"></a>最终一致性的实现</h3><p>说了那么多,看起来跟 OT 算法完全没有关系呀??</p><p>OT 算法的一个核心目标,是实现最终一致性。为什么会有最终一致性的需求呢?</p><p>我们再看回张三和李四的例子,由于系统不允许存在有两个同样名称的子表,因此服务器会根据收到请求的顺序,将第二个子表进行重命名。假设张三的请求先到达服务端,那么李四创建的表格则需要被自动重命名为“工作表2(自动重命名)”。</p><p>为了让用户体验更流畅,我们在用户侧使用无锁、非阻塞的方式来进行协同。也就是对于李四来说,他已经创建了这样一个叫“工作表2”的子表了,由于网络延迟等原因可能还已经编辑上了。这时候服务端告诉李四,张三已经创建了一个“工作表2”的子表了,你自己看着办吧。</p><p>李四说,我已经编辑了这么多,你总不能让我全删掉重来吧。所以李四想了个办法,先将自己本地的表格<code>重命名</code>为”工作表2(自动重命名)”,然后将张三的子表<code>插入</code>。除此之外,由于自己的插入顺序在后面,还需要将自己的子表<code>移动</code>到后面一个位置。做完这些操作之后,李四告诉服务器,自己也<code>插入</code>了一个叫“工作表2(自动重命名)”的子表。</p><p>我们梳理下逻辑,可以得到:</p><ul><li>对于李四本地,需要进行的操作是:<code>重命名 + 插入 + 移动</code></li><li>对于服务器,需要进行的操作是:<code>插入</code>更新后的子表</li></ul><p>我们来看看这个 OT 算法的简略说明图:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/google_ot.jpg" alt></p><p>我们代入到张三李四这个场景下:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/sheet_ot.png" alt></p><p>可以看到,对于服务端来说,最终就是新增了两个子表,一个是张三的“工作表2”,另一个是李四的“工作表2(自动重命名)”。</p><p>除此之外,这个场景中还存在比较细致的时间问题。上面我们说李四收到服务器发来的张三的操作之后,在本地进行<code>重命名 + 插入 + 移动</code>,然后告诉服务器的操作是<code>插入</code>更新后的子表。但是还有个可能性,就是李四收到服务器的消息之前,就已经把自己<code>插入</code>“工作表2”的操作发出去给服务器了。这种情况下,服务器也需要具备处理冲突的能力,来维持最终一致性。</p><p>也就是说,我们在本地和服务器都有一套一致的冲突处理逻辑,才能保证算法的最终一致性。</p><p>但除了最终一致性,冲突处理还有其他很多需要考虑的场景,例如版本管理、性能问题等,后面有机会再慢慢介绍吧。</p>]]></content>
<summary type="html">
<p>最近接触到一些针对多人同时操作进行冲突处理的场景,简单介绍下相关的实现方式。<br>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
</entry>
</feed>