forked from godbasin/godbasin.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
461 lines (275 loc) · 474 KB
/
atom.xml
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
460
461
<?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-10-31T03:20:48.768Z</updated>
<id>https://godbasin.github.io/</id>
<author>
<name>被删</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>Angular框架解读--Ivy编译器之CLI编译器</title>
<link href="https://godbasin.github.io/2021/10/31/angular-design-ivy-2-cli-compiler/"/>
<id>https://godbasin.github.io/2021/10/31/angular-design-ivy-2-cli-compiler/</id>
<published>2021-10-31T03:01:12.000Z</published>
<updated>2021-10-31T03:20:48.768Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中CLI层面的编译器编译过程。</p><a id="more"></a><p>在 Angular 中实现了自己的编译器,来处理 TypeScript 编译器无法完全做到的一些事情。在 Ivy 编译器中,这部分的能力又做了进一步的提升,比如模板类型检查、模块依赖解析等等。</p><h2 id="Ivy-编译器"><a href="#Ivy-编译器" class="headerlink" title="Ivy 编译器"></a>Ivy 编译器</h2><p>在前面<a href="https://godbasin.github.io/2021/08/15/angular-design-ivy-0-design/">Angular 框架解读–Ivy 编译器整体设计</a>一文中,我有介绍 Ivy 编译器主要包括两部分:</p><ol><li><code>ngtsc</code>:作为主要的 Ivy 编译器,将 Angular 装饰器化为静态属性。</li><li><code>ngcc</code>:作为兼容性的 Ivy 编译器,主要负责处理来自 NPM 的代码并生成等效的 Ivy 版本。</li></ol><p>本文将会主要围绕<code>ngtsc</code>该编译器进行介绍。</p><h3 id="Angular-中的-AST-解析"><a href="#Angular-中的-AST-解析" class="headerlink" title="Angular 中的 AST 解析"></a>Angular 中的 AST 解析</h3><p>要实现 AST 的解析和转换,离不开解析器。对于 Typescript 代码来说,编译器的整体流程为:</p><figure class="highlight gherkin"><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="string">------------</span>|</span><br><span class="line"> |<span class="string">----------------------------------> </span>|<span class="string"> TypeScript </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string"> .d.ts </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string">------------</span>|</span><br><span class="line"> |</span><br><span class="line">|<span class="string">------------</span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string">------------</span>|</span><br><span class="line">|<span class="string"> TypeScript </span>|<span class="string"> -parse-> </span>|<span class="string"> AST </span>|<span class="string"> ->transform-> </span>|<span class="string"> AST </span>|<span class="string"> ->print-> </span>|<span class="string"> JavaScript </span>|</span><br><span class="line">|<span class="string"> source </span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string"> source </span>|</span><br><span class="line">|<span class="string">------------</span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string">------------</span>|</span><br><span class="line"> |<span class="string"> type-check </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string"> </span>|</span><br><span class="line"> |<span class="string"> v </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string">--------</span>|<span class="string"> </span>|</span><br><span class="line"> |<span class="string">--> </span>|<span class="string"> errors </span>|<span class="string"> <---</span>|</span><br><span class="line"> |<span class="string">--------</span>|</span><br></pre></td></tr></table></figure><p>该过程包括四个步骤:</p><ol><li>parse 解析:它是一个传统的递归下降解析器,稍微调整以支持增量解析,它发出一个抽象语法树 (AST),有助于识别文件中导入了哪些文件。</li><li>type-check 类型检查器:类型检查器构建一个符号表,然后对文件中的每个表达式进行类型分析,报告它发现的错误。</li><li>transform 转换:转换步骤是一组 AST 到 AST 转换,它们执行各种任务,例如删除类型声明、将模块和类声明降低到 ES5、将异步方法转换为状态机等。</li><li>print 打印:TS 到 JS 的实际转换是整个过程中最昂贵的操作。</li></ol><p>在了解 Angular 是如何处理之前,我们需要知道,对 TypeScript 编译器 API 的任何使用都遵循一个多步骤过程,包括:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-design-ivy-2-ast-1.jpg" alt></p><ul><li>一个<code>ts.CompilerHost</code>被创建</li><li><code>ts.CompilerHost</code>加上一组“根文件”,用于创建<code>ts.Program</code>,<code>ts.Program</code>用于收集各种诊断(类型检查)</li><li><code>ts.Program</code>被要求<code>emit</code>,并生成 JavaScript 代码</li></ul><p>将 Angular 编译集成到此过程中的编译器遵循非常相似的流程,但有一些额外的步骤:</p><ul><li>一个<code>ts.CompilerHost</code>被创建</li><li><code>ts.CompilerHost</code>包含在<code>NgCompilerHost</code>中,它将 Angular 特定文件添加到编译中</li><li><code>ts.Program</code>是从<code>NgCompilerHost</code>及其增强的根文件集创建的</li><li>一个<code>CompilationTicket</code>被创建,可选择合并来自先前编译运行的任何状态</li><li><code>NgCompiler</code>是使用<code>CompilationTicket</code>创建的</li><li>诊断信息可以正常从<code>ts.Program</code>收集,也可以从<code>NgCompiler</code>收集</li><li>在发射之前,调用<code>NgCompiler.prepareEmit</code>以检索需要馈送到<code>ts.Program.emit</code>的 Angular 转换器</li><li>使用上面的 Angular 转换器在<code>ts.Program</code>上调用发射,它生成带有 Angular 扩展的 JavaScript 代码</li></ul><p>在这些 Angular 特定的步骤中,主要进行几件事:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-design-ivy-2-ast-2.jpg" alt></p><ol><li>会将特定于 Angular 的文件添加到编译过程中,比如<code>NgModele</code>、<code>Component</code>的解析。</li><li>修改生成的<code>d.ts</code>,来保存 Angular 中模块和文件间的依赖关系。</li><li>会增加 Angular 中的类型校验,包括<code><tmeplate></code>模板的类型校验。</li></ol><p>而在自定义 TypeScript 编译器中执行 Angular 编译,主要依赖于<code>NgCompiler</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> NgCompiler {</span><br><span class="line"> <span class="comment">// 将一个 CompilationTicket 转换为一个用于请求编译的 NgCompiler 实例</span></span><br><span class="line"> <span class="comment">// 根据编译请求的性质,NgCompiler 实例可能会从以前的编译中重用并随着任何更改进行更新</span></span><br><span class="line"> <span class="comment">// 它可能是一个新实例,它可以增量地重用以前编译中的状态,或者它可能代表一个完全新的编译 </span></span><br><span class="line"> <span class="keyword">static</span> fromTicket(ticket: CompilationTicket, adapter: NgCompilerAdapter, perfRecorder?: PerfRecorder) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 获取文件的资源依赖</span></span><br><span class="line"> getResourceDependencies(file: ts.SourceFile): <span class="built_in">string</span>[] {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 获取此编译的所有与 Angular 相关的诊断信息</span></span><br><span class="line"> getDiagnostics(): ts.Diagnostic[] {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将 Angular.io 错误指南链接添加到此编译的诊断中</span></span><br><span class="line"> <span class="keyword">private</span> addMessageTextDetails(diagnostics: ts.Diagnostic[]): ts.Diagnostic[] {}</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 获取 ts.Program 以用作生成后续增量编译的起点</span></span><br><span class="line"> <span class="comment">// NgCompiler 产生一个内部增量 TypeScript 编译(为了模板类型检查的目的,将消费者的 `ts.Program` 继承到一个新的编译器中)</span></span><br><span class="line"> <span class="comment">// 此操作后,消费者的 ts.Program 不再可用于启动新的增量编译,getNextProgram 检索可以替代使用的 ts.Program</span></span><br><span class="line"> getNextProgram(): ts.Program {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 异步执行 Angular 的分析步骤</span></span><br><span class="line"> <span class="comment">// 通常,每当调用 getDiagnostics 或 prepareEmit 时,都会延迟执行此操作</span></span><br><span class="line"> <span class="comment">// 然而,某些消费者可能希望允许分析的异步阶段,其中诸如 “styleUrls” 之类的资源被异步解析</span></span><br><span class="line"> <span class="comment">// 在这些情况下,必须首先调用 analyzeAsync,并且在调用 NgCompiler 的任何其他 API 之前等待它的 Promise</span></span><br><span class="line"> <span class="keyword">async</span> analyzeAsync(): <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"> listLazyRoutes(entryRoute?: <span class="built_in">string</span>): LazyRoute[] {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可见,<code>NgCompiler</code>主要负责将 Angular 编译集成到 TypeScript 编译器的编译流程中,并支持了上述提到的错误信息诊断(类型检查)、依赖关系检索,其中的设计还支持了增量编译、异步编译等能力。</p><h2 id="ngtsc-编译器"><a href="#ngtsc-编译器" class="headerlink" title="ngtsc 编译器"></a>ngtsc 编译器</h2><p><code>ngtsc</code>是一个 Typescript-to-Javascript 编译器。它是一个最小包装器,包裹在<code>tsc</code>之外,而<code>tsc</code>中则包含一系列的 Angular 变换。</p><h3 id="编译器流程"><a href="#编译器流程" class="headerlink" title="编译器流程"></a>编译器流程</h3><p>和<code>tsc</code>一样,当<code>ngtsc</code>开始运行时,它首先解析<code>tsconfig.json</code>文件,然后创建一个<code>ts.Program</code>。在上述转换可以运行之前,需要进行几件事情:</p><ul><li>为包含修饰符的输入源文件收集元数据</li><li><code>@Component</code>装饰器中列出的资源文件必须异步解析<ul><li>例如 CLI 中,可能希望运行的 WebPack 以产生<code>.css</code>输入到<code>styleUrls</code>的属性<code>@Component</code></li></ul></li><li>运行诊断程序,这会创建<code>TypeChecker</code>并触及程序中的每个节点(一个相当昂贵的操作)</li></ul><p>因为资源加载是异步的(特别是可能通过子进程并发),所以最好在做任何昂贵的事情之前启动尽可能多的资源加载。</p><p><code>ngtsc</code>的运行入口位于<code>NgtscProgram</code>中,可直接替代传统的 View Engine 编译器到诸如命令行<code>main()</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><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">export</span> <span class="keyword">class</span> NgtscProgram <span class="keyword">implements</span> api.Program {</span><br><span class="line"> readonly compiler: NgCompiler;</span><br><span class="line"> <span class="comment">// 主要的 TypeScript 程序,用于分析和发出</span></span><br><span class="line"> <span class="keyword">private</span> tsProgram: ts.Program;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> closureCompilerEnabled: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="keyword">private</span> host: NgCompilerHost;</span><br><span class="line"> <span class="keyword">private</span> incrementalStrategy: TrackedIncrementalBuildStrategy;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 其他方法</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译器流程如下所示:</p><ol><li>创建<code>ts.Program</code>。</li><li>扫描源文件以查找具有微不足道的可检测<code>@Component</code>注释的顶级声明,这避免了创建<code>TypeChecker</code>。<ul><li>对于每个具有<code>templateUrlor</code>的此类声明<code>styleUrls</code>,启动该 URL 的资源加载并将加入<code>Promise</code>队列</li></ul></li><li>获取诊断信息并报告任何初始错误消息。此时,<code>TypeChecker</code>已准备就绪。</li><li>对<code>@Component</code>注释进行彻底扫描,使用<code>TypeChecker</code>和元数据系统来解析任何复杂的表达式。</li><li>等待所有资源得到解决。</li><li>计算需要应用的一组变换。</li><li>启动<code>Tsickle</code>发射,它运行变换。</li><li>在<code>.d.ts</code>文件的发出回调期间,重新解析发出的<code>.d.ts</code>并合并来自<code>Angular</code>编译器的任何请求更改。</li></ol><p>Angular 编译涉及将 Angular 装饰器转换为静态定义字段。在构建时,这是在 TypeScript 编译的整个过程中完成的,其中 TypeScript 代码经过类型检查,然后降级为 JavaScript 代码。在此过程中,还可以生成特定于 Angular 的诊断。</p><h3 id="增量编译"><a href="#增量编译" class="headerlink" title="增量编译"></a>增量编译</h3><p>前面我们介绍了 Ivy 编译器的一些特性,其中包括了通过增加增量编译,来缩短构建时间。</p><p>作为在 TypeScript 编译器中执行 Angular 编译的核心 API,<code>NgCompiler</code>的每个实例都支持单个编译,因此也支持增量编译。</p><p>Angular 编译器能够进行增量编译,其中来自先前编译的信息用于加速下一次编译。在编译期间,编译器产生两种主要信息:本地信息(如组件和指令元数据)和全局信息(如具体化的<code>NgModule</code>范围)。增量编译通过两种方式进行管理:</p><ol><li>对于大多数更改,新的<code>NgCompiler</code>可以有选择地从以前的实例继承本地信息,并且只需要在底层 TypeScript 文件发生更改的地方重新计算它。在这种情况下,全局信息总是从头开始重新计算。</li><li>对于特定的更改,例如组件资源中的更改,<code>NgCompiler</code>可以整体重用,并更新以合并此类更改的影响,而无需重新计算任何其他信息。</li></ol><p>请注意,这两种模式在是否需要新<code>NgCompiler</code>实例或是否可以重用之前的实例方面有所不同。为了防止泄漏这种实现的复杂性并保护消费者不必管理<code>NgCompiler</code>如此具体的生命周期,这个过程通过<code>CompilationTicket</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> CompilationTicket =</span><br><span class="line"> <span class="comment">// 从头开始 Angular 编译操作</span></span><br><span class="line"> FreshCompilationTicket | </span><br><span class="line"> <span class="comment">// 开始包含对 TypeScript 代码的更改的 Angular 编译操作</span></span><br><span class="line"> IncrementalTypeScriptCompilationTicket | </span><br><span class="line"> IncrementalResourceCompilationTicket;</span><br></pre></td></tr></table></figure><p><code>CompilationTicket</code>用于初始化(或更新)<code>NgCompiler</code>实例,该实例为 Angular 编译器的核心。<code>CompilationTicket</code>抽象了编译的起始状态,并允许独立于任何增量编译生命周期管理<code>NgCompiler</code>。</p><p>消费者首先获得一个<code>CompilationTicket</code>(取决于传入更改的性质),然后使用该票据获取<code>NgCompiler</code>实例。在创建<code>CompilationTicket</code>时,编译器可以决定是重用旧<code>NgCompiler</code>实例还是创建新实例。</p><h3 id="异步编译"><a href="#异步编译" class="headerlink" title="异步编译"></a>异步编译</h3><p>在某些编译环境(例如 Angular CLI 中的 Webpack 驱动编译)中,编译的各种输入只能以异步方式生成。例如,<code>styleUrls</code>链接到 SASS 文件的 SASS 编译需要产生一个子 Webpack 编译。为了支持这一点,Angular 有一个异步接口来加载这些资源。</p><p>如果使用此接口,则<code>NgCompiler</code>创建后的另一个异步步骤是调用<code>NgCompiler.analyzeAsync</code>并等待其<code>Promise</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> NgtscProgram <span class="keyword">implements</span> api.Program {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 确保 NgCompiler 已正确分析程序,并允许在此过程中异步加载任何资源。</span></span><br><span class="line"><span class="comment"> * Angular CLI 使用它来允许为 styleUrls 中使用的 SASS 文件等内容生成(异步)子编译。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> loadNgStructureAsync(): <span class="built_in">Promise</span><<span class="built_in">void</span>> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.compiler.analyzeAsync();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此操作完成后,所有资源均已加载,其余<code>NgCompilerAPI</code>可以同步使用。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Angular 是一套大而全的解决方案,想必大家早已对此有所了解。但实际上 Angular 做了很多深度的设计和能力,包括给开发者更好的体验,比如模板类型检查中,是如何将这些 Angular 特定的类型检查能力添加到 TypeScript 编译过程中,并且能通过文件映射能准确反馈给用户具体的代码位置,这些都是作为开发者的我未曾考虑过的。</p><p>感觉 Angular 里面还有特别多值得学习的东西。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://github.com/angular/angular/blob/master/packages/compiler/design/architecture.md" target="_blank" rel="noopener">DESIGN DOC(Ivy): Compiler Architecture</a></li><li><a href="https://indepth.dev/posts/1151/a-deep-dive-into-injectable-and-providedin-in-ivy" target="_blank" rel="noopener">A Deep Dive into @Injectable and providedIn in Ivy</a></li><li><a href="https://indepth.dev/posts/1062/ivy-engine-in-angular-first-in-depth-look-at-compilation-runtime-and-change-detection" target="_blank" rel="noopener">Ivy engine in Angular: first in-depth look at compilation, runtime and change detection</a></li><li><a href="https://indepth.dev/posts/1053/everything-you-need-to-know-about-change-detection-in-angular" target="_blank" rel="noopener">Everything you need to know about change detection in Angular</a></li><li><a href="https://www.youtube.com/watch?v=anphffaCZrQ" target="_blank" rel="noopener">Deep Dive into the Angular Compiler | Alex Rickabaugh</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中CLI层面的编译器编译过程。</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/10/16/web-browser-render/"/>
<id>https://godbasin.github.io/2021/10/16/web-browser-render/</id>
<published>2021-10-16T08:22:23.000Z</published>
<updated>2021-10-16T08:23:43.143Z</updated>
<content type="html"><![CDATA[<p>作为前端开发,我们的日常工作中除了写代码之外,几乎大多数的时间都在跟浏览器打交道。当然,现在我们甚至写代码都可以直接在浏览器里完成,一个浏览器走天下。</p><p>因此,我们应该对浏览器的了解要更加深入,除了了解怎么使用和调试浏览器,我们还要掌握它是怎样将我们编写的代码渲染到页面中的。</p><a id="more"></a><h2 id="认识浏览器"><a href="#认识浏览器" class="headerlink" title="认识浏览器"></a>认识浏览器</h2><p>浏览器的主要功能,是通过向服务器请求并在浏览器窗口中展示 Web 资源内容,通常包括 HTML 文档、PDF、图片等,我们也可以通过插件的方式加载更多其他的资源类型(比如播放视频)。</p><p>对于浏览器的问题,HTTP 请求相关的,想必各位在面试的时候都被问烂了吧,这里直接过一下浏览器中的 HTTP 请求过程:</p><ol><li>DNS 域名解析(此处涉及 DNS 的寻址过程),找到网页的存放服务器。</li><li>浏览器与服务器建立 TCP 连接。</li><li>浏览器发起 HTTP 请求。</li><li>服务器响应 HTTP 请求,返回该页面的 HTML 内容。</li><li>浏览器解析 HTML 代码,并请求 HTML 代码中的资源(如 JavaScript、CSS、图片等,此处可能涉及 HTTP 缓存)。</li><li>浏览器对页面进行渲染呈现给用户。</li></ol><p>这篇文章会重点介绍第 6 步,该步骤涉及浏览器的渲染过程和原理。除了初次加载页面,用户的很多操作都同样涉及到浏览器渲染,比如以下功能:</p><ul><li>地址栏输入 URL</li><li>点击刷新和停止按钮,控制页面加载</li><li>点击后退和前进按钮,快速实现页面跳转</li><li>书签和收藏,快速打开页面</li></ul><p>除了这些,实际上我们和浏览器的几乎所有操作,都涉及到浏览器的渲染过程。为了更深刻地认识这些过程,我们先来认识下浏览器的结构。</p><p>HTML 和 CSS 规范中规定了浏览器解析和渲染 HTML 文档的方式,曾经各个浏览器都只遵循其中一部分,因此前端开发经常需要兼容各种浏览器。现在这些问题已经得到改善,同时配合 Babel 等一些兼容性处理编译过程,我们可以更加关注网站的功能实现和优化。</p><h3 id="浏览器的结构"><a href="#浏览器的结构" class="headerlink" title="浏览器的结构"></a>浏览器的结构</h3><p>从结构上来说,浏览器主要包括了八个子系统:用户界面、浏览器引擎、渲染引擎、网络子系统、JavaScript 解释器、XML 解析器、显示后端、数据持久性子系统。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/how-browser-works-1.jpg" alt></p><p>这些子系统组合构成了我们的浏览器,而谈到页面的加载和渲染,则离不开网络子系统、渲染引擎、JavaScript 解释器和浏览器引擎等。</p><p>如今大多数用户主要使用的浏览器包括两类:</p><ul><li>台式机:Chrome、Internet Explorer、Firefox、Safari、Opera 等</li><li>移动设备:Android 浏览器、iPhone、Opera Mini、Opera Mobile、UC 浏览器、Chrome 等。</li></ul><p>下面我们以前端开发最常使用的 Chrome 浏览器为例(因为 Chrome 浏览器太牛啦,而且它们还要官方文章介绍做参考),进行更详细的介绍。</p><h3 id="Chrome-多进程架构"><a href="#Chrome-多进程架构" class="headerlink" title="Chrome 多进程架构"></a>Chrome 多进程架构</h3><p>应该很多前端开发都知道,Chrome 浏览器使用了多进程架构,包括浏览器进程、渲染器进程、插件进程和 GPU 进程:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/browserui.png" alt></p><p>如今,基本上所有的浏览器都支持多个选项卡。在 Chrome 中,每个选项卡在单独的渲染器进程中运行,渲染器进程主要用于控制和处理选项卡中的网站内容显示。渲染器进程支持多线程,包括:</p><ul><li>GUI 渲染线程:负责对浏览器界面进行渲染</li><li>JavaScript 引擎线程:负责解析和执行 JavaScript 脚本</li><li>浏览器定时器触发线程:<code>setTimeout</code>和<code>setInterval</code>所在的线程</li><li>浏览器事件触发线程:该线程负责处理浏览器事件,并将事件触发后需要执行的代码放置到 JavaScript 引擎中执行</li></ul><p>选项卡之外的所有内容都由浏览器进程处理,浏览器进程则主要用于控制和处理用户可见的 UI 部分(包括地址栏,书签,后退和前进按钮)和用户不可见的隐藏部分(例如网络请求和文件访问)。浏览器进程同样支持多线程,包括:</p><ul><li>UI 线程:用于绘制浏览器的按钮和输入字段</li><li>网络线程:用于处理网络请求,以及从服务器接收数据</li><li>存储线程:用于控制对文件的访问</li></ul><p>这些线程其实我们在学习其他内容的时候也会涉及到,比如在页面的加载过程中,涉及 GUI 渲染线程与 JavaScript 引擎线程间的互斥关系,因此页面中的<code><script></code>和<code><style></code>元素设计不合理会影响页面加载速度。</p><p>除此之外,UI 线程、网络线程、存储线程、浏览器事件触发线程、浏览器定时器触发线程中 I/O 事件通过异步任务完成时触发的函数回调,解决了单线程的 Javascript 阻塞问题。结合 Event Loop 的并发模型设计,解决了 Javascript 中同步任务和异步任务的管理问题。</p><p>下面我们来介绍浏览器中页面的渲染过程,该部分内容同样基于 Chrome 浏览器,更加详细地介绍浏览器进程和线程如何通信来显示页面。</p><h2 id="浏览器中页面的渲染过程"><a href="#浏览器中页面的渲染过程" class="headerlink" title="浏览器中页面的渲染过程"></a>浏览器中页面的渲染过程</h2><p>首先我们将浏览器中页面的渲染过程分为两部分:</p><ol><li>页面导航:用户输入 URL,浏览器进程进行请求和准备处理。</li><li>页面渲染:获取到相关资源后,渲染器进程负责选项卡内部的渲染处理。</li></ol><h3 id="1-页面导航"><a href="#1-页面导航" class="headerlink" title="1. 页面导航"></a>1. 页面导航</h3><p>前面我们介绍了一个 HTTP 的请求过程,该部分内容更倾向于将浏览器当成一个完整的对象,来介绍浏览器与外界的交互过程。</p><p>下面,我们来深入浏览器内部来进行分析,当用户在地址栏中输入内容时:</p><ol><li>首先浏览器进程的 UI 线程会进行处理:如果是 URI,则会发起网络请求来获取网站内容;如果不是,则进入搜索引擎。</li><li>如果需要发起网络请求,请求过程由网络线程来完成。HTTP 请求响应如果是 HTML 文件,则将数据传递到渲染器进程;如果是其他文件则意味着这是下载请求,此时会将数据传递到下载管理器。</li><li>如果请求响应为 HTML 内容,此时浏览器应导航到请求站点,网络线程便通知 UI 线程数据准备就绪。</li><li>接下来,UI 线程会寻找一个渲染器进程来进行网页渲染。当数据和渲染器进程都准备好后,HTML 数据通过 IPC 从浏览器进程传递到渲染器进程中。</li><li>渲染器进程接收 HTML 数据后,将开始加载资源并渲染页面。</li><li>渲染器进程完成渲染后,通过 IPC 通知浏览器进程页面已加载。</li></ol><p>以上是用户在地址栏输入网站地址,到页面开始渲染的整体过程。如果当前页面跳转到其他网站,浏览器将调用一个单独的渲染进程来处理新导航,同时保留当前渲染进程来处理像<code>unload</code>这类事件。</p><p>可以看到,页面导航的过程主要依赖浏览器进程。其中,上述过程中的步骤 5 便是页面的渲染部分,该过程同样依赖渲染器进程,我们一起来看看。</p><h3 id="2-页面渲染"><a href="#2-页面渲染" class="headerlink" title="2. 页面渲染"></a>2. 页面渲染</h3><p>前面说过,渲染器进程负责选项卡内部发生的所有事情,它的核心工作是将 HTML、CSS 和 JavaScript 转换为可交互的页面。整体上,渲染器进程渲染页面的流程基本如下:</p><ul><li>解析(Parser):解析 HTML/CSS/JavaScript 代码</li><li>布局(Layout):定位坐标和大小、是否换行、各种<code>position</code>/<code>overflow</code>/<code>z-index</code>属性等计算</li><li>绘制(Paint):判断元素渲染层级顺序</li><li>光栅化(Raster):将计算后的信息转换为屏幕上的像素</li></ul><p>大致流程如下图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/flow.png" alt="浏览器构造渲染树流程"></p><p>我们来分别看下。</p><h4 id="解析"><a href="#解析" class="headerlink" title="解析"></a>解析</h4><p>渲染器进程的主线程会解析以下内容:</p><ul><li>解析 HTML 内容,产生一个 DOM 节点树</li><li>解析 CSS,产生 CSS 规则树</li><li>解析 Javascript 脚本。由于 Javascript 脚本可以通过 DOM API 和 CSSOM API 来操作 DOM 节点树和 CSS 规则树,因此该过程中会等待 JavaScript 运行完成才继续解析 HTML</li></ul><p>解析完成后,我们得到了 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。</p><h4 id="布局"><a href="#布局" class="headerlink" title="布局"></a>布局</h4><p>通过解析之后,渲染器进程知道每个节点的结构和样式,但如果需要渲染页面,浏览器还需要进行布局,布局过程其实便是我们常说的渲染树的创建过程。</p><p>在这个过程中,像<code>header</code>或<code>display:none</code>的元素,它们会存在 DOM 节点树中,但不会被添加到渲染树里。</p><p>布局完成后,将会进入绘制环节。</p><h4 id="绘制"><a href="#绘制" class="headerlink" title="绘制"></a>绘制</h4><p>在绘制步骤中,渲染器主线程会遍历渲染树来创建绘制记录。</p><p>需要注意的是,如果渲染树发生了改变,则渲染器会触发重绘(Repaint)和重排(Reflow):</p><ul><li>重绘:屏幕的一部分要重画,比如某个 CSS 的背景色变了,但是元素的几何尺寸没有变</li><li>重排:元素的几何尺寸变了(渲染树的一部分或全部发生了变化),需要重新验证并计算渲染树</li></ul><p>为了不对每个小的变化都进行完整的布局计算,渲染器会将更改的元素和它的子元素进行脏位标记,表示该元素需要重新布局。其中,全局样式更改会触发全局布局,部分样式或元素更改会触发增量布局,增量布局是异步完成的,全局布局则会同步触发。</p><p>重排需要涉及变更的所有的结点几何尺寸和位置,成本比重绘的成本高得多的多。所以我们要注意以避免频繁地进行增加、删除、修改 DOM 结点、移动 DOM 的位置、Resize 窗口、滚动等操作,因为可能会导致性能降低。</p><h4 id="光栅化"><a href="#光栅化" class="headerlink" title="光栅化"></a>光栅化</h4><p>通过解析、布局和绘制过程,浏览器获得了文档的结构、每个元素的样式、绘制顺序等信息。将这些信息转换为屏幕上的像素,这个过程被称为光栅化。</p><p>光栅化可以被 GPU 加速,光栅化后的位图会被存储在 GPU 内存中。根据前面介绍的渲染流程,当页面布局变更了会触发重排和重绘,还需要重新进行光栅化。此时如果页面中有动画,则主线程中过多的计算任务很可能会影响动画的性能。</p><p>因此,现代的浏览器通常使用合成的方式,将页面的各个部分分成若干层,分别对其进行栅格化(将它们分割成了不同的瓦片),并通过合成器线程进行页面的合成:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/how-browser-works-2.jpg" alt></p><p>合成过程如下:</p><ol><li>当主线程创建了合成层并确定了绘制顺序,便将这些信息提交给合成线程。</li><li>合成器线程将每个图层栅格化,然后将每个图块发送给光栅线程。</li><li>光栅线程栅格化每个瓦片,并将它们存储在 GPU 内存中。</li><li>合成器线程通过 IPC 提交给浏览器进程,这些合成器帧被发送到 GPU 进程处理,并显示在屏幕上。</li></ol><p>合成的真正目的是,在移动合成层的时候不用重新光栅化。因为有了合成器线程,页面才可以独立于主线程进行流畅的滚动。</p><p>到这里,页面才真正渲染到屏幕上。</p><p>我们在绘制页面的时候,也可能会遇到很多奇怪的渲染问题,比如使用了<code>transform:scale</code>可能会导致某些浏览器中渲染模糊,究其原因则是由于光栅化过程导致的。像前面所说,前端开发需要频繁跟浏览器打交道,所谓知己知彼百战不殆,我们应该对其运行过程有更好的了解。</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><a href="https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/" target="_blank" rel="noopener">《How Browsers Work: Behind the scenes of modern web browsers》</a></li><li><a href="https://developers.google.com/web/updates/2018/09/inside-browser-part1" target="_blank" rel="noopener">《Inside look at modern web browser》</a>(分为四篇,左侧导航栏可以找到)</li></ul><p>这篇文章也是参考了这两篇文章以及一些论文,以我自己的理解来进行总结输出,推荐大家也要阅读原文哦。</p>]]></content>
<summary type="html">
<p>作为前端开发,我们的日常工作中除了写代码之外,几乎大多数的时间都在跟浏览器打交道。当然,现在我们甚至写代码都可以直接在浏览器里完成,一个浏览器走天下。</p>
<p>因此,我们应该对浏览器的了解要更加深入,除了了解怎么使用和调试浏览器,我们还要掌握它是怎样将我们编写的代码渲染到页面中的。</p>
</summary>
<category term="js什锦" scheme="https://godbasin.github.io/categories/js%E4%BB%80%E9%94%A6/"/>
<category term="分享" scheme="https://godbasin.github.io/tags/%E5%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>前端这几年--11.关于一年一换的魔咒</title>
<link href="https://godbasin.github.io/2021/10/10/about-front-end-11/"/>
<id>https://godbasin.github.io/2021/10/10/about-front-end-11/</id>
<published>2021-10-10T03:00:13.000Z</published>
<updated>2021-10-10T03:09:42.601Z</updated>
<content type="html"><![CDATA[<p>工作也有些年了,虽然常常换工作,但实际上我也很希望有一份热爱又持久的工作。</p><a id="more"></a><h2 id="换工作的魔咒?"><a href="#换工作的魔咒?" class="headerlink" title="换工作的魔咒?"></a>换工作的魔咒?</h2><p>总觉得自己身上有一个职业魔咒:每年都得换一次工作/工作环境。</p><p>如果算上部门内的主动调整,该魔咒的的确确从未被打破,从 2014 年开始一直到现在。</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>我也是凭着爱好成为开发,让我疲惫的往往不是写代码本身,而是工作中遇到难以磨合的问题。常常产生矛盾的点在于,每个人的取舍并不一致。举个例子,我认为技术最终是服务于产品和用户的,产品的体验、用户的感受很重要。而对于很多人来说,关注技术和 KPI 会对职业发展更重要。这是一个常见的矛盾点,虽然这样的矛盾并不存在高低之分、也不存在对与错,但它会影响到我们工作中对各个事情的优先级划分。</p><p>最常见的,莫过于我们常常需要主动 push 其他人,来协作完成我们需要的一些事情。因为在对方眼里,很可能有对他来说更重要的事情要做。</p><p>这本来是一件很普通、很小的事情,但夹杂着个人情绪、特殊场景之后,就容易被放大。我们都知道相互理解和尊重是沟通的基础,但工作中能做到的人并不多,所以我们常常会跟别人产生各种冲突和矛盾,这会导致我们慢慢对工作失去了耐心和热情。</p><h3 id="为何无法专注做一件事?"><a href="#为何无法专注做一件事?" class="headerlink" title="为何无法专注做一件事?"></a>为何无法专注做一件事?</h3><p>全神贯注写代码的时候很开心,但每天能专注写代码的时间却很少,因为一份工作对我们的要求远不止如此。我们要有 owner 意识,要主动发现问题和解决问题,主动思考如何优化。</p><p>随着职级的上升,对开发要求中,代码实现往往占比越来越小。推动方案落地,常常需要多方配合,需要不少的沟通成本;推动项目按照预期运行,又涉及各种风险把控、多方的进度管控。开发需要一专多长,方案调研、项目管理、风险管控、沟通能力、推动能力,一个本只需认真思考更优实现的人,被要求将精力分散到各个地方。</p><p>对于研发群体来说,适合全方面发展的人本就少之又少,因此大多数的人,对涉及沟通的事情觉得疲惫和无趣。而多线程处理事情,对于很多人来说都难以做好,常常会顾此失彼,因此对于大多数研发来说,更适合给予某个领域让其负责。</p><p>理想情况下,大多数的研发只需要负责好自己手上的事情,少部分热爱又擅长沟通的人将其管理和串联起来,便是较和谐高效的协作方式。</p><p>至于如今大多数工作都对我们有了更多的要求,主要原因大概是:</p><ol><li>有挑战、需要完全专注和具备技术不可替代性,这样的工作内容很少。</li><li>僧多粥少,工作内容的简单性,直接提升了开发人员的可替代性。</li><li>为了提升竞争力,因此个人、团队都对普遍群体有了更多的要求。</li></ol><p>因此,专注地做自己热爱的事情,对于大部分的人来说,这样的工作选择少之又少。相比之下,选择提升个人竞争力、获得更好的待遇,才是大多数人的选择。</p><h3 id="怎样的团队能做出好的产品?"><a href="#怎样的团队能做出好的产品?" class="headerlink" title="怎样的团队能做出好的产品?"></a>怎样的团队能做出好的产品?</h3><p>我最近常常在思考,导致团队目标割裂、难以做出团队成员自身也认可的产品的原因,到底在哪里?</p><p>从团队规模来看,小团队的确更容易比大团队达到目标的一致性,也更容易各司其职地专注自身的领域,齐心协力产品获得更好的体验。由于团队的一致性对每个团队成员都有要求,因此小团队可通过严格把控成员质量来达成,比如选择目标一致的成员。而在大团队里,不可避免地产生利益分歧,从而导致人人自危。</p><p>而在如今大多数的大公司体制下,产品的成败与个人的关系有所割裂。产品做得好,并不意味着团队里的每个人都有理想的收益;产品做得差,也并不是团队中的每个人都需要承担结果。</p><p>很多公司都有绩效考核与职级晋升两种激励体系,这两种激励是与团队成员有最直接利益关系。因为开发的工作往往无法量化,都是由直接或间接上级给出结果,因此,KPI 工程、PPT 工程、汇报氛围也一并形成。相比于思考把产品做好,替领导解决问题、提升自己在团队的影响力、建立自身的技术壁垒,更容易获得直接的收益,而后者并不一定需要把产品做好。</p><p>因此,在这样的环境下,大部分团队成员并不在意产品是否能获得成功,也很少认为自己会与该产品生死与共。底层抢着去做更容易晋升、更容易拿到好的考核的工作内容,上层也在思考怎样扩大团队规模、也给团队提供更多的机会,而产品是否做得好,或许只有该产品的初创者、拥护者会在乎,而他们往往又难以在这样的氛围内坚持下来。</p><p>所以,经历了好些快速扩招的团队之后,我对选择迅速扩招的团队有着极高的警惕。但事实上只要在这样的体制内,只要产品的发展看起来比较顺利,或是还有机会,团队扩招显然无法避免。</p><p>从这样的角度来看,产品发展得顺利,也不知是祸是福。</p><h3 id="能动性和惰性的平衡?"><a href="#能动性和惰性的平衡?" class="headerlink" title="能动性和惰性的平衡?"></a>能动性和惰性的平衡?</h3><p>团队对成员是有所要求的,这样的要求有时候会推动其成长,有时候却也会抑制其能动性。</p><p>我们常常说,具体情况具体分析。对于不同性格、脾气、追求的人,找到其闪光点并提供让其发挥出能力的环境,这对管理者有极高的要求。而大多数管理者都难以做到,因此会选择更加简单的管理方式,抹平团队差异性,通过培训、考试以及粗暴的横向对比、优胜劣汰,统一团队倾向和能力,批量产出高可替代性的团队成员。</p><p>大多数的产品开发,很多时候都是用常规的方式和套路便可,并不需要能力突出的团队成员。而对于追求极致和需要突破的产品来说,则需要个性更强的成员来推动产品发展。</p><p>如果想成为管理者,那么很大程度上要接受自己并不能完美地面面俱到,更多时候还是需要学习使用简单粗暴的管理方式,来维护和管理团队。</p><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><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>Angular框架解读--Ivy编译器的视图数据和依赖解析</title>
<link href="https://godbasin.github.io/2021/09/19/angular-design-ivy-1-view-data-and-node-injector/"/>
<id>https://godbasin.github.io/2021/09/19/angular-design-ivy-1-view-data-and-node-injector/</id>
<published>2021-09-19T11:10:12.000Z</published>
<updated>2021-09-19T11:12:06.441Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要介绍在 Angular 的 Ivy 编译器中,是如何管理和查找视图数据的。</p><a id="more"></a><h2 id="Ivy-模型"><a href="#Ivy-模型" class="headerlink" title="Ivy 模型"></a>Ivy 模型</h2><p>在<a href="https://godbasin.github.io/2021/08/15/angular-design-ivy-0-design/">上一节《Ivy 编译器整体设计》</a>中,我们从整体上介绍了 Ivy 编译器主要做的一些事情,包括模板编译、TypeScript 解析器等。我们可以看到 Ivy 编译器实现了更优的 Tree-shaking 支持、组件的延迟加载、支持增量编译等,而达到这些效果的一个核心设计点便在于视图的解析和数据管理。</p><h3 id="视图数据-LView-TView"><a href="#视图数据-LView-TView" class="headerlink" title="视图数据 LView/TView"></a>视图数据 LView/TView</h3><p>在 Angular Ivy 中,使用了<code>LView</code>和<code>TView.data</code>来管理和跟踪渲染模板所需要的内部数据。</p><p>其中,<code>LView</code>存储了从模板调用指令时,处理指令所需的所有信息。每个嵌入式视图和组件视图都有自己的<code>LView</code>,我们来看看<code>LView</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> LView <span class="keyword">extends</span> Array<any> {</span><br><span class="line"> <span class="comment">// 插入该 LView 的节点</span></span><br><span class="line"> [HOST]: RElement | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 此视图的静态数据</span></span><br><span class="line"> readonly [TVIEW]: TView;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 父视图</span></span><br><span class="line"> [PARENT]: LView | LContainer | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 下一个同级视图或容器</span></span><br><span class="line"> [NEXT]: LView | LContainer | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 对此视图有效的查询-视图中的节点将报告给这些查询</span></span><br><span class="line"> [QUERIES]: LQueries | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 存储当前 LView 插入位置的 TNode</span></span><br><span class="line"> [T_HOST]: TNode | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 当视图被破坏时,需要释放侦听器,并且必须取消订阅输出</span></span><br><span class="line"> [CLEANUP]: <span class="built_in">any</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"> [CONTEXT]: {} | RootContext | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 在咨询了元素注入器之后,将使用可选的模块注入器作为回退</span></span><br><span class="line"> readonly [INJECTOR]: Injector | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 用于创建渲染器的工厂</span></span><br><span class="line"> [RENDERER_FACTORY]: RendererFactory3;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 要用于此视图的渲染器</span></span><br><span class="line"> [RENDERER]: Renderer3;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 引用层次结构中此 LView 下的第一个 LView 或 LContainer</span></span><br><span class="line"> [CHILD_HEAD]: LView | LContainer | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 层次结构中此 LView 下的最后一个 LView 或 LContainer</span></span><br><span class="line"> [CHILD_TAIL]: LView | LContainer | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 查看声明此视图的模板的位置</span></span><br><span class="line"> [DECLARATION_VIEW]: LView | <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 指向声明组件视图,用于跟踪已移植的 LView</span></span><br><span class="line"> [DECLARATION_COMPONENT_VIEW]: LView;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 嵌入视图的声明点(基于 <ng-template> 的内容实例化的声明点),其他类型的视图为 null</span></span><br><span class="line"> [DECLARATION_LCONTAINER]: LContainer | <span class="literal">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们能看到,<code>LView</code>中存储了足够多的信息,这样的设计使单个数组可以以紧凑的形式包含模板渲染所需的所有必要数据。</p><p>其中,<code>[TVIEW]</code>为该视图的静态数据,存储了所有可在模板实例之间共享的信息(比如<code>template</code>、<code>components</code>、<code>data</code>以及各种钩子),以便可以轻松地在 DI 中遍历节点树并获取与节点(存储指令<code>defs</code>的节点)关联的<code>TView.data</code>数组。这些信息存储在<code>ComponentDef.tView</code>中。</p><p>显然,<code>LView</code>还存储了除此之外的所有渲染模板需要的信息,比如:</p><ul><li><code>[PARENT]</code>用于存储父视图。在处理特定视图时,Ivy 将<code>viewData</code>设置为该<code>LView</code>;完成该视图的处理后,将<code>viewData</code>设置回原始<code>viewData</code>之前的状态(父<code>LView</code>)</li><li><code>[NEXT]</code>用来链接组件视图和跨容器的视图</li><li><code>[T_HOST]</code>存储当前<code>LView</code>插入位置的<code>TNode</code>,因为“子级”除了插入到“父级”中,还可以插入到任何地方,因此不能将插入信息存储在<code>TView</code>中</li><li><code>[DECLARATION_VIEW]</code>用于存储“声明视图”(声明模板的视图),因为动态创建的视图的模板可以在与插入的视图不同的视图中声明,因此,上下文应继承自声明视图树,而不是插入视图树</li><li><code>[CHILD_HEAD]</code>存储引用层次结构中此<code>LView</code>下的第一个<code>LView</code>或<code>LContainer</code>,以便视图可以遍历其嵌套视图以除去侦听器并调用<code>onDestroy</code>回调</li><li><code>[CHILD_TAIL]</code>存储层次结构中此<code>LView</code>下的最后一个<code>LView</code>或<code>LContainer</code>,尾部使 Ivy 可以快速向视图列表的末尾添加新状态,而不必从第一个孩子开始传播</li></ul><p><code>LView</code>的设计,可以为每个视图保留单独的状态以方便视图的插入/删除,因此我们不必根据存在的视图来编辑数据数组。</p><h3 id="LView-TView-data-数据视图"><a href="#LView-TView-data-数据视图" class="headerlink" title="LView/TView.data 数据视图"></a>LView/TView.data 数据视图</h3><p>在 Ivy 中,<code>LView</code>和<code>TView.data</code>都是数组,它们的索引指向相同的项目。它们的数据视图布局如下:</p><table><thead><tr><th>Section</th><th><code>LView</code></th><th><code>TView.data</code></th></tr></thead><tbody><tr><td><code>HEADER</code></td><td>上下文数据</td><td>大多数为<code>null</code></td></tr><tr><td><code>DECLS</code></td><td>DOM、pipe 和本地引用实例</td><td></td></tr><tr><td><code>VARS</code></td><td>绑定值</td><td>属性名称</td></tr><tr><td><code>EXPANDO</code></td><td>host bindings; directive instances; providers; dynamic nodes</td><td>host prop names; directive tokens; provider tokens; <code>null</code></td></tr></tbody></table><p>其中:</p><ul><li><code>HEADER</code>是一个固定的数组大小,其中包含有关模板的上下文信息。主要是诸如父级<code>LView`</code>Sanitizer <code>、</code>TView`之类的信息,以及模板渲染所需的更多信息</li><li><code>DECKS</code>包含 DOM 元素、管道实例和本地引用,<code>DECKS</code>节的大小在组件定义的属性<code>decl</code>中声明</li><li><code>VARS</code>包含有关如何处理绑定的信息,<code>VARS</code>部分的大小在组件定义的属性<code>var</code>中声明</li><li><code>EXPANDO</code>包含有关在编译时未知大小的数据的信息。比如<code>Component/Directives</code>,因为 Ivy 在编译时不知道会匹配哪些指令</li></ul><p>至于具体的例子这里便不展开介绍了,你可以从 <a href="https://github.com/angular/angular/blob/master/packages/core/src/render3/VIEW_DATA.md" target="_blank" rel="noopener">DOCS: View Data Explanation</a> 文档中找到。</p><h2 id="Ivy-中-的-DI"><a href="#Ivy-中-的-DI" class="headerlink" title="Ivy 中 的 DI"></a>Ivy 中 的 DI</h2><p>在 Angular DI 中,注入器获取对应的示例依赖于 token 令牌。Ivy 将所有令牌存储在<code>TView.data</code>中,将实例存储在<code>LView</code>中,因此我们可以检索查看该视图的所有注入器。</p><p>而 DI 查找依赖的过程,离不开<code>NodeInjector</code>。</p><h3 id="NodeInjector"><a href="#NodeInjector" class="headerlink" title="NodeInjector"></a>NodeInjector</h3><p><a href>上一节中</a>,我们介绍了 Ivy 编译器中使用增量编译来优化构建速度,增量编译意味着一个库只会根据变更的部分进行重新编译。要做到增量编译,Ivy 编译器不得依赖未直接传递给它的任何输入(可理解为“纯函数”)。使用<code>Lview</code>来存储每个视图的状态和数据,则可以通过 DI 注入依赖的视图数据。</p><p>在<a href="https://godbasin.github.io/2021/07/11/angular-design-di-2-hierarchical-di/">《Angular 框架解读–多级依赖注入设计》</a>一文中,我们介绍了 Angular 中的两种注入器:模块注入器<code>ModuleInjector</code>和元素注入器<code>ElementInjector</code>。Angular 通过依次遍历元素注入器树和模块注入器树来查找提供令牌的注入器。</p><p>实际上,在 Ivy 中使用<code>NodeInjector</code>替换了 View Engine 中的元素注入器:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> NodeInjector <span class="keyword">implements</span> Injector {</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> _tNode:</span></span><br><span class="line"><span class="params"> | TElementNode</span></span><br><span class="line"><span class="params"> | TContainerNode</span></span><br><span class="line"><span class="params"> | TElementContainerNode</span></span><br><span class="line"><span class="params"> | <span class="literal">null</span>,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _lView: LView</span></span><br><span class="line"><span class="params"> </span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span>(token: <span class="built_in">any</span>, notFoundValue?: <span class="built_in">any</span>): <span class="built_in">any</span> {</span><br><span class="line"> <span class="keyword">return</span> getOrCreateInjectable(</span><br><span class="line"> <span class="keyword">this</span>._tNode,</span><br><span class="line"> <span class="keyword">this</span>._lView,</span><br><span class="line"> token,</span><br><span class="line"> <span class="literal">undefined</span>,</span><br><span class="line"> notFoundValue</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中,<code>getOrCreateInjectable</code>方法从<code>NodeInjectors</code>到<code>ModuleInjector</code>进行遍历,并返回(或创建)与给定令牌关联的值。</p><h3 id="DI-查找依赖的过程"><a href="#DI-查找依赖的过程" class="headerlink" title="DI 查找依赖的过程"></a>DI 查找依赖的过程</h3><p>我们知道 Angular 会构建一棵视图树,该视图树总是以只含一个根元素的伪根视图开始(参考<a href="https://godbasin.github.io/2021/04/05/angular-design-dom-define/">《Angular 框架解读–视图抽象定义》</a>)。</p><p>Ivy 使用<code>LView</code>和<code>TView.data</code>数组来存储视图数据,其中便包括了节点的注入信息。这意味着,<strong><code>NodeInjector</code>需要从<code>LView</code>和<code>TView.data</code>数组中得到具体的视图数据信息</strong>。</p><p>我们可以从<code>getOrCreateInjectable</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></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">getOrCreateInjectable</span><<span class="title">T</span>>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> tNode: TDirectiveHostNode | <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> lView: LView,</span></span></span><br><span class="line"><span class="function"><span class="params"> token: Type<T> | AbstractType<T> | InjectionToken<T>,</span></span></span><br><span class="line"><span class="function"><span class="params"> flags: InjectFlags = InjectFlags.Default,</span></span></span><br><span class="line"><span class="function"><span class="params"> notFoundValue?: <span class="built_in">any</span></span></span></span><br><span class="line"><span class="function"><span class="params"></span>): <span class="title">T</span> | <span class="title">null</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (tNode !== <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">const</span> bloomHash = bloomHashBitOrFactory(token);</span><br><span class="line"> <span class="comment">// 如果此处存储的 ID 是一个函数,则这是一个特殊的对象,例如 ElementRef 或 TemplateRef</span></span><br><span class="line"> <span class="comment">// 因此只需调用 factory 函数即可创建它</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> bloomHash === <span class="string">"function"</span>) {</span><br><span class="line"> <span class="keyword">if</span> (!enterDI(lView, tNode, flags)) {</span><br><span class="line"> <span class="comment">// 无法进入 DI,则尝试使用模块注入器</span></span><br><span class="line"> <span class="comment">// 如果使用 @Host 标志注入令牌,则在 Ivy 中不会在模块注入器中搜索该令牌</span></span><br><span class="line"> <span class="keyword">return</span> flags & InjectFlags.Host</span><br><span class="line"> ? notFoundValueOrThrow<T>(notFoundValue, token, flags)</span><br><span class="line"> : lookupTokenUsingModuleInjector<T>(</span><br><span class="line"> lView,</span><br><span class="line"> token,</span><br><span class="line"> flags,</span><br><span class="line"> notFoundValue</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> value = bloomHash(flags);</span><br><span class="line"> <span class="keyword">if</span> (value == <span class="literal">null</span> && !(flags & InjectFlags.Optional)) {</span><br><span class="line"> throwProviderNotFoundError(token);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> value;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> leaveDI();</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (<span class="keyword">typeof</span> bloomHash === <span class="string">"number"</span>) {</span><br><span class="line"> <span class="comment">// 对遍历元素注入器树时找到的上一个注入器 TView 的引用</span></span><br><span class="line"> <span class="comment">// 这用于了解是否可以在当前注射器上访问 viewProviders</span></span><br><span class="line"> <span class="keyword">let</span> previousTView: TView | <span class="literal">null</span> = <span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">let</span> injectorIndex = getInjectorIndex(tNode, lView);</span><br><span class="line"> <span class="keyword">let</span> parentLocation: RelativeInjectorLocation = NO_PARENT_INJECTOR;</span><br><span class="line"> <span class="keyword">let</span> hostTElementNode: TNode | <span class="literal">null</span> =</span><br><span class="line"> flags & InjectFlags.Host</span><br><span class="line"> ? lView[DECLARATION_COMPONENT_VIEW][T_HOST]</span><br><span class="line"> : <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">if</span> (injectorIndex === <span class="number">-1</span> || flags & InjectFlags.SkipSelf) {</span><br><span class="line"> parentLocation =</span><br><span class="line"> injectorIndex === <span class="number">-1</span></span><br><span class="line"> ? getParentInjectorLocation(tNode, lView)</span><br><span class="line"> : lView[injectorIndex + NodeInjectorOffset.PARENT];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (</span><br><span class="line"> parentLocation === NO_PARENT_INJECTOR ||</span><br><span class="line"> !shouldSearchParent(flags, <span class="literal">false</span>)</span><br><span class="line"> ) {</span><br><span class="line"> injectorIndex = <span class="number">-1</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> previousTView = lView[TVIEW];</span><br><span class="line"> injectorIndex = getParentInjectorIndex(parentLocation);</span><br><span class="line"> lView = getParentInjectorView(parentLocation, lView);</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">while</span> (injectorIndex !== <span class="number">-1</span>) {</span><br><span class="line"> ngDevMode && assertNodeInjector(lView, injectorIndex);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 检查当前的注入器。如果匹配,请查看它是否包含令牌</span></span><br><span class="line"> <span class="keyword">const</span> tView = lView[TVIEW];</span><br><span class="line"> ngDevMode &&</span><br><span class="line"> assertTNodeForLView(</span><br><span class="line"> tView.data[injectorIndex + NodeInjectorOffset.TNODE] <span class="keyword">as</span> TNode,</span><br><span class="line"> lView</span><br><span class="line"> );</span><br><span class="line"> <span class="keyword">if</span> (bloomHasToken(bloomHash, injectorIndex, tView.data)) {</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">const</span> instance: T | <span class="literal">null</span> = searchTokensOnInjector<T>(</span><br><span class="line"> injectorIndex,</span><br><span class="line"> lView,</span><br><span class="line"> token,</span><br><span class="line"> previousTView,</span><br><span class="line"> flags,</span><br><span class="line"> hostTElementNode</span><br><span class="line"> );</span><br><span class="line"> <span class="keyword">if</span> (instance !== NOT_FOUND) {</span><br><span class="line"> <span class="keyword">return</span> instance;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> parentLocation = lView[injectorIndex + NodeInjectorOffset.PARENT];</span><br><span class="line"> <span class="keyword">if</span> (</span><br><span class="line"> parentLocation !== NO_PARENT_INJECTOR &&</span><br><span class="line"> shouldSearchParent(</span><br><span class="line"> flags,</span><br><span class="line"> lView[TVIEW].data[injectorIndex + NodeInjectorOffset.TNODE] ===</span><br><span class="line"> hostTElementNode</span><br><span class="line"> ) &&</span><br><span class="line"> bloomHasToken(bloomHash, injectorIndex, lView)</span><br><span class="line"> ) {</span><br><span class="line"> <span class="comment">// 在此节点上的任何位置都找不到 def,因此它是误报。遍历树并继续搜索</span></span><br><span class="line"> previousTView = tView;</span><br><span class="line"> injectorIndex = getParentInjectorIndex(parentLocation);</span><br><span class="line"> lView = getParentInjectorView(parentLocation, lView);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 如果我们不应该搜索父对象,或者如果祖先的 bloom 过滤器值没有对应于该指令的位</span></span><br><span class="line"> <span class="comment">// 我们可以放弃遍历以查找特定的注入器</span></span><br><span class="line"> injectorIndex = <span class="number">-1</span>;</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 class="keyword">return</span> lookupTokenUsingModuleInjector<T>(lView, token, flags, notFoundValue);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从上述代码中,如果我们调用<code>injector.get(SomeClass)</code>方法,会产生以下步骤:</p><ol><li>Angular 在<code>SomeClass.__NG_ELEMENT_ID__</code>静态属性中查找哈希。</li><li>如果该哈希是工厂函数,则还有另一种特殊情况,即应通过调用该函数来初始化对象。</li><li>如果该哈希等于-1,则是一种特殊情况,我们将获得<code>NodeInjector</code>实例。</li><li>如果该哈希是一个数字,那么我们会从<code>TNode</code>获取<code>injectorIndex</code>。</li><li>查看模板布隆过滤器(<code>TView.data [injectorIndex]</code>),如果为真,那么我们将搜索<code>SomeClass</code>令牌(通过<code>tNode.providerIndexes</code>可以找到所需的令牌)。</li><li>如果模板布隆过滤器返回错误,那么会查看一下累积布隆过滤器。如果它为真,则继续进行遍历,否则将切换到<code>ModuleInjector</code>。</li></ol><p>该过程可以用以下流程图表示:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-ivy-2-2.png" alt></p><p>这便是在 Ivy 中,使用<code>NodeInjector</code>来解析依赖关系的过程。可以看到,该过程中还使用了两个布隆过滤器:累积布隆过滤器(cumulativeBloom)和模板布隆过滤器(templateBloom)。</p><h3 id="布隆过滤器"><a href="#布隆过滤器" class="headerlink" title="布隆过滤器"></a>布隆过滤器</h3><p>布隆过滤器常用于加快数据检索的过程,属于哈希函数的一种,你可以阅读 <a href="https://hackernoon.com/probabilistic-data-structures-bloom-filter-5374112a7832" target="_blank" rel="noopener">Probabilistic Data structures: Bloom filter</a> 一文来了解它。</p><p>在 Ivy 中,一个视图可以具有与为该视图上的节点创建的注入器数量一样多的布隆过滤器。下图为可视化结果:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-ivy-2-1.png" alt></p><p>可以看到,布隆过滤器存储在前面提到的<code>LView/TView.data</code>布局中的<code>EXPANDO</code>部分:</p><ul><li><code>LView</code>和<code>TView.data</code>数组可以包含许多布隆过滤器,长度为 8 个时隙([n,n + 7]索引),它们的数量与为其创建喷射器的节点数量成正比</li><li>每个布隆过滤器在“压缩的”<code>parentLocation</code>插槽(n + 8 索引)中都有一个指向父布隆过滤器的指针</li></ul><p>我们结合<code>NodeInjector</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span>({</span><br><span class="line"> selector: <span class="string">"my-app"</span>,</span><br><span class="line"> template: <span class="string">`</span></span><br><span class="line"><span class="string"> <div dirA></span></span><br><span class="line"><span class="string"> <div dirB>Hello Ivy</div></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> `</span>,</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> AppComponent {}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Directive</span>({ selector: <span class="string">"[dirA]"</span> })</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> DirA {}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Directive</span>({ selector: <span class="string">"[dirB]"</span> })</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> DirB {</span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params"><span class="keyword">private</span> rootComp: AppComponent</span>) {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 Ivy 中,上述代码会生成这样的可视化视图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-ivy-2-3.png" alt></p><p>Ivy 在<code>TNode</code>上创建了<code>InjectorIndex</code>属性,以便知道专用于此节点布隆过滤器的位置。除此之外,Ivy 还在<code>LView</code>数组中存储了<code>parentLocation</code>指针,以便我们可以遍历所有父注入器。</p><p>而我们也看到,<code>NodeInjector</code>是具有对<code>TNode</code>和<code>LView</code>对象的引用的对象。因此,每个<code>NodeInjector</code>分别保存在<code>LView</code>的 9 个连续插槽和<code>TView.data</code>的 9 个连续插槽中,如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-ivy-2-4.png" alt></p><p>那么,上面简单的代码示例中,DI 查找依赖的过程如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-ivy-2-5.png" alt></p><blockquote><p>以上例子来自于 <a href="https://indepth.dev/posts/1268/angular-di-getting-to-know-the-ivy-nodeinjector" target="_blank" rel="noopener">Angular DI: Getting to know the Ivy NodeInjector</a></p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>今天给大家介绍了 Ivy 编译器中的数据视图<code>LView/TView</code>,而依赖解析过程中需要从中取出对应的数据,该过程使用到<code>NodeInjector</code>。<code>NodeInjector</code>用于创建注入器,为了加快 DI 搜索依赖的过程,Ivy 还设计了累加布隆过滤器和模板布隆过滤器。</p><p>这些内容,是理解 Angular 中依赖注入过程中不可或缺的。而在查阅这部分文章和代码之前,我甚至无法想象在 Angular 中依赖注入过程如此复杂。很多时候,我们都认为前端领域并不存在太多的算法和数据结构相关的内容,实际上可能只是我们并没有接触到而已。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://indepth.dev/posts/1268/angular-di-getting-to-know-the-ivy-nodeinjector" target="_blank" rel="noopener">Angular DI: Getting to know the Ivy NodeInjector</a></li><li><a href="https://github.com/angular/angular/blob/master/packages/core/src/render3/VIEW_DATA.md" target="_blank" rel="noopener">DOCS: View Data Explanation</a></li><li><a href="https://hackernoon.com/probabilistic-data-structures-bloom-filter-5374112a7832" target="_blank" rel="noopener">Probabilistic Data structures: Bloom filter</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要介绍在 Angular 的 Ivy 编译器中,是如何管理和查找视图数据的。</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框架解读--Ivy编译器整体设计</title>
<link href="https://godbasin.github.io/2021/08/15/angular-design-ivy-0-design/"/>
<id>https://godbasin.github.io/2021/08/15/angular-design-ivy-0-design/</id>
<published>2021-08-15T05:53:34.000Z</published>
<updated>2021-08-15T06:53:55.644Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,首先介绍该编译器的整体设计。</p><a id="more"></a><p>对于前端框架来说,模板编译器(渲染器)属于非常核心的能力了。在 Angular 8.0 中引入了一个新的模板编译器——Ivy 编译器,在这之前 Angular 一直使用 View Engine 来编译模板。</p><h2 id="Ivy-编译器能力"><a href="#Ivy-编译器能力" class="headerlink" title="Ivy 编译器能力"></a>Ivy 编译器能力</h2><p>编译器的用途,基本上是将开发者编写的代码,编译成可在浏览器中运行的代码。使用了编译器之后,前端框架就可以定义很多自身的语法,在编译过程可以给代码增加一些性能优化、安全检测等功能。对于 Angular 来说,编译器还需要支持将开发者代码编译成 AOT 和 JIT 两种。</p><p>Angular 重构编译器并将之命名为 Ivy 编译器,这对于 Angular 框架来说有着非常重要的意义,有点类似于 React 重构 Fiber。</p><h3 id="Ivy-新特性"><a href="#Ivy-新特性" class="headerlink" title="Ivy 新特性"></a>Ivy 新特性</h3><p>我们先来看看 Ivy 编译器的一些特性,包括但不限于以下的内容:</p><ul><li>🚀 缩短构建时间(增加增量编译)</li><li>🔥 达到更好的构建大小(生成的代码和 Tree-shaking 更兼容),有效地降低代码包大小</li><li>🔓 解锁新的潜在功能(元编程或更高级别的组件,支持组件的延迟加载,支持不基于 zone.js 的新变更检测系统,等等)</li></ul><p>前面章节中我们有介绍 Angular 的<a href="https://godbasin.github.io/2021/03/27/angular-design-metadata/">元编程</a>、组件和模块之间的关系、<a href="https://godbasin.github.io/2021/05/01/angular-design-zonejs/">zone.js 中的设计</a>和<a href="https://godbasin.github.io/2021/05/30/angular-design-zone-ngzone/">引入</a>等内容,其中不少能力和设计都无法与 Ivy 编译器的设计和引入脱离关系。比如,<a href="https://godbasin.github.io/2021/07/11/angular-design-di-2-hierarchical-di/">Angular 依赖设计</a>中由于延迟模块引入的 bug,前面我们说过 Angular 中的依赖注入通过将注入器分为元素注入器和模块注入器,而在 Ivy 编译器中,使用了支持到组件级别的延迟加载(Node ),最终解决了延迟模块重复加载的问题。</p><p>今天我们先来了解一下 Ivy 编译器的整体设计,后面会再分具体的章节来详细介绍内部的一些源码实现。</p><h2 id="Ivy-架构设计"><a href="#Ivy-架构设计" class="headerlink" title="Ivy 架构设计"></a>Ivy 架构设计</h2><p>在 Angular 中,开发者编写的代码大多数为 Typescript 代码,其中还包括了许多 Angular 提供的 API 和语法糖,因此 Angular 需要通过语法解析转换为 AST,并根据 AST 编译成最终可以跑在浏览器中的代码,这便是 Ivy 编译器需要实现的核心能力。</p><p>Ivy 编译器主要包括两部分:</p><ol><li><code>ngtsc</code>是一个 Typescript-to-Javascript 编译器,它将 Angular 装饰器化为静态属性。它是一个最小包装器,包裹在<code>tsc</code>之外,而<code>tsc</code>中则包含一系列的 Angular 变换。</li><li><code>ngcc</code>主要负责处理来自 NPM 的代码并生成等效的 Ivy 版本,就像使用<code>ngtsc</code>编译代码一样。</li></ol><h3 id="模板编译"><a href="#模板编译" class="headerlink" title="模板编译"></a>模板编译</h3><p>在 Ivy 编译器中使用<code>TemplateCompiler</code>来编译模板,该过程中会执行以下操作:</p><ol><li>标记模板。</li><li>将标记内容解析为 HTML AST。</li><li>将 HTML AST 转换为 Angular 模板 AST。</li><li>将 Angular 模板 AST 转换为模板函数。</li></ol><p>Angular Template AST 转换和注释的 HTML AST 版本时,会执行以下操作:</p><ol><li>将 Angular 模板语法快捷方式(例如<code>*ngFor</code>和<code>[name]</code>)转换为其规范版本(和<code>bind-name</code>)。</li><li>收集引用(<code>#</code>属性)和变量(<code>let-</code>属性)。</li><li>使用收集的变量和引用,解析并转换绑定表达式 AST 中的绑定表达式。</li></ol><p>除了以上操作之外,该过程还会生成详尽的选择器目标列表,包括任何组件、指令或管道的选择器的潜在目标。确定组件包含它所依赖的组件、指令和管道的列表,可在运行时知道将哪些组件和指令应用于元素以及绑定表达式引用了哪些管道。从而<code>TemplateCompiler</code>可以从字符串生成模板函数,而无需附加信息。</p><p>确定此列表的过程称为引用反转,因为它将从模块(包含依赖项)到组件的链接反转为从组件到其依赖项的链接。然后,程序只需要包含呈现的初始组件所依赖的类型以及这些依赖项所需的任何类型。除此之外,还解决了 tree-shaking 的问题。</p><h3 id="Typescript-解析器"><a href="#Typescript-解析器" class="headerlink" title="Typescript 解析器"></a>Typescript 解析器</h3><p>要实现 AST 的解析和转换,离不开解析器。对于 Typescript 代码来说,编译器的整体流程为:</p><figure class="highlight gherkin"><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="string">------------</span>|</span><br><span class="line"> |<span class="string">----------------------------------> </span>|<span class="string"> TypeScript </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string"> .d.ts </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string">------------</span>|</span><br><span class="line"> |</span><br><span class="line">|<span class="string">------------</span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string">------------</span>|</span><br><span class="line">|<span class="string"> TypeScript </span>|<span class="string"> -parse-> </span>|<span class="string"> AST </span>|<span class="string"> ->transform-> </span>|<span class="string"> AST </span>|<span class="string"> ->print-> </span>|<span class="string"> JavaScript </span>|</span><br><span class="line">|<span class="string"> source </span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string">-----</span>|<span class="string"> </span>|<span class="string"> source </span>|</span><br><span class="line">|<span class="string">------------</span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string"> </span>|<span class="string">------------</span>|</span><br><span class="line"> |<span class="string"> type-check </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string"> </span>|</span><br><span class="line"> |<span class="string"> v </span>|</span><br><span class="line"> |<span class="string"> </span>|<span class="string">--------</span>|<span class="string"> </span>|</span><br><span class="line"> |<span class="string">--> </span>|<span class="string"> errors </span>|<span class="string"> <---</span>|</span><br><span class="line"> |<span class="string">--------</span>|</span><br></pre></td></tr></table></figure><p>其中,解析步骤是传统的递归下降解析器,经过增强以支持增量解析,该解析器发出抽象语法树(AST)。转换步骤是一组 AST 到 AST 的转换,这些转换执行各种任务,例如删除类型声明,将模块和类声明降低到 ES5,将<code>async</code>方法转换为状态机等。</p><h3 id="编译器设计"><a href="#编译器设计" class="headerlink" title="编译器设计"></a>编译器设计</h3><p>前面我们提到 Ivy 支持增量编译,从而达到缩短构建时间的效果。增量编译的预期是当一个库已经被编译时,我们就不必每次都重新编译它,而是根据变更的部分进行重新编译。这看起来比较简单,实际上它对编译器提供了不小的挑战,因为组件的生成代码可能会使用另一个组件的内部细节。</p><p>从广义上讲,Ivy 模型是将 Angular 装饰器编译为类上的静态属性,包括:</p><ul><li>组件编译(<code>ViewCompiler</code>和样式编译器):编译<code>@Component</code> => <code>ɵɵdefineComponent</code></li><li>管道编译<code>PipeCompiler</code>:编译<code>@Pipe</code>=><code>ɵɵdefinePipe</code></li><li>指令编译<code>DirectiveCompiler</code>:编译<code>@Directive</code>=><code>ɵɵdefineDirective</code></li><li>可注入编译<code>InjectableCompiler</code>:编译<code>@Injectable</code>=><code>ɵɵdefineInjectable</code></li><li>模块编译<code>NgModuleCompiler</code>:编译<code>@NgModule</code>=><code>ɵɵdefineInjector</code>(<code>ɵɵdefineNgModule</code>仅在 JIT 中)</li></ul><p>这些操作必须在没有全局程序数据的情况下进行,并且在大多数情况下,仅在具有单个装饰器数据的情况下进行。</p><p>因此,Ivy 编译器不得依赖未直接传递给它的任何输入(例如,它不得扫描源或元数据中的其他数据)。该限制很重要,原因有两个:</p><ol><li>由于可以看到编译器的所有输入,因此它有助于强制执行 Ivy 局部性原则。</li><li>它可以防止在<code>--watch</code>模式下进行错误的构建,因为文件之间的依赖关系很容易跟踪。</li></ol><p>所以在 Ivy 中,每个将单个装饰器转换为静态字段的“编译器”都将充当“纯函数”。给定有关特定类型和装饰器的输入元数据,它将生成一个对象,该对象描述要添加到该类型的字段,以及该字段的初始化值(采用 AST 格式)。</p><p>举个例子,<code>@Component</code>编译器的输入包括以下内容:</p><ul><li>对组件类的引用</li><li>组件的模板和样式资源</li><li>组件的选择器</li><li>组件所属模块的选择器映射</li></ul><h3 id="Ivy-编译模型"><a href="#Ivy-编译模型" class="headerlink" title="Ivy 编译模型"></a>Ivy 编译模型</h3><p>在 Angular 中,实例化组件、创建 DOM 节点以及运行变更检测,以上的逻辑被实现为一个原子单位,被称为“Angular 解释器”。编译器仅生成有关其模板中定义的组件和元素的元数据。</p><p>在旧版 View Engine 中,编译过程为:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-ivy-1-1.png" alt></p><p><code><span>My name is </span></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">viewDef(<span class="number">0</span>,[</span><br><span class="line"> elementDef(<span class="number">0</span>,<span class="literal">null</span>,<span class="literal">null</span>,<span class="number">1</span>,<span class="string">'span'</span>,...),</span><br><span class="line"> textDef(<span class="literal">null</span>,[<span class="string">'My name is '</span>,...])</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>而在 Ivy 编译器中,编译后的代码大概长这个样子:</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">// create mode</span></span><br><span class="line"><span class="keyword">if</span> (rf & RenderFlags.Create) {</span><br><span class="line"> elementStart(<span class="number">0</span>, <span class="string">"span"</span>);</span><br><span class="line"> text(<span class="number">1</span>);</span><br><span class="line"> elementEnd();</span><br><span class="line">}</span><br><span class="line"><span class="comment">// update mode</span></span><br><span class="line"><span class="keyword">if</span> (rf & RenderFlags.Update) {</span><br><span class="line"> textBinding(<span class="number">1</span>, interpolation1(<span class="string">"My name is"</span>, ctx.name));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 Ivy 编译器中,编译过程为:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-ivy-1-2.png" alt></p><p>在 View Engine 中,组件定义(模板数据)独立于组件类而位于其自己的文件中。而在 Ivy 编译器中,组件定义将通过静态字段附加到组件类,编译期间不会创建单独的文件。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>今天大致介绍了 Angular 中 Ivy 编译器的整体设计,其实 Angular 本身就对 Ivy 编译器的整体架构提供了很详细的说明,本文很多内容也都参考来自这些内容,建议大家可以都去看看 <a href="https://github.com/angular/angular/blob/master/packages/compiler/design/architecture.md" target="_blank" rel="noopener">DESIGN DOC(Ivy): Compiler Architecture</a>。</p><p>Ivy 编译器作为 Angular 的核心能力,并不是一篇文章足以概括完毕。本文也并未介绍<code>ngtsc</code>的编译流程、资源加载等内容,也并未开始结合 Angular 中的源码一起研究其实现。这些我后续会尝试一点一点地挖掘,希望能从中学到架构文档以外更多的知识,我也会尝试将自己的学习过程记录下来,继续分享给对 Angular 感兴趣的你们~~</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://github.com/angular/angular/blob/master/packages/compiler/design/architecture.md" target="_blank" rel="noopener">DESIGN DOC(Ivy): Compiler Architecture</a></li><li><a href="https://indepth.dev/posts/1259/angular-compatability-compiler" target="_blank" rel="noopener">Under the hood of the Angular Compatibility Compiler (ngcc)</a></li><li><a href="https://blog.ninja-squad.com/2019/05/07/what-is-angular-ivy/" target="_blank" rel="noopener">What is Angular Ivy?</a></li><li><a href="https://medium.com/angular-in-depth/all-you-need-to-know-about-ivy-the-new-angular-engine-9cde471f42cf" target="_blank" rel="noopener">All you need to know about Ivy, The new Angular engine!</a></li><li><a href="https://indepth.dev/posts/1062/ivy-engine-in-angular-first-in-depth-look-at-compilation-runtime-and-change-detection" target="_blank" rel="noopener">Ivy engine in Angular: first in-depth look at compilation, runtime and change detection</a></li><li><a href="https://indepth.dev/posts/1053/everything-you-need-to-know-about-change-detection-in-angular" target="_blank" rel="noopener">Everything you need to know about change detection in Angular</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,首先介绍该编译器的整体设计。</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/07/25/angular-design-di-3-bootstrap/"/>
<id>https://godbasin.github.io/2021/07/25/angular-design-di-3-bootstrap/</id>
<published>2021-07-25T05:55:21.000Z</published>
<updated>2021-07-25T06:43:59.545Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的最大特点——依赖注入,介绍 Angular 依赖注入在体系在应用引导过程中的的设计和实现。</p><a id="more"></a><p><a href="https://godbasin.github.io/2021/07/11/angular-design-di-2-hierarchical-di/">多级依赖注入</a>中,介绍了模块注入器和元素注入器两种层次结构的注入器。那么,Angular 在引导过程中,又是如何初始化根模块和入口组件的呢?</p><h1 id="Angular-的引导过程"><a href="#Angular-的引导过程" class="headerlink" title="Angular 的引导过程"></a>Angular 的引导过程</h1><p>前面我们说到,Angular 应用在浏览器中引导时,会创建浏览器平台,并引导根模块:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">platformBrowserDynamic().bootstrapModule(AppModule);</span><br></pre></td></tr></table></figure><h2 id="引导根模块"><a href="#引导根模块" class="headerlink" title="引导根模块"></a>引导根模块</h2><h3 id="根模块-AppModule"><a href="#根模块-AppModule" class="headerlink" title="根模块 AppModule"></a>根模块 AppModule</h3><p>在 Angular 中,每个应用有至少一个 Angular 模块,根模块就是你用来引导此应用的模块,它通常命名为 AppModule。</p><p>当你使用 Angular CLI 命令 ng new 生成一个应用时,其默认的 AppModule 是这样的:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { BrowserModule } <span class="keyword">from</span> <span class="string">'@angular/platform-browser'</span>;</span><br><span class="line"><span class="keyword">import</span> { NgModule } <span class="keyword">from</span> <span class="string">'@angular/core'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> { AppComponent } <span class="keyword">from</span> <span class="string">'./app.component'</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@NgModule</span>({</span><br><span class="line"> declarations: [</span><br><span class="line"> AppComponent</span><br><span class="line"> ],</span><br><span class="line"> imports: [</span><br><span class="line"> BrowserModule</span><br><span class="line"> ],</span><br><span class="line"> providers: [],</span><br><span class="line"> bootstrap: [AppComponent]</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> AppModule { }</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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> PlatformRef {</span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):</span><br><span class="line"> <span class="built_in">Promise</span><NgModuleRef<M>> {</span><br><span class="line"> <span class="comment">// 由于实例化模块时,会需要创建一些提供者,所以这里需要在实例化模块之前创建 NgZone</span></span><br><span class="line"> <span class="comment">// 因此,这里创建了一个仅包含新 NgZone 的微型父注入器,并将其作为父传递给 NgModuleFactory</span></span><br><span class="line"> <span class="keyword">const</span> ngZoneOption = options ? options.ngZone : <span class="literal">undefined</span>;</span><br><span class="line"> <span class="keyword">const</span> ngZoneEventCoalescing = (options && options.ngZoneEventCoalescing) || <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">const</span> ngZoneRunCoalescing = (options && options.ngZoneRunCoalescing) || <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">const</span> ngZone = getNgZone(ngZoneOption, {ngZoneEventCoalescing, ngZoneRunCoalescing});</span><br><span class="line"> <span class="keyword">const</span> providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];</span><br><span class="line"> <span class="comment">// ApplicationRef 将在 Angular zone 之外创建</span></span><br><span class="line"> <span class="keyword">return</span> ngZone.run(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="comment">// 在 ngZone.run 中创建 ngZoneInjector,以便在 Angular zone 中创建所有实例化的服务</span></span><br><span class="line"> <span class="keyword">const</span> ngZoneInjector = Injector.create(</span><br><span class="line"> {providers: providers, parent: <span class="keyword">this</span>.injector, name: moduleFactory.moduleType.name});</span><br><span class="line"> <span class="keyword">const</span> moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);</span><br><span class="line"> <span class="keyword">const</span> exceptionHandler: ErrorHandler|<span class="literal">null</span> = moduleRef.injector.get(ErrorHandler, <span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">if</span> (!exceptionHandler) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'No ErrorHandler. Is platform module (BrowserModule) included?'</span>);</span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">return</span> _callAndReportToErrorHandler(exceptionHandler, ngZone!, <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);</span><br><span class="line"> initStatus.runInitializers();</span><br><span class="line"> <span class="keyword">return</span> initStatus.donePromise.then(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 引导模块</span></span><br><span class="line"> <span class="keyword">this</span>._moduleDoBootstrap(moduleRef);</span><br><span class="line"> <span class="keyword">return</span> moduleRef;</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"> bootstrapModule<M>(</span><br><span class="line"> moduleType: Type<M>,</span><br><span class="line"> compilerOptions: (CompilerOptions&BootstrapOptions)|</span><br><span class="line"> <span class="built_in">Array</span><CompilerOptions&BootstrapOptions> = []): <span class="built_in">Promise</span><NgModuleRef<M>> {</span><br><span class="line"> <span class="keyword">const</span> options = optionsReducer({}, compilerOptions);</span><br><span class="line"> <span class="comment">// 编译并创建 @NgModule 的实例</span></span><br><span class="line"> <span class="keyword">return</span> compileNgModuleFactory(<span class="keyword">this</span>.injector, options, moduleType)</span><br><span class="line"> .then(<span class="function"><span class="params">moduleFactory</span> =></span> <span class="keyword">this</span>.bootstrapModuleFactory(moduleFactory, options));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> _moduleDoBootstrap(moduleRef: InternalNgModuleRef<<span class="built_in">any</span>>): <span class="built_in">void</span> {</span><br><span class="line"> <span class="keyword">const</span> appRef = moduleRef.injector.get(ApplicationRef) <span class="keyword">as</span> ApplicationRef;</span><br><span class="line"> <span class="comment">// 引导应用程序</span></span><br><span class="line"> <span class="keyword">if</span> (moduleRef._bootstrapComponents.length > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 在应用程序的根级别引导新组件</span></span><br><span class="line"> moduleRef._bootstrapComponents.forEach(<span class="function"><span class="params">f</span> =></span> appRef.bootstrap(f));</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (moduleRef.instance.ngDoBootstrap) {</span><br><span class="line"> moduleRef.instance.ngDoBootstrap(appRef);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">this</span>._modules.push(moduleRef);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根模块引导时,除了编译并创建 AppModule 的实例,还会创建 NgZone,关于 NgZone 的请参考<a href></a>。在编译和创建 AppModule 的过程中,便会创建<code>ApplicationRef</code>,即 Angular 应用程序。</p><h2 id="引导-Angular-应用程序"><a href="#引导-Angular-应用程序" class="headerlink" title="引导 Angular 应用程序"></a>引导 Angular 应用程序</h2><p>前面在引导根模块过程中,创建了 Angular 应用程序之后,便会在应用程序的根级别引导新组件:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 在应用程序的根级别引导新组件</span></span><br><span class="line">moduleRef._bootstrapComponents.forEach(<span class="function"><span class="params">f</span> =></span> appRef.bootstrap(f));</span><br></pre></td></tr></table></figure><p>我们来看看这个过程会发生什么。</p><h3 id="应用程序-ApplicationRef"><a href="#应用程序-ApplicationRef" class="headerlink" title="应用程序 ApplicationRef"></a>应用程序 ApplicationRef</h3><p>一个 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><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"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> ApplicationRef {</span><br><span class="line"> <span class="comment">// 获取已注册到该应用程序的组件类型的列表</span></span><br><span class="line"> <span class="keyword">public</span> readonly componentTypes: Type<<span class="built_in">any</span>>[] = [];</span><br><span class="line"> <span class="comment">// 获取已注册到该应用程序的组件的列表</span></span><br><span class="line"> <span class="keyword">public</span> readonly components: ComponentRef<<span class="built_in">any</span>>[] = [];</span><br><span class="line"> <span class="comment">// 返回一个 Observable,指示应用程序何时稳定或不稳定</span></span><br><span class="line"> <span class="comment">// 如果在应用程序引导时,引导任何种类的周期性异步任务,则该应用程序将永远不会稳定(例如轮询过程)</span></span><br><span class="line"> <span class="keyword">public</span> readonly isStable!: Observable<<span class="built_in">boolean</span>>;</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> _zone: NgZone, <span class="keyword">private</span> _injector: Injector, <span class="keyword">private</span> _exceptionHandler: ErrorHandler,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _componentFactoryResolver: ComponentFactoryResolver,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _initStatus: ApplicationInitStatus</span>) {</span><br><span class="line"> <span class="comment">// 创建时,主要进行两件事:</span></span><br><span class="line"> <span class="comment">// 1. 宏任务结束后,检测视图是否需要更新。</span></span><br><span class="line"> <span class="comment">// 2. 在 Angular Zone 之外创建对 onStable 的预订,以便在 Angular Zone 之外运行回调。</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 在应用程序的根级别引导新组件</span></span><br><span class="line"> bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>, rootSelectorOrNode?: <span class="built_in">string</span>|<span class="built_in">any</span>):</span><br><span class="line"> ComponentRef<C> {}</span><br><span class="line"> <span class="comment">// 调用此方法以显式处理更改检测及其副作用</span></span><br><span class="line"> tick(): <span class="built_in">void</span> {}</span><br><span class="line"> <span class="comment">// 关联视图,以便对其进行脏检查,视图销毁后将自动分离</span></span><br><span class="line"> attachView(viewRef: ViewRef): <span class="built_in">void</span> {}</span><br><span class="line"> <span class="comment">// 再次从脏检查中分离视图</span></span><br><span class="line"> detachView(viewRef: ViewRef): <span class="built_in">void</span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么,我们来看看<code>bootstrap()</code>过程中,Angular 都做了些什么。</p><h3 id="在应用程序的根级别引导根组件"><a href="#在应用程序的根级别引导根组件" class="headerlink" title="在应用程序的根级别引导根组件"></a>在应用程序的根级别引导根组件</h3><p>将新的根组件引导到应用程序中时,Angular 将指定的应用程序组件安装到由<code>componentType</code>的选择器标识的 DOM 元素上,并引导自动更改检测以完成组件的初始化。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> ApplicationRef {</span><br><span class="line"> bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>, rootSelectorOrNode?: <span class="built_in">string</span>|<span class="built_in">any</span>):</span><br><span class="line"> ComponentRef<C> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 如果未与其他模块绑定,则创建与当前模块关联的工厂</span></span><br><span class="line"> <span class="keyword">const</span> ngModule =</span><br><span class="line"> isBoundToModule(componentFactory) ? <span class="literal">undefined</span> : <span class="keyword">this</span>._injector.get(NgModuleRef);</span><br><span class="line"> <span class="keyword">const</span> selectorOrNode = rootSelectorOrNode || componentFactory.selector;</span><br><span class="line"> <span class="comment">// 创建组件</span></span><br><span class="line"> <span class="keyword">const</span> compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);</span><br><span class="line"> <span class="keyword">const</span> nativeElement = compRef.location.nativeElement;</span><br><span class="line"> <span class="comment">// 创建可测试服务挂钩</span></span><br><span class="line"> <span class="keyword">const</span> testability = compRef.injector.get(Testability, <span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">const</span> testabilityRegistry = testability && compRef.injector.get(TestabilityRegistry);</span><br><span class="line"> <span class="keyword">if</span> (testability && testabilityRegistry) {</span><br><span class="line"> testabilityRegistry.registerApplication(nativeElement, testability);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 组件销毁时,销毁关联视图以及相关的服务</span></span><br><span class="line"> compRef.onDestroy(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">this</span>.detachView(compRef.hostView);</span><br><span class="line"> remove(<span class="keyword">this</span>.components, compRef);</span><br><span class="line"> <span class="keyword">if</span> (testabilityRegistry) {</span><br><span class="line"> testabilityRegistry.unregisterApplication(nativeElement);</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">this</span>._loadComponent(compRef);</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">return</span> compRef;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在创建根组件的过程中,会关联 DOM 元素视图、添加对状态变更的检测机制。</p><p>根组件是一个入口组件,Angular CLI 创建的默认应用只有一个组件<code>AppComponent</code>,Angular 会在引导过程中把它加载到 DOM 中。</p><p>在根组件的创建过程中,通常会根据根组件中引用到的其他组件,触发一系列组件的创建并形成组件树。大多数应用只有一个组件树,并且只从一个根组件开始引导。</p><h3 id="创建组件过程"><a href="#创建组件过程" class="headerlink" title="创建组件过程"></a>创建组件过程</h3><p>Angular 中创建组件的过程如下(参考<a href="https://angular.cn/guide/architecture-services" target="_blank" rel="noopener">服务与依赖注入简介</a>):</p><ol><li>当 Angular 创建组件类的新实例时,它会通过查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。</li><li>当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供者来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。</li><li>当所有请求的服务已解析并返回时,Angular 可以用这些服务实例为参数,调用该组件的构造函数。</li></ol><p>Angular 会在执行应用时创建注入器,第一个注入器是根注入器,创建于引导过程中。借助注入器继承机制,可以把全应用级的服务注入到这些组件中。</p><p>到这里,Angular 分别完成了根模块、根组件和组件树的引导过程,通过编译器则可以将组件和视图渲染到页面上。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在应用程序的引导过程中,Angular 采取了以下步骤来加载我们的第一个视图:</p><ol><li>加载<code>index.html</code>。</li><li>加载 Angular、第三方库和应用程序。</li><li>加载应用程序入口点<code>Main.ts</code>。</li><li>加载根模块。</li><li>加载根组件。</li><li>加载模板。</li></ol><p>本文我们重点从根模块的引导过程开始,介绍了引导 Angular 应用程序、引导根组件、组件的创建等过程。至于组件树的创建和渲染,则可以参考<a href>编译器</a>相关的内容。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://angular.cn/guide/bootstrapping" target="_blank" rel="noopener">通过根模块启动应用</a></li><li><a href="https://angular.cn/guide/entry-components" target="_blank" rel="noopener">Angular-入口组件</a></li><li><a href="https://www.tektutorialshub.com/angular/angular-bootstrapping-application/" target="_blank" rel="noopener">Bootstrapping in Angular: How It Works Internally</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 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/07/11/angular-design-di-2-hierarchical-di/"/>
<id>https://godbasin.github.io/2021/07/11/angular-design-di-2-hierarchical-di/</id>
<published>2021-07-11T06:55:31.000Z</published>
<updated>2021-07-11T07:21:17.823Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的最大特点——依赖注入,介绍 Angular 中多级依赖注入的设计。</p><a id="more"></a><p>上一篇我们介绍了 Angular 中的<code>Injectot</code>注入器、<code>Provider</code>提供者,以及注入器机制。那么,在 Angular 应用中,各个组件和模块间又是怎样共享依赖的,同样的服务是否可以多次实例化呢?</p><p>组件和模块的依赖注入过程,离不开 Angular 多级依赖注入的设计,我们来看看。</p><h1 id="多级依赖注入"><a href="#多级依赖注入" class="headerlink" title="多级依赖注入"></a>多级依赖注入</h1><p><a href="https://godbasin.github.io/2021/06/27/angular-design-di-1-basic-concepts/">前面</a>我们说过,Angular 中的注入器是可继承、且分层的。</p><p>在 Angular 中,有两个注入器层次结构:</p><ul><li><code>ModuleInjector</code>模块注入器:使用<code>@NgModule()</code>或<code>@Injectable()</code>注解在此层次结构中配置<code>ModuleInjector</code></li><li><code>ElementInjector</code>元素注入器:在每个 DOM 元素上隐式创建</li></ul><p>模块注入器和元素注入器都是树状结构的,但它们的分层结构并不完全一致。</p><h2 id="模块注入器"><a href="#模块注入器" class="headerlink" title="模块注入器"></a>模块注入器</h2><p>模块注入器的分层结构,除了与应用中模块设计有关系,还有平台模块(PlatformModule)注入器与应用程序模块(AppModule)注入器的分层结构。</p><h3 id="平台模块(PlatformModule)注入器"><a href="#平台模块(PlatformModule)注入器" class="headerlink" title="平台模块(PlatformModule)注入器"></a>平台模块(PlatformModule)注入器</h3><p>在 Angular 术语中,平台是供 Angular 应用程序在其中运行的上下文。Angular 应用程序最常见的平台是 Web 浏览器,但它也可以是移动设备的操作系统或 Web 服务器。</p><p>Angular 应用在启动时,会创建一个平台层:</p><ul><li>平台是 Angular 在网页上的入口点,每个页面只有一个平台</li><li>页面上运行的每个 Angular 应用程序,所共有的服务都在平台内绑定</li></ul><p>一个 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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> PlatformRef {</span><br><span class="line"> <span class="comment">// 传入注入器,作为平台注入器</span></span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params"><span class="keyword">private</span> _injector: Injector</span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 为给定的平台创建一个 @NgModule 的实例,以进行离线编译</span></span><br><span class="line"> bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):</span><br><span class="line"> <span class="built_in">Promise</span><NgModuleRef<M>> {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 使用给定的运行时编译器,为给定的平台创建一个 @NgModule 的实例</span></span><br><span class="line"> bootstrapModule<M>(</span><br><span class="line"> moduleType: Type<M>,</span><br><span class="line"> compilerOptions: (CompilerOptions&BootstrapOptions)|</span><br><span class="line"> <span class="built_in">Array</span><CompilerOptions&BootstrapOptions> = []): <span class="built_in">Promise</span><NgModuleRef<M>> {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 注册销毁平台时要调用的侦听器</span></span><br><span class="line"> onDestroy(callback: <span class="function"><span class="params">()</span> =></span> <span class="built_in">void</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="comment">// 该平台注入器是页面上每个 Angular 应用程序的父注入器,并提供单例提供程序</span></span><br><span class="line"> <span class="keyword">get</span> injector(): Injector {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 销毁页面上的当前 Angular 平台和所有 Angular 应用程序,包括销毁在平台上注册的所有模块和侦听器</span></span><br><span class="line"> destroy() {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实际上,平台在启动的时候(<code>bootstrapModuleFactory</code>方法中),在<code>ngZone.run</code>中创建<code>ngZoneInjector</code>,以便在 Angular 区域中创建所有实例化的服务,而<code>ApplicationRef</code>(页面上运行的 Angular 应用程序)将在 Angular 区域之外创建。</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">export</span> <span class="keyword">const</span> platformBrowser: <span class="function">(<span class="params">extraProviders?: StaticProvider[]</span>) =></span> PlatformRef =</span><br><span class="line"> createPlatformFactory(platformCore, <span class="string">'browser'</span>, INTERNAL_BROWSER_PLATFORM_PROVIDERS);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 其中,platformCore 平台必须包含在任何其他平台中</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> platformCore = createPlatformFactory(<span class="literal">null</span>, <span class="string">'core'</span>, _CORE_PLATFORM_PROVIDERS);</span><br></pre></td></tr></table></figure><p>使用平台工厂(例如上面的<code>createPlatformFactory</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="function"><span class="keyword">function</span> <span class="title">createPlatformFactory</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|<span class="literal">null</span>, name: <span class="built_in">string</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> providers: StaticProvider[] = []</span>): (<span class="params">extraProviders?: StaticProvider[]</span>) => <span class="title">PlatformRef</span> </span>{</span><br><span class="line"> <span class="keyword">const</span> desc = <span class="string">`Platform: <span class="subst">${name}</span>`</span>;</span><br><span class="line"> <span class="keyword">const</span> marker = <span class="keyword">new</span> InjectionToken(desc); <span class="comment">// DI 令牌</span></span><br><span class="line"> <span class="keyword">return</span> <span class="function">(<span class="params">extraProviders: StaticProvider[] = []</span>) =></span> {</span><br><span class="line"> <span class="keyword">let</span> platform = getPlatform();</span><br><span class="line"> <span class="comment">// 若平台已创建,则不做处理</span></span><br><span class="line"> <span class="keyword">if</span> (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, <span class="literal">false</span>)) {</span><br><span class="line"> <span class="keyword">if</span> (parentPlatformFactory) {</span><br><span class="line"> <span class="comment">// 若有父级平台,则直接使用父级平台,并更新相应的提供者</span></span><br><span class="line"> parentPlatformFactory(</span><br><span class="line"> providers.concat(extraProviders).concat({provide: marker, useValue: <span class="literal">true</span>}));</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">const</span> injectedProviders: StaticProvider[] =</span><br><span class="line"> providers.concat(extraProviders).concat({provide: marker, useValue: <span class="literal">true</span>}, {</span><br><span class="line"> provide: INJECTOR_SCOPE,</span><br><span class="line"> useValue: <span class="string">'platform'</span></span><br><span class="line"> });</span><br><span class="line"> <span class="comment">// 若无父级平台,则新建注入器,并创建平台</span></span><br><span class="line"> createPlatform(Injector.create({providers: injectedProviders, name: desc}));</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> assertPlatform(marker);</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过以上过程,我们知道 Angular 应用在创建平台的时候,创建平台的模块注入器<code>ModuleInjector</code>。我们从<a href>上一节</a><code>Injector</code>定义中也能看到,<code>NullInjector</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">abstract</span> <span class="keyword">class</span> Injector {</span><br><span class="line"> <span class="keyword">static</span> NULL: Injector = <span class="keyword">new</span> NullInjector();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因此,在平台模块注入器之上,还有<code>NullInjector()</code>。而在平台模块注入器之下,则还有应用程序模块注入器。</p><h3 id="应用程序根模块(AppModule)注入器"><a href="#应用程序根模块(AppModule)注入器" class="headerlink" title="应用程序根模块(AppModule)注入器"></a>应用程序根模块(AppModule)注入器</h3><p>每个应用程序有至少一个 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></pre></td><td class="code"><pre><span class="line"><span class="meta">@NgModule</span>({ providers: APPLICATION_MODULE_PROVIDERS })</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> ApplicationModule {</span><br><span class="line"> <span class="comment">// ApplicationRef 需要引导程序提供组件</span></span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params">appRef: ApplicationRef</span>) {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>AppModule</code>根应用模块由<code>BrowserModule</code>重新导出,当我们使用 CLI 的<code>new</code>命令创建新应用时,它会自动包含在根<code>AppModule</code>中。应用程序根模块中,提供者关联着内置的 DI 令牌,用于为引导程序配置根注入器。</p><p>Angular 还将<code>ComponentFactoryResolver</code>添加到根模块注入器中。此解析器存储了<code>entryComponents</code>系列工厂,因此它负责动态创建组件。</p><h3 id="模块注入器层级"><a href="#模块注入器层级" class="headerlink" title="模块注入器层级"></a>模块注入器层级</h3><p>到这里,我们可以简单地梳理出模块注入器的层级关系:</p><ol><li>模块注入器树的最上层则是应用程序根模块(AppModule)注入器,称作 root。</li><li>在 root 之上还有两个注入器,一个是平台模块(PlatformModule)注入器,一个是<code>NullInjector()</code>。</li></ol><p>因此,模块注入器的分层结构如下:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-1-injectors-1.svg" alt></p><p>在我们实际的应用中,它很可能是这样的:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/1_rjG7U4vLG_keRYoZnryxbA.png" alt></p><p>Angular DI 具有分层注入体系,这意味着下级注入器也可以创建它们自己的服务实例。</p><h2 id="元素注入器"><a href="#元素注入器" class="headerlink" title="元素注入器"></a>元素注入器</h2><p>前面说过,在 Angular 中有两个注入器层次结构,分别是模块注入器和元素注入器。</p><h3 id="元素注入器的引入"><a href="#元素注入器的引入" class="headerlink" title="元素注入器的引入"></a>元素注入器的引入</h3><p>当 Angular 中懒加载的模块开始广泛使用时,出现了一个 <a href="https://github.com/angular/angular/issues/13722" target="_blank" rel="noopener">issue</a>:依赖注入系统导致懒加载模块的实例化加倍。</p><p>在这一次修复中,引入了<a href="https://github.com/angular/angular/commit/13686bb" target="_blank" rel="noopener">新的设计</a>:<strong>注入器使用两棵并行的树,一棵用于元素,另一棵用于模块</strong>。</p><p>Angular 会为所有<code>entryComponents</code>创建宿主工厂,它们是所有其他组件的根视图。</p><p>这意味着每次我们创建动态 Angular 组件时,都会使用根数据(<code>RootData</code>)创建根视图(<code>RootView</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> ComponentFactory_ <span class="keyword">extends</span> ComponentFactory<<span class="built_in">any</span>>{</span><br><span class="line"> create(</span><br><span class="line"> injector: Injector, projectableNodes?: <span class="built_in">any</span>[][], rootSelectorOrNode?: <span class="built_in">string</span>|<span class="built_in">any</span>,</span><br><span class="line"> ngModule?: NgModuleRef<<span class="built_in">any</span>>): ComponentRef<<span class="built_in">any</span>> {</span><br><span class="line"> <span class="keyword">if</span> (!ngModule) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'ngModule should be provided'</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> viewDef = resolveDefinition(<span class="keyword">this</span>.viewDefFactory);</span><br><span class="line"> <span class="keyword">const</span> componentNodeIndex = viewDef.nodes[<span class="number">0</span>].element!.componentProvider!.nodeIndex;</span><br><span class="line"> <span class="comment">// 使用根数据创建根视图</span></span><br><span class="line"> <span class="keyword">const</span> view = Services.createRootView(</span><br><span class="line"> injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT);</span><br><span class="line"> <span class="comment">// view.nodes 的访问器</span></span><br><span class="line"> <span class="keyword">const</span> component = asProviderData(view, componentNodeIndex).instance;</span><br><span class="line"> <span class="keyword">if</span> (rootSelectorOrNode) {</span><br><span class="line"> view.renderer.setAttribute(asElementData(view, <span class="number">0</span>).renderElement, <span class="string">'ng-version'</span>, VERSION.full);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 创建组件</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ComponentRef_(view, <span class="keyword">new</span> ViewRef_(view), component);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>该根数据(<code>RootData</code>)包含对<code>elInjector</code>和<code>ngModule</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createRootData</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> elInjector: Injector, ngModule: NgModuleRef<<span class="built_in">any</span>>, rendererFactory: RendererFactory2,</span></span></span><br><span class="line"><span class="function"><span class="params"> projectableNodes: <span class="built_in">any</span>[][], rootSelectorOrNode: <span class="built_in">any</span></span>): <span class="title">RootData</span> </span>{</span><br><span class="line"> <span class="keyword">const</span> sanitizer = ngModule.injector.get(Sanitizer);</span><br><span class="line"> <span class="keyword">const</span> errorHandler = ngModule.injector.get(ErrorHandler);</span><br><span class="line"> <span class="keyword">const</span> renderer = rendererFactory.createRenderer(<span class="literal">null</span>, <span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> ngModule,</span><br><span class="line"> injector: elInjector,</span><br><span class="line"> projectableNodes,</span><br><span class="line"> selectorOrNode: rootSelectorOrNode,</span><br><span class="line"> sanitizer,</span><br><span class="line"> rendererFactory,</span><br><span class="line"> renderer,</span><br><span class="line"> errorHandler,</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>引入元素注入器树,原因是这样的设计比较简单。通过更改注入器层次结构,避免交错插入模块和组件注入器,从而导致延迟加载模块的双倍实例化。因为每个注入器都只有一个父对象,并且每次解析都必须精确地寻找一个注入器来检索依赖项。</p><h3 id="元素注入器(Element-Injector)"><a href="#元素注入器(Element-Injector)" class="headerlink" title="元素注入器(Element Injector)"></a>元素注入器(Element Injector)</h3><p>在 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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> ElementDef {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 在该视图中可见的 DI 的公共提供者</span></span><br><span class="line"> publicProviders: {[tokenKey: <span class="built_in">string</span>]: NodeDef}|<span class="literal">null</span>;</span><br><span class="line"> <span class="comment">// 与 visiblePublicProviders 相同,但还包括位于此元素上的私有提供者</span></span><br><span class="line"> allProviders: {[tokenKey: <span class="built_in">string</span>]: NodeDef}|<span class="literal">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>默认情况下<code>ElementInjector</code>为空,除非在<code>@Directive()</code>或<code>@Component()</code>的<code>providers</code>属性中进行配置。</p><p>当 Angular 为嵌套的 HTML 元素创建元素注入器时,要么从父元素注入器继承它,要么直接将父元素注入器分配给子节点定义。</p><p>如果子 HTML 元素上的元素注入器具有提供者,则应该继承该注入器。否则,无需为子组件创建单独的注入器,并且如果需要,可以直接从父级的注入器中解决依赖项。</p><h3 id="元素注入器与模块注入器的设计"><a href="#元素注入器与模块注入器的设计" class="headerlink" title="元素注入器与模块注入器的设计"></a>元素注入器与模块注入器的设计</h3><p>那么,元素注入器与模块注入器是从哪个地方开始成为平行树的呢?</p><p>我们已经知道,应用程序根模块(<code>AppModule</code>)会在使用 CLI 的<code>new</code>命令创建新应用时,自动包含在根<code>AppModule</code>中。</p><p>当应用程序(<code>ApplicationRef</code>)启动(<code>bootstrap</code>)时,会创建<code>entryComponent</code>:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);</span><br></pre></td></tr></table></figure><p>该过程会使用根数据(<code>RootData</code>)创建根视图(<code>RootView</code>),同时会创建根元素注入器,在这里<code>elInjector</code>为<code>Injector.NULL</code>。</p><p>在这里,Angular 的注入器树被分成元素注入器树和模块注入器树,这两个平行的树了。</p><p>Angular 会有规律的创建下级注入器,每当 Angular 创建一个在<code>@Component()</code>中指定了<code>providers</code>的组件实例时,它也会为该实例创建一个新的子注入器。类似的,当在运行期间加载一个新的<code>NgModule</code>时,Angular 也可以为它创建一个拥有自己的提供者的注入器。</p><p>子模块和组件注入器彼此独立,并且会为所提供的服务分别创建自己的实例。当 Angular 销毁<code>NgModule</code>或组件实例时,也会销毁这些注入器以及注入器中的那些服务实例。</p><h2 id="Angular-解析依赖过程"><a href="#Angular-解析依赖过程" class="headerlink" title="Angular 解析依赖过程"></a>Angular 解析依赖过程</h2><p>上面我们介绍了 Angular 中的两种注入器树:模块注入器树和元素注入器树。那么,Angular 在提供依赖时,又会以怎样的方式去进行解析呢。</p><p>在 Angular 种,当为组件/指令解析 token 获取依赖时,Angular 分为两个阶段来解析它:</p><ul><li>针对<code>ElementInjector</code>层次结构(其父级)</li><li>针对<code>ModuleInjector</code>层次结构(其父级)</li></ul><p>其过程如下(参考<a href="https://angular.cn/guide/hierarchical-dependency-injection#resolution-rules" target="_blank" rel="noopener">多级注入器-解析规则</a>):</p><ol><li>当组件声明依赖项时,Angular 会尝试使用它自己的<code>ElementInjector</code>来满足该依赖。</li><li>如果组件的注入器缺少提供者,它将把请求传给其父组件的<code>ElementInjector</code>。</li><li>这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先<code>ElementInjector</code>。</li><li>如果 Angular 在任何<code>ElementInjector</code>中都找不到提供者,它将返回到发起请求的元素,并在<code>ModuleInjector</code>层次结构中进行查找。</li><li>如果 Angular 仍然找不到提供者,它将引发错误。</li></ol><p>为此,Angular 引入一种特殊的合并注入器。</p><h3 id="合并注入器(Merge-Injector)"><a href="#合并注入器(Merge-Injector)" class="headerlink" title="合并注入器(Merge Injector)"></a>合并注入器(Merge Injector)</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> Injector_ <span class="keyword">implements</span> Injector {</span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params"><span class="keyword">private</span> view: ViewData, <span class="keyword">private</span> elDef: NodeDef|<span class="literal">null</span></span>) {}</span><br><span class="line"> <span class="keyword">get</span>(token: <span class="built_in">any</span>, notFoundValue: <span class="built_in">any</span> = Injector.THROW_IF_NOT_FOUND): <span class="built_in">any</span> {</span><br><span class="line"> <span class="keyword">const</span> allowPrivateServices =</span><br><span class="line"> <span class="keyword">this</span>.elDef ? (<span class="keyword">this</span>.elDef.flags & NodeFlags.ComponentView) !== <span class="number">0</span> : <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">return</span> Services.resolveDep(</span><br><span class="line"> <span class="keyword">this</span>.view, <span class="keyword">this</span>.elDef, allowPrivateServices,</span><br><span class="line"> {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当 Angular 解析依赖项时,合并注入器则是元素注入器树和模块注入器树之间的桥梁。当 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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> ViewContainerRef_ <span class="keyword">implements</span> ViewContainerData {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 父级试图元素注入器的查询</span></span><br><span class="line"> <span class="keyword">get</span> parentInjector(): Injector {</span><br><span class="line"> <span class="keyword">let</span> view = <span class="keyword">this</span>._view;</span><br><span class="line"> <span class="keyword">let</span> elDef = <span class="keyword">this</span>._elDef.parent;</span><br><span class="line"> <span class="keyword">while</span> (!elDef && view) {</span><br><span class="line"> elDef = viewParentEl(view);</span><br><span class="line"> view = view.parent!;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> view ? <span class="keyword">new</span> Injector_(view, elDef) : <span class="keyword">new</span> Injector_(<span class="keyword">this</span>._view, <span class="literal">null</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>注入器是可继承的,这意味着如果指定的注入器无法解析某个依赖,它就会请求父注入器来解析它。具体的解析算法在<code>resolveDep()</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="function"><span class="keyword">function</span> <span class="title">resolveDep</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> view: ViewData, elDef: NodeDef, allowPrivateServices: <span class="built_in">boolean</span>, depDef: DepDef,</span></span></span><br><span class="line"><span class="function"><span class="params"> notFoundValue: <span class="built_in">any</span> = Injector.THROW_IF_NOT_FOUND</span>): <span class="title">any</span> </span>{</span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// mod1</span></span><br><span class="line"> <span class="comment">// /</span></span><br><span class="line"> <span class="comment">// el1 mod2</span></span><br><span class="line"> <span class="comment">// \ /</span></span><br><span class="line"> <span class="comment">// el2</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// 请求 el2.injector.get(token)时,按以下顺序检查并返回找到的第一个值:</span></span><br><span class="line"> <span class="comment">// - el2.injector.get(token, default)</span></span><br><span class="line"> <span class="comment">// - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> do not check the module</span></span><br><span class="line"> <span class="comment">// - mod2.injector.get(token, default)</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果是<code><child></child></code>这样模板的根<code>AppComponent</code>组件,那么在 Angular 中将具有三个视图:</p><figure class="highlight html"><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="comment"><!-- HostView_AppComponent --></span></span><br><span class="line"> <span class="tag"><<span class="name">my-app</span>></span><span class="tag"></<span class="name">my-app</span>></span></span><br><span class="line"><span class="comment"><!-- View_AppComponent --></span></span><br><span class="line"> <span class="tag"><<span class="name">child</span>></span><span class="tag"></<span class="name">child</span>></span></span><br><span class="line"><span class="comment"><!-- View_ChildComponent --></span></span><br><span class="line"> some content</span><br></pre></td></tr></table></figure><p>依赖解析过程,解析算法会基于视图层次结构,如图所示进行:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/1_p3nTsvwXWjCilG5zG3ecKw.png" alt></p><p>如果在子组件中解析某些令牌,Angular 将:</p><ol><li>首先查看子元素注入器,进行检查<code>elRef.element.allProviders|publicProviders</code>。</li><li>然后遍历所有父视图元素(1),并检查元素注入器中的提供者。</li><li>如果下一个父视图元素等于<code>null</code>(2),则返回到<code>startView</code>(3),检查<code>startView.rootData.elnjector</code>(4)。</li><li>只有在找不到令牌的情况下,才检查<code>startView.rootData module.injector</code>( 5 )。</li></ol><p>由此可见,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><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="comment">// 对于嵌入式视图,这是包含视图容器的父节点的索引</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">viewParentEl</span>(<span class="params">view: ViewData</span>): <span class="title">NodeDef</span>|<span class="title">null</span> </span>{</span><br><span class="line"> <span class="keyword">const</span> parentView = view.parent;</span><br><span class="line"> <span class="keyword">if</span> (parentView) {</span><br><span class="line"> <span class="keyword">return</span> view.parentNodeDef !.parent;</span><br><span class="line"> } <span class="keyword">else</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></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要介绍了 Angular 中注入器的层级结构,在 Angular 中有两棵平行的注入器树:模块注入器树和元素注入器树。</p><p>元素注入器树的引入,主要是为了解决依赖注入解析懒加载模块时,导致模块的双倍实例化问题。在元素注入器树引入后,Angular 解析依赖的过程也有调整,优先寻找元素注入器以及父视图元素注入器等注入器的依赖,只有元素注入器中无法找到令牌时,才会查询模块注入器中的依赖。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://angular.cn/guide/hierarchical-dependency-injection" target="_blank" rel="noopener">Angular-多级注入器</a></li><li><a href="https://medium.com/angular-in-depth/angular-dependency-injection-and-tree-shakeable-tokens-4588a8f70d5d" target="_blank" rel="noopener">What you always wanted to know about Angular Dependency Injection tree</a></li><li><a href="https://indepth.dev/posts/1063/a-curious-case-of-the-host-decorator-and-element-injectors-in-angular" target="_blank" rel="noopener">A curious case of the @Host decorator and Element Injectors in Angular</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 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/06/27/angular-design-di-1-basic-concepts/"/>
<id>https://godbasin.github.io/2021/06/27/angular-design-di-1-basic-concepts/</id>
<published>2021-06-27T05:55:23.000Z</published>
<updated>2021-06-27T06:10:04.941Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的最大特点——依赖注入,首先来介绍一些 Angular 依赖注入体系中的基本概念。</p><a id="more"></a><h2 id="依赖注入"><a href="#依赖注入" class="headerlink" title="依赖注入"></a>依赖注入</h2><p>既然要介绍 Angular 框架的依赖注入设计,那么先铺垫一下依赖注入的基本概念。我们常常会搞混依赖倒置原则(DIP)、控制反转(IoC)、依赖注入(DI)这几个概念,因此这里会先简单介绍一下。</p><h3 id="依赖倒置原则、控制反转、依赖注入"><a href="#依赖倒置原则、控制反转、依赖注入" class="headerlink" title="依赖倒置原则、控制反转、依赖注入"></a>依赖倒置原则、控制反转、依赖注入</h3><p>低耦合、高内聚大概是每个系统的设计目标之一,而为此产生了很多的设计模式和理念,其中便包括依赖倒置原则、控制反转的设计思想。</p><p><strong>(1) 依赖倒置原则(DIP)。</strong></p><p>依赖倒置原则的原始定义为:</p><ul><li>高层模块不应该依赖低层模块,两者都应该依赖其抽象;</li><li>抽象不应该依赖细节,细节应该依赖抽象。</li></ul><p>简单说便是:模块间不应该直接依赖对方,应该依赖一个抽象的规则(接口或者时抽象类)。</p><p><strong>(2) 控制反转(IoC)。</strong></p><p>控制反转的定义为:模块间的依赖关系从程序内部提到外部来实例化管理。即对象在被创建的时候,由一个调控系统内所有对象的外界实体控制,并将其所依赖的对象的引用传递(注入)给它。</p><p>实现控制反转主要有两种方式:</p><ul><li>依赖注入:被动的接收依赖对象</li><li>依赖查找:主动索取依赖的对象</li></ul><p><strong>(3) 依赖注入。</strong></p><p>依赖注入,是控制反转的最为常见的一种技术。</p><p>依赖倒置和控制反转两者相辅相成,常常可以一起使用,可有效地降低模块间的耦合。</p><h2 id="Angular-中的依赖注入"><a href="#Angular-中的依赖注入" class="headerlink" title="Angular 中的依赖注入"></a>Angular 中的依赖注入</h2><p>在 Angular 中,同样使用了依赖注入的技术,DI 框架会在实例化某个类时,向其提供这个类所声明的依赖项(依赖项:指当类需要执行其功能时,所需要的服务或对象)。</p><p>Angular 中的依赖注入基本上是围绕着组件或者是模块展开的,主要用于给新建的组件提供依赖。</p><p>Angular 中主要的依赖注入机制是<strong>注入器机制</strong>:</p><ul><li>应用中所需的任何依赖,都必须使用该应用的注入器来注册一个提供者,以便注入器可以使用这个提供者来创建新实例</li><li>Angular 会在启动过程中,创建全应用级注入器以及所需的其它注入器</li></ul><p>这里面主要涉及两个概念,分别是<strong>Injector 注入器</strong>和<strong>Provider 提供商</strong>,我们来看看。</p><h3 id="Injector-注入器"><a href="#Injector-注入器" class="headerlink" title="Injector 注入器"></a>Injector 注入器</h3><p>Injector 注入器用于创建依赖,会维护一个容器来管理这些依赖,并尽可能地复用它们。注入器会提供依赖的一个单例,并把这个单例对象注入到多个组件中。</p><p>显然,作为一个用来创建、管理、维护依赖的容器,注入器的功能很简单:创建依赖实例、获取依赖实例、管理依赖实例。我们也可以从抽象类<code>Injector</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">abstract</span> <span class="keyword">class</span> Injector {</span><br><span class="line"> <span class="comment">// 找不到依赖</span></span><br><span class="line"> <span class="keyword">static</span> THROW_IF_NOT_FOUND = THROW_IF_NOT_FOUND;</span><br><span class="line"> <span class="comment">// NullInjector 是树的顶部</span></span><br><span class="line"> <span class="comment">// 如果你在树中向上走了很远,以至于要在 NullInjector 中寻找服务,那么将收到错误消息,或者对于 @Optional(),返回 null</span></span><br><span class="line"> <span class="keyword">static</span> NULL: Injector = <span class="keyword">new</span> NullInjector();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 根据提供的 Token 从 Injector 检索实例</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="keyword">get</span><T>(</span><br><span class="line"> token: Type<T> | AbstractType<T> | InjectionToken<T>,</span><br><span class="line"> notFoundValue?: T,</span><br><span class="line"> flags?: InjectFlags</span><br><span class="line"> ): T;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 创建一个新的 Injector 实例,该实例提供一个或多个依赖项</span></span><br><span class="line"> <span class="keyword">static</span> create(options: {</span><br><span class="line"> providers: StaticProvider[];</span><br><span class="line"> parent?: Injector;</span><br><span class="line"> name?: <span class="built_in">string</span>;</span><br><span class="line"> }): Injector;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ɵɵdefineInjectable 用于构造一个 InjectableDef</span></span><br><span class="line"> <span class="comment">// 它定义 DI 系统将如何构造 Token,并且在哪些 Injector 中可用</span></span><br><span class="line"> <span class="keyword">static</span> ɵprov = ɵɵdefineInjectable({</span><br><span class="line"> token: Injector,</span><br><span class="line"> providedIn: <span class="string">"any"</span> <span class="keyword">as</span> <span class="built_in">any</span>,</span><br><span class="line"> <span class="comment">// ɵɵinject 生成的指令:从当前活动的 Injector 注入 Token</span></span><br><span class="line"> factory: <span class="function"><span class="params">()</span> =></span> ɵɵinject(INJECTOR),</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> __NG_ELEMENT_ID__ = InjectorMarkers.Injector;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>也就是说,我们可以将需要共享的依赖实例添加到注入器中,并通过 Token 查询和检索注入器来获取相应的依赖实例。</p><p>需要注意的是,Angular 中的注入器是分层的,因此查找依赖的过程也是向上遍历注入器树的过程。</p><p>这是因为在 Angular 中,应用是以模块的方式组织的,具体可以参考<a href="https://godbasin.github.io/2021/06/13/angular-design-module/">Angular 框架解读–模块化组织</a>篇。一般来说,页面的 DOM 是以<code>html</code>作为根节点的树状结构,以此为基础,Angular 应用中的组件和模块也是与之相伴的树状结构。</p><p>而注入器服务于组件和模块,同样是挂载与模块和组织上的树状结构。因此,Injector 也划分为模块和组件级别,可分别为组件和模块提供依赖的具体实例。注入器是可继承的,这意味着如果指定的注入器无法解析某个依赖,它就会请求父注入器来解析它,我们同样可以从上面的创建注入器代码中看到:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建一个新的 Injector 实例,可传入 parent 父注入器</span></span><br><span class="line"><span class="keyword">static</span> create(options: {providers: StaticProvider[], parent?: Injector, name?: <span class="built_in">string</span>}): Injector;</span><br></pre></td></tr></table></figure><p>在某个注入器的范围内,服务是单例的。也就是说,在指定的注入器中最多只有某个服务的最多一个实例。如果不希望在所有地方都使用该服务的同一个实例,则可以通过注册多个注入器、并按照需要关联到组件和模块中的方式,来按需共享某个服务依赖的实例。</p><p>我们可以看到创建一个新的<code>Injector</code>实例时,传入的参数包括<code>Provider</code>,这是因为<code>Injector</code>不会直接创建依赖,而是通过<code>Provider</code>来完成的。每个注入器会维护一个提供者的列表,并根据组件或其它服务的需要,用它们来提供服务的实例。</p><h3 id="Provider-提供者"><a href="#Provider-提供者" class="headerlink" title="Provider 提供者"></a>Provider 提供者</h3><p>Provider 提供者用来告诉注入器应该如何获取或创建依赖,要想让注入器能够创建服务(或提供其它类型的依赖),必须使用某个提供者配置好注入器。</p><p>一个提供者对象定义了如何获取与 DI 令牌(token) 相关联的可注入依赖,而注入器会使用这个提供者来创建它所依赖的那些类的实例。</p><blockquote><p>关于 DI 令牌:</p><ul><li>当使用提供者配置注入器时,就会把提供者和一个 DI 令牌关联起来;</li><li>注入器维护一个内部令牌-提供者的映射表,当请求一个依赖项时就会引用它,令牌就是这个映射表的键。</li></ul></blockquote><p>提供者的类型很多,从<a href="https://angular.cn/guide/dependency-injection-providers" target="_blank" rel="noopener">官方文档</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> Provider =</span><br><span class="line"> | TypeProvider</span><br><span class="line"> | ValueProvider</span><br><span class="line"> | ClassProvider</span><br><span class="line"> | ConstructorProvider</span><br><span class="line"> | ExistingProvider</span><br><span class="line"> | FactoryProvider</span><br><span class="line"> | <span class="built_in">any</span>[];</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><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">resolveReflectiveFactory</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> provider: NormalizedProvider</span></span></span><br><span class="line"><span class="function"><span class="params"></span>): <span class="title">ResolvedReflectiveFactory</span> </span>{</span><br><span class="line"> <span class="keyword">let</span> factoryFn: <span class="built_in">Function</span>;</span><br><span class="line"> <span class="keyword">let</span> resolvedDeps: ReflectiveDependency[];</span><br><span class="line"> <span class="keyword">if</span> (provider.useClass) {</span><br><span class="line"> <span class="comment">// 使用类来提供依赖</span></span><br><span class="line"> <span class="keyword">const</span> useClass = resolveForwardRef(provider.useClass);</span><br><span class="line"> factoryFn = reflector.factory(useClass);</span><br><span class="line"> resolvedDeps = _dependenciesFor(useClass);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (provider.useExisting) {</span><br><span class="line"> <span class="comment">// 使用已有依赖</span></span><br><span class="line"> factoryFn = <span class="function">(<span class="params">aliasInstance: <span class="built_in">any</span></span>) =></span> aliasInstance;</span><br><span class="line"> <span class="comment">// 从根据 token 获取具体的依赖</span></span><br><span class="line"> resolvedDeps = [</span><br><span class="line"> ReflectiveDependency.fromKey(ReflectiveKey.get(provider.useExisting)),</span><br><span class="line"> ];</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (provider.useFactory) {</span><br><span class="line"> <span class="comment">// 使用工厂方法提供依赖</span></span><br><span class="line"> factoryFn = provider.useFactory;</span><br><span class="line"> resolvedDeps = constructDependencies(provider.useFactory, provider.deps);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 使用提供者具体的值作为依赖</span></span><br><span class="line"> factoryFn = <span class="function"><span class="params">()</span> =></span> provider.useValue;</span><br><span class="line"> resolvedDeps = _EMPTY_LIST;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ResolvedReflectiveFactory(factoryFn, resolvedDeps);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据不同类型的提供者,通过解析之后,得到由注入器 Injector 使用的提供者的内部解析表示形式:</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">interface</span> ResolvedReflectiveProvider {</span><br><span class="line"> <span class="comment">// 键,包括系统范围内的唯一 id,以及一个 token</span></span><br><span class="line"> key: ReflectiveKey;</span><br><span class="line"> <span class="comment">// 可以返回由键表示的对象的实例的工厂函数</span></span><br><span class="line"> resolvedFactories: ResolvedReflectiveFactory[];</span><br><span class="line"> <span class="comment">// 指示提供者是多提供者,还是常规提供者</span></span><br><span class="line"> multiProvider: <span class="built_in">boolean</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>提供者可以是服务类<code>ClassProvider</code>本身,如果把服务类指定为提供者令牌,那么注入器的默认行为是用<code>new</code>来实例化那个类。</p><h3 id="Angular-中的依赖注入服务"><a href="#Angular-中的依赖注入服务" class="headerlink" title="Angular 中的依赖注入服务"></a>Angular 中的依赖注入服务</h3><p>在 Angular 中,服务就是一个带有<code>@Injectable</code>装饰器的类,它封装了可以在应用程序中复用的非 UI 逻辑和代码。Angular 把组件和服务分开,是为了增进模块化程度和可复用性。</p><p>用<code>@Injectable</code>标记一个类,以确保编译器将在注入类时生成必要的<a href="https://godbasin.github.io/2021/03/27/angular-design-metadata/">元数据</a>(元数据在 Angular 中也是很重要的一部分),以创建类的依赖项。</p><p><code>@Injectable</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 根据其 Injectable 元数据,编译 Angular 可注入对象,并对结果进行修补</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">compileInjectable</span>(<span class="params"><span class="keyword">type</span>: Type<<span class="built_in">any</span>>, srcMeta?: Injectable</span>): <span class="title">void</span> </span>{</span><br><span class="line"> <span class="comment">// 该编译过程依赖 @angular/compiler</span></span><br><span class="line"> <span class="comment">// 可参考编译器中的 compileFactoryFunction compileInjectable 实现</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Angular 中可注入对象(<code>InjectableDef</code>)定义 DI 系统将如何构造 token 令牌,以及在哪些注入器(如果有)中可用:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> ɵɵInjectableDef<T> {</span><br><span class="line"> <span class="comment">// 指定给定类型属于特定注入器,包括 root/platform/any/null 以及特定的 NgModule</span></span><br><span class="line"> providedIn: InjectorType<<span class="built_in">any</span>> | <span class="string">"root"</span> | <span class="string">"platform"</span> | <span class="string">"any"</span> | <span class="literal">null</span>;</span><br><span class="line"> <span class="comment">// 此定义所属的令牌</span></span><br><span class="line"> token: unknown;</span><br><span class="line"> <span class="comment">// 要执行以创建可注入实例的工厂方法</span></span><br><span class="line"> factory: <span class="function">(<span class="params">t?: Type<<span class="built_in">any</span>></span>) =></span> T;</span><br><span class="line"> <span class="comment">// 在没有显式注入器的情况下,存储可注入实例的位置</span></span><br><span class="line"> value: T | <span class="literal">undefined</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用<code>@Injectable()</code>的<code>providedIn</code>时,优化工具可以进行 Tree-shaking 优化,从而删除应用程序中未使用的服务,以减小捆绑包尺寸。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文简单介绍了在 Angular 依赖注入体系中比较关键的几个概念,主要包括<code>Injector</code>、<code>Provider</code>和<code>Injectable</code>。</p><p>对于注入器、提供者和可注入服务,我们可以简单地这样理解:</p><ol><li>注入器用于创建依赖,会维护一个容器来管理这些依赖,并尽可能地复用它们。</li><li>一个注入器中的依赖服务,只有一个实例。</li><li>注入器需要使用提供者来管理依赖,并通过 token(DI 令牌)来进行关联。</li><li>提供者用于高速注入器应该如何获取或创建依赖。</li><li>可注入服务类会根据元数据编译后,得到可注入对象,该对象可用于创建实例。</li></ol><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://angular.cn/guide/dependency-injection" target="_blank" rel="noopener">Angular-Angular 中的依赖注入</a></li><li><a href="https://angular.cn/guide/dependency-injection-providers" target="_blank" rel="noopener">Angular-依赖提供者</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 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/06/13/angular-design-module/"/>
<id>https://godbasin.github.io/2021/06/13/angular-design-module/</id>
<published>2021-06-13T07:33:33.000Z</published>
<updated>2021-06-13T07:42:06.616Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的模块设计、模块化组织等内容进行介绍。</p><a id="more"></a><h2 id="Angular-中的模块"><a href="#Angular-中的模块" class="headerlink" title="Angular 中的模块"></a>Angular 中的模块</h2><p>在 AngularJS 升级到 Angular(2+ 版本)之后,引入了模块的设计。在我们进行 Angular 应用开发时,总是离不开模块,包括 Angular 自带的通用模块,以及应用启动的根模块等等。</p><p>说到模块化,前端开发首先会想到 <a href="https://hacks.mozilla.org/2015/08/es6-in-depth-modules/" target="_blank" rel="noopener">ES6 的模块</a>,这两者其实并没有什么关联:</p><ul><li>ES6 模块以文件为单位;Angular 模块则是以 NgModule 为单位。</li><li>ES6 模块用于跨文件的功能调用;Angular 模块用于组织有特定意义的功能块。</li><li>ES6 模块在编译阶段确认各个模块的依赖关系,模块间关系扁平;Angular 模块则可以带有深度的层次结构。</li></ul><h3 id="NgModules-定义"><a href="#NgModules-定义" class="headerlink" title="NgModules 定义"></a>NgModules 定义</h3><p>在 Angular 中,会使用 NgModules 来进行模块组织和管理。</p><p>NgModule 是一个带有<code>@NgModule</code>装饰器的类,<code>@NgModule</code>的参数是一个元数据对象,用于描述如何编译组件的模板,以及如何在运行时创建注入器。 它会标出该模块自己的组件、指令和管道,通过<code>exports</code>属性公开其中的一部分,以便外部组件使用它们。 关于元数据和装饰器,可参考<a href="https://godbasin.github.io/2021/03/27/angular-design-metadata/">Angular框架解读–元数据和装饰器</a>一节。</p><p>NgModule 把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。运行时,模块相关的信息存储在<code>NgModuleDef</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// NgModuleDef 是运行时用于组装组件、指令、管道和注入器的内部数据结构</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> NgModuleDef<T> {</span><br><span class="line"> <span class="comment">// 表示模块的令牌,由DI使用</span></span><br><span class="line"> <span class="keyword">type</span>: T;</span><br><span class="line"> <span class="comment">// 要引导的组件列表</span></span><br><span class="line"> bootstrap: Type<<span class="built_in">any</span>>[]|<span class="function">(<span class="params">(<span class="params"></span>) => Type<<span class="built_in">any</span>>[]</span>);</span></span><br><span class="line"><span class="function"> // 此模块声明的组件、指令和管道的列表</span></span><br><span class="line"><span class="function"> <span class="params">declarations</span>: <span class="params">Type</span><<span class="params">any</span>>[]|(<span class="params">(<span class="params"></span>) => Type<<span class="built_in">any</span>>[]</span>);</span></span><br><span class="line"><span class="function"> // 此模块导入的模块列表或 <span class="params">ModuleWithProviders</span> </span></span><br><span class="line"><span class="function"> <span class="params">imports</span>: <span class="params">Type</span><<span class="params">any</span>>[]|(<span class="params">(<span class="params"></span>) => Type<<span class="built_in">any</span>>[]</span>);</span></span><br><span class="line"><span class="function"> // 该模块导出的模块、<span class="params">ModuleWithProviders</span>、组件、指令或管道的列表</span></span><br><span class="line"><span class="function"> <span class="params">exports</span>: <span class="params">Type</span><<span class="params">any</span>>[]|(<span class="params">(<span class="params"></span>) => Type<<span class="built_in">any</span>>[]</span>);</span></span><br><span class="line"><span class="function"> // 为该模块计算的 <span class="params">transitiveCompileScopes</span> 的缓存值</span></span><br><span class="line"><span class="function"> <span class="params">transitiveCompileScopes</span>: <span class="params">NgModuleTransitiveScopes</span>|<span class="params">null</span>;</span></span><br><span class="line"><span class="function"> // 声明 <span class="params">NgModule</span> 中允许的元素的一组模式</span></span><br><span class="line"><span class="function"> <span class="params">schemas</span>: <span class="params">SchemaMetadata</span>[]|<span class="params">null</span>;</span></span><br><span class="line"><span class="function"> // 应为其注册模块的唯一<span class="params">ID</span></span></span><br><span class="line"><span class="function"> <span class="params">id</span>: <span class="params">string</span>|<span class="params">null</span>;</span></span><br><span class="line"><span class="function">}</span></span><br></pre></td></tr></table></figure><p>宏观来讲,NgModule 是组织 Angular 应用的一种方式,它们通过<code>@NgModule</code>装饰器中的元数据来实现这一点,这些元数据可以分成三类:</p><ul><li>静态的:编译器配置,通过<code>declarations</code>数组来配置。用于告诉编译器指令的选择器,并通过选择器匹配的方式,决定要把该指令应用到模板中的什么位置</li><li>运行时:通过<code>providers</code>数组提供给注入器的配置</li><li>组合/分组:通过<code>imports</code>和<code>exports</code>数组来把多个 NgModule 放在一起,并让它们可用</li></ul><p>可以看到,一个 NgModules 模块通过<code>declarations</code>声明该模块的组件、指令和管道,同时通过<code>import</code>导入其他模块和服务,以此来构成内聚的功能块。NgModule 还能把一些服务提供者添加到应用的依赖注入器中,具体可参考后续依赖注入部分内容。</p><h3 id="模块化组织"><a href="#模块化组织" class="headerlink" title="模块化组织"></a>模块化组织</h3><p>每个 Angular 应用有至少一个模块,该模块称为根模块(AppModule)。Angular 应用的启动,便是由根模块开始的,可以参考后续的依赖注入的引导过程内容。</p><p>对于一个简单的 Angular 应用来说,一个根模块就足以管理整个应用的功能。对于复杂的应用来说,则可以根据功能来划分成不同的模块,每个模块可专注于某项功能或业务领域、工作流程或导航流程、通用工具集,或者成为一个或多个服务提供者。</p><p>在 Angular 中,推荐的模块可以根据类型划分为:</p><ul><li>领域模块:领域模块围绕特性、业务领域或用户体验进行组织</li><li>带路由的模块:模块的顶层组件充当路由器访问这部分路由时的目的地</li><li>路由配置模块:路由配置模块为另一个模块提供路由配置</li><li>服务模块:服务模块提供实用服务,比如数据访问和消息传递</li><li>小部件:小部件模块可以为其它模块提供某些组件、指令或管道</li><li>共享模块:共享模块可以为其它的模块提供组件,指令和管道的集合</li></ul><p>可见,模块可以以不同的方式进行组织,可以包括组件、指令和管道和服务,也可以仅提供其中一种,比如<code>HttpClientModule</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@NgModule</span>({</span><br><span class="line"> <span class="comment">// XSRF 保护的可选配置</span></span><br><span class="line"> imports: [</span><br><span class="line"> HttpClientXsrfModule.withOptions({</span><br><span class="line"> cookieName: <span class="string">'XSRF-TOKEN'</span>,</span><br><span class="line"> headerName: <span class="string">'X-XSRF-TOKEN'</span>,</span><br><span class="line"> }),</span><br><span class="line"> ],</span><br><span class="line"> <span class="comment">// 配置 DI,并在其中将其与 HTTP 通信的支持服务一起导入</span></span><br><span class="line"> providers: [</span><br><span class="line"> HttpClient,</span><br><span class="line"> {provide: HttpHandler, useClass: HttpInterceptingHandler},</span><br><span class="line"> HttpXhrBackend,</span><br><span class="line"> {provide: HttpBackend, useExisting: HttpXhrBackend},</span><br><span class="line"> BrowserXhr,</span><br><span class="line"> {provide: XhrFactory, useExisting: BrowserXhr},</span><br><span class="line"> ],</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> HttpClientModule {</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="模块能力"><a href="#模块能力" class="headerlink" title="模块能力"></a>模块能力</h2><p>现在我们已经知道,NgModule 是把组件、指令和管道打包成内聚的功能块,那么在 NgModule 里面是怎么管理这些内容的呢?</p><h3 id="模块与组件"><a href="#模块与组件" class="headerlink" title="模块与组件"></a>模块与组件</h3><p>在 Angular 中,每个组件都应该(且只能)声明(declare)在一个 NgModule 类中。属于相同 NgModule 的组件会共享同一个编译上下文环境,该环境信息由<code>LocalModuleScopeRegistry</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> LocalModuleScopeRegistry <span class="keyword">implements</span> MetadataRegistry, ComponentScopeReader {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 从当前编译单元到声明它们的 NgModule 的组件映射</span></span><br><span class="line"> <span class="keyword">private</span> declarationToModule = <span class="keyword">new</span> Map<ClassDeclaration, DeclarationData>();</span><br><span class="line"> <span class="comment">// 这从指令/管道类映射到声明该指令/管道的每个 NgModule 的数据映射</span></span><br><span class="line"> <span class="keyword">private</span> duplicateDeclarations =</span><br><span class="line"> <span class="keyword">new</span> Map<ClassDeclaration, Map<ClassDeclaration, DeclarationData>>();</span><br><span class="line"> <span class="keyword">private</span> moduleToRef = <span class="keyword">new</span> Map<ClassDeclaration, Reference<ClassDeclaration>>();</span><br><span class="line"> <span class="comment">// 为当前程序中声明的每个 NgModule 计算的 LocalModuleScope 的缓存</span></span><br><span class="line"> <span class="keyword">private</span> cache = <span class="keyword">new</span> Map<ClassDeclaration, LocalModuleScope|<span class="literal">null</span>>();</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 将 NgModule 的数据添加到注册表中</span></span><br><span class="line"> registerNgModuleMetadata(data: NgModuleMeta): <span class="built_in">void</span> {}</span><br><span class="line"> <span class="comment">// 为组件获取作用域</span></span><br><span class="line"> getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|<span class="literal">null</span> {</span><br><span class="line"> <span class="keyword">const</span> scope = !<span class="keyword">this</span>.declarationToModule.has(clazz) ?</span><br><span class="line"> <span class="literal">null</span> :</span><br><span class="line"> <span class="comment">// 返回 NgModule 的作用域</span></span><br><span class="line"> <span class="keyword">this</span>.getScopeOfModule(<span class="keyword">this</span>.declarationToModule.get(clazz)!.ngModule);</span><br><span class="line"> <span class="keyword">return</span> scope;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 收集模块及其指令/管道的注册数据,并将其转换为完整的 LocalModuleScope</span></span><br><span class="line"> getScopeOfModule(clazz: ClassDeclaration): LocalModuleScope|<span class="literal">null</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.moduleToRef.has(clazz) ?</span><br><span class="line"> <span class="keyword">this</span>.getScopeOfModuleReference(<span class="keyword">this</span>.moduleToRef.get(clazz)!) :</span><br><span class="line"> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>LocalModuleScopeRegistry</code>类实现 NgModule 声明、导入和导出的逻辑,并且可以为给定组件生成在该组件的模板中“可见”的一组指令和管道。它收集有关本地的 NgModules,指令、组件和管道的信息,并且可以生成<code>LocalModuleScope</code>,概括了组件的编译范围。</p><p>每个 NgModule 在编译<code>@NgModule</code>装饰器的元数据时,会向<code>LocalModuleScopeRegistry</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> NgModuleDecoratorHandler <span class="keyword">implements</span></span><br><span class="line"> DecoratorHandler<Decorator, NgModuleAnalysis, NgModuleResolution> {</span><br><span class="line"> register(node: ClassDeclaration, analysis: NgModuleAnalysis): <span class="built_in">void</span> {</span><br><span class="line"> <span class="comment">// 这样可以确保在 compile() 阶段,模块的元数据可用于选择器作用域计算</span></span><br><span class="line"> <span class="keyword">this</span>.metaRegistry.registerNgModuleMetadata({</span><br><span class="line"> ref: <span class="keyword">new</span> Reference(node),</span><br><span class="line"> schemas: analysis.schemas,</span><br><span class="line"> declarations: analysis.declarations,</span><br><span class="line"> imports: analysis.imports,</span><br><span class="line"> exports: analysis.exports,</span><br><span class="line"> rawDeclarations: analysis.rawDeclarations,</span><br><span class="line"> });</span><br><span class="line"> ...</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>当组件在编译<code>@Component</code>装饰器的元数据时,会检查该组件是否已在 NgModule 中注册。如果已在某个模块中注册,则向<code>LocalModuleScopeRegistry</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> ComponentDecoratorHandler <span class="keyword">implements</span></span><br><span class="line"> DecoratorHandler<Decorator, ComponentAnalysisData, ComponentResolutionData> {</span><br><span class="line"> resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>):</span><br><span class="line"> ResolveResult<ComponentResolutionData> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 获取模块的作用域</span></span><br><span class="line"> <span class="keyword">const</span> scope = <span class="keyword">this</span>.scopeReader.getScopeForComponent(node);</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">if</span> (scope !== <span class="literal">null</span> && (!scope.compilation.isPoisoned || <span class="keyword">this</span>.usePoisonedData)) {</span><br><span class="line"> <span class="comment">// 对模块的作用域中的信息进行处理</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> dir of scope.compilation.directives) {</span><br><span class="line"> <span class="keyword">if</span> (dir.selector !== <span class="literal">null</span>) {</span><br><span class="line"> matcher.addSelectables(CssSelector.parse(dir.selector), dir <span class="keyword">as</span> MatchedDirective);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> pipes = <span class="keyword">new</span> Map<<span class="built_in">string</span>, Reference<ClassDeclaration>>();</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> pipe of scope.compilation.pipes) {</span><br><span class="line"> pipes.set(pipe.name, pipe.ref);</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>在获取到作用域之后,接下来组件会使用<code>R3TargetBinder</code>绑定组件模板 AST,这些内容会在 Ivy 编译器部分进行更多的介绍。</p><p>默认情况下,NgModule 都是急性加载的,也就是说它会在应用加载时尽快加载,所有模块都是如此,无论是否立即要用。对于带有很多路由的大型应用,考虑使用惰性加载:一种按需加载 NgModule 的模式。惰性加载可以减小初始包的尺寸,从而减少加载时间。</p><p>要惰性加载 Angular 模块,则需要用到<code>AppRoutingModule</code>,同时惰性加载还支持预加载的能力。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在 Angular 中,使用模块是最佳的组织方式。模块提供了聚焦于特定应用需求的一组功能,可以把应用划分成一些聚焦的功能区,比如用户工作流、路由或表单。 </p><p>对于 NgModule 模块,可以通过模块提供的服务以及共享出的组件、指令和管道来与根模块和其它 NgModule 模块进行合作。通过设置模块的导入和导出,Angular 可以解析出各个模块间的依赖关系。Angular 模块之间不允许出现循环依赖,因此一个 Angular 应用中的模块最终是呈现为以根模块为根节点的树状结构的。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://angular.cn/guide/ngmodules" target="_blank" rel="noopener">Angular-NgModules</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框架解读--Zone区域之ngZone</title>
<link href="https://godbasin.github.io/2021/05/30/angular-design-zone-ngzone/"/>
<id>https://godbasin.github.io/2021/05/30/angular-design-zone-ngzone/</id>
<published>2021-05-30T03:38:21.000Z</published>
<updated>2021-06-13T07:39:44.833Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的 NgZone 的设计和实现来介绍。</p><a id="more"></a><p>上一篇我们介绍了 <a href="https://godbasin.github.io/2021/05/01/angular-design-zonejs/">zone.js</a>,它解决了很多 Javascript 异步编程时上下文的问题。</p><p>NgZone 基于 zone.js 集成了适用于 Angular 框架的一些能力。其中,对于 Angular 中的数据变更检测(脏检查)的性能优化,则主要依赖了 NgZone 的设计,我们一起来看一下。</p><h2 id="NgZone"><a href="#NgZone" class="headerlink" title="NgZone"></a>NgZone</h2><p>虽然 zone.js 可以监视同步和异步操作的所有状态,但 Angular 还提供了一项名为 NgZone 的服务。</p><p>NgZone 是一种用于在 Angular 区域内部或外部执行工作的可注射服务,对于不需要 Angular 处理 UI 更新或错误处理的异步任务来说,进行了性能优化的工作。</p><h3 id="NgZone-设计"><a href="#NgZone-设计" class="headerlink" title="NgZone 设计"></a>NgZone 设计</h3><p>我们来看看 NgZone 的实现:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> NgZone {</span><br><span class="line"> readonly hasPendingMacrotasks: <span class="built_in">boolean</span> = <span class="literal">false</span>;</span><br><span class="line"> readonly hasPendingMicrotasks: <span class="built_in">boolean</span> = <span class="literal">false</span>;</span><br><span class="line"> readonly isStable: <span class="built_in">boolean</span> = <span class="literal">true</span>;</span><br><span class="line"> readonly onUnstable: EventEmitter<<span class="built_in">any</span>> = <span class="keyword">new</span> EventEmitter(<span class="literal">false</span>);</span><br><span class="line"> readonly onMicrotaskEmpty: EventEmitter<<span class="built_in">any</span>> = <span class="keyword">new</span> EventEmitter(<span class="literal">false</span>);</span><br><span class="line"> readonly onStable: EventEmitter<<span class="built_in">any</span>> = <span class="keyword">new</span> EventEmitter(<span class="literal">false</span>);</span><br><span class="line"> readonly onError: EventEmitter<<span class="built_in">any</span>> = <span class="keyword">new</span> EventEmitter(<span class="literal">false</span>);</span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params">{</span></span><br><span class="line"><span class="params"> enableLongStackTrace = <span class="literal">false</span>,</span></span><br><span class="line"><span class="params"> shouldCoalesceEventChangeDetection = <span class="literal">false</span>,</span></span><br><span class="line"><span class="params"> shouldCoalesceRunChangeDetection = <span class="literal">false</span></span></span><br><span class="line"><span class="params"> }</span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 在当前区域创建子区域,作为 Angular 区域</span></span><br><span class="line"> forkInnerZoneWithAngularBehavior(self);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 是否在 Angular 区域里</span></span><br><span class="line"> <span class="keyword">static</span> isInAngularZone(): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> Zone.current.get(<span class="string">'isAngularZone'</span>) === <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 在 Angular 区域内同步执行 fn 函数,并返回该函数返回的值</span></span><br><span class="line"> <span class="comment">// 通过 run 运行可让在 Angular 区域之外执行的任务重新进入 Angular 区域</span></span><br><span class="line"> run<T><span class="function">(<span class="params">fn: (<span class="params">...args: <span class="built_in">any</span>[]</span>) => T, applyThis?: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[]</span>): <span class="params">T</span> {</span></span><br><span class="line"><span class="function"> <span class="params">return</span> (<span class="params"><span class="keyword">this</span> <span class="keyword">as</span> <span class="built_in">any</span> <span class="keyword">as</span> NgZonePrivate</span>)._<span class="params">inner</span>.<span class="params">run</span>(<span class="params">fn, applyThis, applyArgs</span>);</span></span><br><span class="line"><span class="function"> }</span></span><br><span class="line"><span class="function"> // 在 <span class="params">Angular</span> 区域内作为任务同步执行 <span class="params">fn</span> 函数,并返回该函数返回的值</span></span><br><span class="line"><span class="function"> <span class="params">runTask</span><<span class="params">T</span>>(<span class="params">fn: (<span class="params">...args: <span class="built_in">any</span>[]</span>) => T, applyThis?: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[], name?: <span class="built_in">string</span></span>): <span class="params">T</span> {</span></span><br><span class="line"><span class="function"> <span class="params">const</span> <span class="params">zone</span> = (<span class="params"><span class="keyword">this</span> <span class="keyword">as</span> <span class="built_in">any</span> <span class="keyword">as</span> NgZonePrivate</span>)._<span class="params">inner</span>;</span></span><br><span class="line"><span class="function"> <span class="params">const</span> <span class="params">task</span> = <span class="params">zone</span>.<span class="params">scheduleEventTask</span>(<span class="params">'NgZoneEvent: ' + name, fn, EMPTY_PAYLOAD, noop, noop</span>);</span></span><br><span class="line"><span class="function"> <span class="params">try</span> {</span></span><br><span class="line"><span class="function"> <span class="params">return</span> <span class="params">zone</span>.<span class="params">runTask</span>(<span class="params">task, applyThis, applyArgs</span>);</span></span><br><span class="line"><span class="function"> } <span class="params">finally</span> {</span></span><br><span class="line"><span class="function"> <span class="params">zone</span>.<span class="params">cancelTask</span>(<span class="params">task</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="params">run</span> 相同,除了同步错误是通过 <span class="params">onError</span> 捕获并转发的,而不是重新抛出</span></span><br><span class="line"><span class="function"> <span class="params">runGuarded</span><<span class="params">T</span>>(<span class="params">fn: (<span class="params">...args: <span class="built_in">any</span>[]</span>) => T, applyThis?: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[]</span>): <span class="params">T</span> {</span></span><br><span class="line"><span class="function"> <span class="params">return</span> (<span class="params"><span class="keyword">this</span> <span class="keyword">as</span> <span class="built_in">any</span> <span class="keyword">as</span> NgZonePrivate</span>)._<span class="params">inner</span>.<span class="params">runGuarded</span>(<span class="params">fn, applyThis, applyArgs</span>);</span></span><br><span class="line"><span class="function"> }</span></span><br><span class="line"><span class="function"> // 在 <span class="params">Angular</span> 区域外同步执行 <span class="params">fn</span> 函数,并返回该函数返回的值</span></span><br><span class="line"><span class="function"> <span class="params">runOutsideAngular</span><<span class="params">T</span>>(<span class="params">fn: (<span class="params">...args: <span class="built_in">any</span>[]</span>) => T</span>): <span class="params">T</span> {</span></span><br><span class="line"><span class="function"> <span class="params">return</span> (<span class="params"><span class="keyword">this</span> <span class="keyword">as</span> <span class="built_in">any</span> <span class="keyword">as</span> NgZonePrivate</span>)._<span class="params">outer</span>.<span class="params">run</span>(<span class="params">fn</span>);</span></span><br><span class="line"><span class="function"> }</span></span><br><span class="line"><span class="function">}</span></span><br></pre></td></tr></table></figure><p>NgZone 基于 zone.js 之上再做了一层封装,通过<code>fork</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><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">forkInnerZoneWithAngularBehavior</span>(<span class="params">zone: NgZonePrivate</span>) </span>{</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 创建子区域,为 Angular 区域</span></span><br><span class="line"> zone._inner = zone._inner.fork({</span><br><span class="line"> name: <span class="string">'angular'</span>,</span><br><span class="line"> properties: <<span class="built_in">any</span>>{<span class="string">'isAngularZone'</span>: <span class="literal">true</span>},</span><br><span class="line"> ...</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>除此之外,NgZone 里添加了用于表示没有微任务或宏任务的属性<code>isStable</code>,可用于状态的检测。另外,NgZone 还定义了四个事件:</p><ul><li><code>onUnstable</code>: 通知代码何时进入 Angular Zone,首先会在 VM Turn 上触发</li><li><code>onMicrotaskEmpty</code>: 通知当前的 VM Turn 中没有更多的微任务排队。这是 Angular 进行更改检测的提示,它可能会排队更多的微任务(此事件可在每次 VM 翻转时触发多次)</li><li><code>onStable</code>: 通知最后一个<code>onMicrotaskEmpty</code>已运行并且没有更多的微任务,这意味着即将放弃 VM 转向(此事件仅被调用一次)</li><li><code>onError</code>: 通知已传送错误</li></ul><p>上一节我们讲到,zone.js 处理了大多数异步 API,比如<code>setTimeout()</code>、<code>Promise.then()</code>和<code>addEventListener()</code>等。对于一些 zone.js 无法处理的第三方 API,NgZone 服务的<code>run()</code>方法可允许在 angular Zone 中执行函数。</p><p>通过使用 Angular Zone,函数中的所有异步操作会在正确的时间自动触发变更检测。</p><h3 id="自动触发变更检测"><a href="#自动触发变更检测" class="headerlink" title="自动触发变更检测"></a>自动触发变更检测</h3><p>当 NgZone 满足以下条件时,会创建一个名为 angular 的 Zone 来自动触发变更检测:</p><ul><li>当执行同步或异步功能时(zone.js 内置变更检测,最终会通过<code>onMicrotaskEmpty</code>来触发)</li><li>已经没有已计划的 Microtask(<code>onMicrotaskEmpty</code>)</li></ul><p><code>onMicrotaskEmpty</code>条件的触发监听,以及检测逻辑位于<code>ApplicationRef</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> ApplicationRef {</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> _zone: NgZone, <span class="keyword">private</span> _injector: Injector, <span class="keyword">private</span> _exceptionHandler: ErrorHandler,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _componentFactoryResolver: ComponentFactoryResolver,</span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> _initStatus: ApplicationInitStatus</span>) {</span><br><span class="line"> <span class="comment">// Microtask 为空时,触发变更检测</span></span><br><span class="line"> <span class="keyword">this</span>._onMicrotaskEmptySubscription = <span class="keyword">this</span>._zone.onMicrotaskEmpty.subscribe({</span><br><span class="line"> next: <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="keyword">this</span>._zone.run(<span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="comment">// tick 为变更检测的逻辑,会重新进行 template 的计算和渲染</span></span><br><span class="line"> <span class="keyword">this</span>.tick();</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>我们来看看,在什么时候会触发<code>onMicrotaskEmpty</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">checkStable</span>(<span class="params">zone: NgZonePrivate</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (zone._nesting == <span class="number">0</span> && !zone.hasPendingMicrotasks && !zone.isStable) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> zone._nesting++;</span><br><span class="line"> zone.onMicrotaskEmpty.emit(<span class="literal">null</span>);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> zone._nesting--;</span><br><span class="line"> <span class="keyword">if</span> (!zone.hasPendingMicrotasks) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> zone.runOutsideAngular(<span class="function"><span class="params">()</span> =></span> zone.onStable.emit(<span class="literal">null</span>));</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> zone.isStable = <span class="literal">true</span>;</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>当<code>onInvokeTask</code>和<code>onInvoke</code>两个钩子被触发时,微任务队列中可能会发生变化,因此 Angular 必须在每次钩子被触发时运行检查。除此之外,<code>onHasTask</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">forkInnerZoneWithAngularBehavior</span>(<span class="params">zone: NgZonePrivate</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> delayChangeDetectionForEventsDelegate = <span class="function"><span class="params">()</span> =></span> {</span><br><span class="line"> <span class="comment">// delayChangeDetectionForEvents 内部调用了 checkStable()</span></span><br><span class="line"> delayChangeDetectionForEvents(zone);</span><br><span class="line"> };</span><br><span class="line"> zone._inner = zone._inner.fork({</span><br><span class="line"> ...</span><br><span class="line"> onInvokeTask:</span><br><span class="line"> (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: <span class="built_in">any</span>, applyArgs: <span class="built_in">any</span>): <span class="function"><span class="params">any</span> =></span> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 进行检测</span></span><br><span class="line"> delayChangeDetectionForEventsDelegate();</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> onInvoke:</span><br><span class="line"> (delegate: ZoneDelegate, current: Zone, target: Zone, callback: <span class="built_in">Function</span>, applyThis: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[], source?: <span class="built_in">string</span>): <span class="function"><span class="params">any</span> =></span> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 进行检测</span></span><br><span class="line"> delayChangeDetectionForEventsDelegate();</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> onHasTask:</span><br><span class="line"> (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">if</span> (current === target) {</span><br><span class="line"> <span class="comment">// 只检查当前区域的任务</span></span><br><span class="line"> <span class="keyword">if</span> (hasTaskState.change == <span class="string">'microTask'</span>) {</span><br><span class="line"> zone._hasPendingMicrotasks = hasTaskState.microTask;</span><br><span class="line"> updateMicroTaskStatus(zone);</span><br><span class="line"> <span class="comment">// 跟踪 MicroTask 队列,并进行检查</span></span><br><span class="line"> checkStable(zone);</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>默认情况下,所有异步操作都在 Angular Zone 内,这会自动触发变更检测。</p><p>另一个常见的情况是我们不想触发变更检测(比如不希望像<code>scroll</code>等事件过于频繁地进行变更检测,从而导致性能问题),此时可以使用 NgZone 的<code>runOutsideAngular()</code>方法。</p><p>zone.js 能帮助 Angular 知道何时要触发变更检测,使得开发人员专注于应用开发。默认情况下,zone.js 已加载且无需其他配置即可工作。如果希望选择自己触发变更检测,则可以通过禁用 zone.js 的方式来处理。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文介绍了 NgZone 在 zone.js 的基础上进行了封装,从而使得在 Angular Zone 内函数中的所有异步操作可以在正确的时间自动触发变更检测。</p><p>可以根据自身的需要,使用 NgZone 的<code>runOutsideAngular()</code>方法减少变更检测,也可以通过禁用 zone.js 的方式,来自己实现变更检测的逻辑。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://angular.cn/guide/zone" target="_blank" rel="noopener">Angular-NgZone</a></li><li><a href="https://indepth.dev/posts/1059/do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular" target="_blank" rel="noopener">Do you still think that NgZone (zone.js) is required for change detection in Angular?</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的 NgZone 的设计和实现来介绍。</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/05/16/a-book-with-one-story/"/>
<id>https://godbasin.github.io/2021/05/16/a-book-with-one-story/</id>
<published>2021-05-16T08:21:23.000Z</published>
<updated>2021-05-16T14:35:45.939Z</updated>
<content type="html"><![CDATA[<p>今天我的电子书《前端的进击》终于上架啦!</p><p>这本书说来话长,中间过程也算一波三折了,因此也值得写个文章来纪念下。</p><a id="more"></a><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/%E5%89%8D%E7%AB%AF%E7%9A%84%E8%BF%9B%E5%87%BB.jpg" alt></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>编辑小姐姐也很喜欢我写的内容,她跟我说,因为都是纯文字,如果加点插画可能会更好。我说我只会画一些很幼稚的画,她说能表达清楚就可以,不用画的多漂亮。</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>我们有考虑过,将这部分的内容进行开源,就像之前《深入理解 Vue.js 实战》这本书一样。对我来说,有一些很天真的想法和坚持,正如 Vue.js 这本书都写完了,最后我却征求编辑小姐姐的同意,将它进行了开源。</p><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>这本书定价不高,28 块钱大概是一顿饭钱,或者是一杯奶茶,如果感兴趣你也可以买来看看,特别推荐第三部分哦~s</p><blockquote><p>依然希望未来的某一天,这本书有机会印刷出版。</p></blockquote><ul><li>本书地址:<a href="https://www.ituring.com.cn/book/2942" target="_blank" rel="noopener">https://www.ituring.com.cn/book/2942</a> </li><li>目前只上架了图灵社区,后续会逐渐在其他平台上架哦~</li></ul>]]></content>
<summary type="html">
<p>今天我的电子书《前端的进击》终于上架啦!</p>
<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%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>Angular框架解读--Zone区域之zone.js</title>
<link href="https://godbasin.github.io/2021/05/01/angular-design-zonejs/"/>
<id>https://godbasin.github.io/2021/05/01/angular-design-zonejs/</id>
<published>2021-05-01T02:58:22.000Z</published>
<updated>2021-05-01T03:04:59.889Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的 NgZone 核心能力,这些能力主要基于 zone.js 来实现,因此本文先介绍 zone.js。</p><a id="more"></a><p>在 Angular 中,对于数据变更检测使用的是脏检查(dirty check),这曾经在 AngularJS 版本中被诟病,认为存在性能问题。而在 Angular(2+) 版本之后,通过引入模块化组织,以及 NgZone 的设计,提升了脏检查的性能。</p><p>对于 NgZone 的引入,并不只是为了解决脏检查的问题,它解决了很多 Javascript 异步编程时上下文的问题,其中 zone.js 便是针对异步编程提出的作用域解决方案。</p><h2 id="zone-js"><a href="#zone-js" class="headerlink" title="zone.js"></a>zone.js</h2><p>Zone 是跨异步任务而持久存在的执行上下文,zone.js 提供以下能力:</p><ul><li>提供异步操作之间的执行上下文</li><li>提供异步生命周期挂钩</li><li>提供统一的异步错误处理机制</li></ul><h3 id="异步操作的困惑"><a href="#异步操作的困惑" class="headerlink" title="异步操作的困惑"></a>异步操作的困惑</h3><p>在 Javascript 中,<a href="https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf#6e11" target="_blank" rel="noopener">代码执行过程中会产生堆栈,函数会在堆栈中执行</a>。</p><p>对于异步操作来说,异步代码和函数执行的时候,上下文可能发生了变化,为此可能导致一些难题。比如:</p><ul><li>异步代码执行时,上下文发生了变更,导致预期不一致</li><li><code>throw Error</code>时,无法准确定位到上下文</li><li>测试某个函数的执行耗时,但因为函数内有异步逻辑,无法得到准确的执行时间</li></ul><p>一般来说,异步代码执行时的上下文问题,可以通过传参或是全局变量的方式来解决,但两种方式都不是很优雅(尤其全局变量)。zone.js 正是为了解决以上问题而提出的,我们来看看。</p><h3 id="zone-js-的设计"><a href="#zone-js-的设计" class="headerlink" title="zone.js 的设计"></a>zone.js 的设计</h3><p>zone.js 的设计灵感来自 <a href="https://dart.dev/articles/archive/zones" target="_blank" rel="noopener">Dart Zones</a>,你也可以将其视为 JavaScript VM 中的 <a href="https://en.wikipedia.org/wiki/Thread-local_storage" target="_blank" rel="noopener">TLS–线程本地存储</a>。</p><p>zone 具有当前区域的概念:当前区域是随所有异步操作一起传播的异步上下文,它表示与当前正在执行的堆栈帧/异步任务关联的区域。</p><p>当前上下文可以使用<code>Zone.current</code>获取,可比作 Javascript 中的<code>this</code>,在 zone.js 中使用<code>_currentZoneFrame</code>变量跟踪当前区域。每个区域都有<code>name</code>属性,主要用于工具和调试目的,zone.js 还定义了用于操纵区域的方法:</p><ul><li><code>zone.fork(zoneSpec)</code>: 创建一个新的子区域,并将其<code>parent</code>设置为用于分支的区域</li><li><code>zone.run(callback, ...)</code>:在给定区域中同步调用一个函数</li><li><code>zone.runGuarded(callback, ...)</code>:与<code>run</code>捕获运行时错误相同,并提供了一种拦截它们的机制。如果任何父区域未处理错误,则将其重新抛出。</li><li><code>zone.wrap(callback)</code>:产生一个新的函数,该函数将区域绑定在一个闭包中,并在执行<code>zone.runGuarded(callback)</code>时执行,与 JavaScript 中的<code>Function.prototype.bind</code>工作原理类似。</li></ul><p>我们可以看到<code>Zone</code>的主要实现逻辑(<code>new Zone()</code>/<code>fork()</code>/<code>run()</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> Zone <span class="keyword">implements</span> AmbientZone {</span><br><span class="line"> <span class="comment">// 获取根区域</span></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> root(): AmbientZone {</span><br><span class="line"> <span class="keyword">let</span> zone = Zone.current;</span><br><span class="line"> <span class="comment">// 找到最外层,父区域为自己</span></span><br><span class="line"> <span class="keyword">while</span> (zone.parent) {</span><br><span class="line"> zone = zone.parent;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> zone;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 获取当前区域</span></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">get</span> current(): AmbientZone {</span><br><span class="line"> <span class="keyword">return</span> _currentZoneFrame.zone;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">private</span> _parent: Zone|<span class="literal">null</span>; <span class="comment">// 父区域</span></span><br><span class="line"> <span class="keyword">private</span> _name: <span class="built_in">string</span>; <span class="comment">// 区域名字</span></span><br><span class="line"> <span class="keyword">private</span> _properties: {[key: <span class="built_in">string</span>]: <span class="built_in">any</span>};</span><br><span class="line"> <span class="comment">// 拦截区域操作时的委托,用于生命周期钩子相关处理</span></span><br><span class="line"> <span class="keyword">private</span> _zoneDelegate: ZoneDelegate;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params">parent: Zone|<span class="literal">null</span>, zoneSpec: ZoneSpec|<span class="literal">null</span></span>) {</span><br><span class="line"> <span class="comment">// 创建区域时,设置区域的属性</span></span><br><span class="line"> <span class="keyword">this</span>._parent = parent;</span><br><span class="line"> <span class="keyword">this</span>._name = zoneSpec ? zoneSpec.name || <span class="string">'unnamed'</span> : <span class="string">'<root>'</span>;</span><br><span class="line"> <span class="keyword">this</span>._properties = zoneSpec && zoneSpec.properties || {};</span><br><span class="line"> <span class="keyword">this</span>._zoneDelegate =</span><br><span class="line"> <span class="keyword">new</span> ZoneDelegate(<span class="keyword">this</span>, <span class="keyword">this</span>._parent && <span class="keyword">this</span>._parent._zoneDelegate, zoneSpec);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// fork 会产生子区域</span></span><br><span class="line"> <span class="keyword">public</span> fork(zoneSpec: ZoneSpec): AmbientZone {</span><br><span class="line"> <span class="keyword">if</span> (!zoneSpec) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'ZoneSpec required!'</span>);</span><br><span class="line"> <span class="comment">// 以当前区域为父区域,调用 new Zone() 产生子区域</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>._zoneDelegate.fork(<span class="keyword">this</span>, zoneSpec);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 在区域中同步运行某段代码</span></span><br><span class="line"> <span class="keyword">public</span> run(callback: <span class="built_in">Function</span>, applyThis?: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[], source?: <span class="built_in">string</span>): <span class="built_in">any</span>;</span><br><span class="line"> <span class="keyword">public</span> run<T>(</span><br><span class="line"> callback: <span class="function">(<span class="params">...args: <span class="built_in">any</span>[]</span>) =></span> T, applyThis?: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[], source?: <span class="built_in">string</span>): T {</span><br><span class="line"> <span class="comment">// 准备执行,入栈处理</span></span><br><span class="line"> _currentZoneFrame = {parent: _currentZoneFrame, zone: <span class="keyword">this</span>};</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 使用 callback.apply(applyThis, applyArgs) 实现</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>._zoneDelegate.invoke(<span class="keyword">this</span>, callback, applyThis, applyArgs, source);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// 执行完毕,出栈处理</span></span><br><span class="line"> _currentZoneFrame = _currentZoneFrame.parent!;</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>除了上面介绍的,Zone 还提供了许多方法来运行、计划和取消任务,包括:</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">interface</span> Zone {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 通过在任务区域中恢复 Zone.currentTask 来执行任务</span></span><br><span class="line"> runTask<T>(task: Task, applyThis?: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>): T;</span><br><span class="line"> <span class="comment">// 安排一个 MicroTask</span></span><br><span class="line"> scheduleMicroTask(source: <span class="built_in">string</span>, callback: <span class="built_in">Function</span>, data?: TaskData, customSchedule?: <span class="function">(<span class="params">task: Task</span>) =></span> <span class="built_in">void</span>): MicroTask;</span><br><span class="line"> <span class="comment">// 安排一个 MacroTask</span></span><br><span class="line"> scheduleMacroTask(source: <span class="built_in">string</span>, callback: <span class="built_in">Function</span>, data?: TaskData, customSchedule?: <span class="function">(<span class="params">task: Task</span>) =></span> <span class="built_in">void</span>, customCancel?: <span class="function">(<span class="params">task: Task</span>) =></span> <span class="built_in">void</span>): MacroTask;</span><br><span class="line"> <span class="comment">// 安排一个 EventTask</span></span><br><span class="line"> scheduleEventTask(source: <span class="built_in">string</span>, callback: <span class="built_in">Function</span>, data?: TaskData, customSchedule?: <span class="function">(<span class="params">task: Task</span>) =></span> <span class="built_in">void</span>, customCancel?: <span class="function">(<span class="params">task: Task</span>) =></span> <span class="built_in">void</span>): EventTask;</span><br><span class="line"> <span class="comment">// 安排现有任务(对重新安排已取消的任务很有用)</span></span><br><span class="line"> scheduleTask<T <span class="keyword">extends</span> Task>(task: T): T;</span><br><span class="line"> <span class="comment">// 允许区域拦截计划任务的取消,使用 ZoneSpec.onCancelTask 配置拦截</span></span><br><span class="line"> cancelTask(task: Task): <span class="built_in">any</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="让异步逻辑运行在指定区域中"><a href="#让异步逻辑运行在指定区域中" class="headerlink" title="让异步逻辑运行在指定区域中"></a>让异步逻辑运行在指定区域中</h3><p>在 zone.js 中,通过<code>zone.fork</code>可以创建子区域,通过<code>zone.run</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> zoneBC = Zone.current.fork({name: <span class="string">'BC'</span>});</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">c</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(Zone.current.name); <span class="comment">// BC</span></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">b</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(Zone.current.name); <span class="comment">// BC</span></span><br><span class="line"> setTimeout(c, <span class="number">2000</span>);</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">a</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="built_in">console</span>.log(Zone.current.name); <span class="comment">// <root></span></span><br><span class="line"> zoneBC.run(b);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">a();</span><br></pre></td></tr></table></figure><p>执行的效果如图:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-zone-1.png" alt></p><p>实际上,每个异步任务的调用堆栈会以根区域开始。因此,在 zone.js 中该区域会使用与任务关联的信息来还原正确的区域,然后调用该任务:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/angular-zone-2.png" alt></p><p>对于<code>Zone.fork()</code>和<code>Zone.run()</code>的作用和实现,上面已经介绍过了。那么,zone.js 是如何识别出异步任务的呢?其实 zone.js 主要是通过猴子补丁拦截异步 API(包括 DOM 事件、<code>XMLHttpRequest</code>和 NodeJS 的 API 如<code>EventEmitter</code>、<code>fs</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 为指定的本地模块加载补丁</span></span><br><span class="line"><span class="keyword">static</span> __load_patch(name: <span class="built_in">string</span>, fn: _PatchFn, ignoreDuplicate = <span class="literal">false</span>): <span class="built_in">void</span> {</span><br><span class="line"> <span class="comment">// 检查是否已经加载补丁</span></span><br><span class="line"> <span class="keyword">if</span> (patches.hasOwnProperty(name)) {</span><br><span class="line"> <span class="keyword">if</span> (!ignoreDuplicate && checkDuplicate) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="built_in">Error</span>(<span class="string">'Already loaded patch: '</span> + name);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 检查是否需要加载补丁</span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (!global[<span class="string">'__Zone_disable_'</span> + name]) {</span><br><span class="line"> <span class="keyword">const</span> perfName = <span class="string">'Zone:'</span> + name;</span><br><span class="line"> <span class="comment">// 使用 performance.mark 标记时间戳</span></span><br><span class="line"> mark(perfName);</span><br><span class="line"> <span class="comment">// 拦截指定异步 API,并进行相关处理</span></span><br><span class="line"> patches[name] = fn(global, Zone, _api);</span><br><span class="line"> <span class="comment">// 使用 performance.measure 计算耗时</span></span><br><span class="line"> performanceMeasure(perfName, perfName);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以<code>setTimeout</code>等定时器为例子,通过拦截和捕获特定 API:</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">Zone.__load_patch(<span class="string">'timers'</span>, <span class="function">(<span class="params">global: <span class="built_in">any</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">set</span> = <span class="string">'set'</span>;</span><br><span class="line"> <span class="keyword">const</span> clear = <span class="string">'clear'</span>;</span><br><span class="line"> patchTimer(global, <span class="keyword">set</span>, clear, <span class="string">'Timeout'</span>);</span><br><span class="line"> patchTimer(global, <span class="keyword">set</span>, clear, <span class="string">'Interval'</span>);</span><br><span class="line"> patchTimer(global, <span class="keyword">set</span>, clear, <span class="string">'Immediate'</span>);</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p><code>patchTimer</code>做了很多兼容性的逻辑处理,包括 Node.js 和浏览器环境的检测和处理,其中比较关键的实现逻辑在:</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 检测该函数属性是否可写</span></span><br><span class="line"><span class="keyword">if</span> (isPropertyWritable(desc)) {</span><br><span class="line"> <span class="keyword">const</span> patchDelegate = patchFn(delegate!, delegateName, name);</span><br><span class="line"> <span class="comment">// 修改函数默认行为</span></span><br><span class="line"> proto[name] = <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="keyword">return</span> patchDelegate(<span class="keyword">this</span>, <span class="built_in">arguments</span> <span class="keyword">as</span> <span class="built_in">any</span>);</span><br><span class="line"> };</span><br><span class="line"> attachOriginToPatched(proto[name], delegate);</span><br><span class="line"> <span class="keyword">if</span> (shouldCopySymbolProperties) {</span><br><span class="line"> copySymbolProperties(delegate, proto[name]);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// patchFn 用于使用当前的区域创建 MacroTask 任务</span></span><br><span class="line"><span class="keyword">const</span> patchFn = <span class="function"><span class="keyword">function</span>(<span class="params">self: <span class="built_in">any</span>, args: <span class="built_in">any</span>[]</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> args[<span class="number">0</span>] === <span class="string">'function'</span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">const</span> callback = args[<span class="number">0</span>];</span><br><span class="line"> args[<span class="number">0</span>] = <span class="function"><span class="keyword">function</span> <span class="title">timer</span>(<span class="params"><span class="keyword">this</span>: unknown</span>) </span>{</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">return</span> callback.apply(<span class="keyword">this</span>, <span class="built_in">arguments</span>);</span><br><span class="line"> } <span class="keyword">finally</span> {</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><span class="line"> <span class="comment">// 使用当前的区域创建 MacroTask 任务,调用 Zone.current.scheduleMacroTask</span></span><br><span class="line"> <span class="keyword">const</span> task = scheduleMacroTaskWithCurrentZone(setName, args[<span class="number">0</span>], options, scheduleTask, clearTask);</span><br><span class="line"> <span class="keyword">if</span> (!task) {</span><br><span class="line"> <span class="keyword">return</span> task;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 一些兼容性处理工作,比如对于nodejs 环境,将任务引用保存在 timerId 对象中,用于 clearTimeout</span></span><br><span class="line"> <span class="keyword">return</span> task;</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">return</span> delegate.apply(<span class="built_in">window</span>, args);</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>在这里,计时器相关的 Timer 会被创建 MacroTask 任务并添加到 Zone 的任务中进行处理。在 zone.js 中,有将各种异步任务拆分为三种:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> TaskType = <span class="string">'microTask'</span>|<span class="string">'macroTask'</span>|<span class="string">'eventTask'</span>;</span><br></pre></td></tr></table></figure><p>zone.js 可以支持选择性地打补丁,具体更多的补丁机制可以参考 <a href="https://github.com/angular/angular/blob/master/packages/zone.js/STANDARD-APIS.md" target="_blank" rel="noopener">Zone.js’s support for standard apis</a>。</p><h3 id="任务执行的生命周期"><a href="#任务执行的生命周期" class="headerlink" title="任务执行的生命周期"></a>任务执行的生命周期</h3><p>zone.js 提供了异步操作生命周期钩子,有了这些钩子,Zone 可以监视和拦截异步操作的所有生命周期:</p><ul><li><code>onScheduleTask</code>:此回调将在<code>async</code>操作为之前被调用<code>scheduled</code>,这意味着<code>async</code>操作即将发送到浏览器(或 NodeJS )以计划在以后运行时</li><li><code>onInvokeTask</code>:此回调将在真正调用异步回调之前被调用</li><li><code>onHasTask</code>:当任务队列的状态在<code>empty</code>和之间更改时,将调用此回调<code>not empty</code></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> ZoneSpec {</span><br><span class="line"> <span class="comment">// 允许拦截 Zone.fork,对该区域进行 fork 时,请求将转发到此方法以进行拦截</span></span><br><span class="line"> onFork?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, zoneSpec: ZoneSpec</span>) =></span> Zone;</span><br><span class="line"> <span class="comment">// 允许拦截回调的 wrap</span></span><br><span class="line"> onIntercept?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: <span class="built_in">Function</span>, source: <span class="built_in">string</span></span>) =></span> <span class="built_in">Function</span>;</span><br><span class="line"> <span class="comment">// 允许拦截回调调用</span></span><br><span class="line"> onInvoke?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: <span class="built_in">Function</span>, applyThis: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[], source?: <span class="built_in">string</span></span>) =></span> <span class="built_in">any</span>;</span><br><span class="line"> <span class="comment">// 允许拦截错误处理</span></span><br><span class="line"> onHandleError?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: <span class="built_in">any</span></span>) =></span> <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="comment">// 允许拦截任务计划</span></span><br><span class="line"> onScheduleTask?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task</span>) =></span> Task;</span><br><span class="line"> <span class="comment">// 允许拦截任务回调调用</span></span><br><span class="line"> onInvokeTask?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task, applyThis: <span class="built_in">any</span>, applyArgs?: <span class="built_in">any</span>[]</span>) =></span> <span class="built_in">any</span>;</span><br><span class="line"> <span class="comment">// 允许拦截任务取消</span></span><br><span class="line"> onCancelTask?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task</span>) =></span> <span class="built_in">any</span>;</span><br><span class="line"> <span class="comment">// 通知对任务队列为空状态的更改</span></span><br><span class="line"> onHasTask?: <span class="function">(<span class="params">parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, hasTaskState: HasTaskState</span>) =></span> <span class="built_in">void</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这些生命周期的钩子回调会在<code>zone.fork()</code>时,通过<code>new Zone()</code>创建子区域并创建和传入到<code>ZoneDelegate</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> Zone <span class="keyword">implements</span> AmbientZone {</span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params">parent: Zone|<span class="literal">null</span>, zoneSpec: ZoneSpec|<span class="literal">null</span></span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">this</span>._zoneDelegate = <span class="keyword">new</span> ZoneDelegate(<span class="keyword">this</span>, <span class="keyword">this</span>._parent && <span class="keyword">this</span>._parent._zoneDelegate, zoneSpec);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以<code>onFork</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> ZoneDelegate <span class="keyword">implements</span> AmbientZoneDelegate {</span><br><span class="line"> <span class="keyword">constructor</span>(<span class="params">zone: Zone, parentDelegate: ZoneDelegate|<span class="literal">null</span>, zoneSpec: ZoneSpec|<span class="literal">null</span></span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 管理 onFork 钩子回调</span></span><br><span class="line"> <span class="keyword">this</span>._forkZS = zoneSpec && (zoneSpec && zoneSpec.onFork ? zoneSpec : parentDelegate!._forkZS);</span><br><span class="line"> <span class="keyword">this</span>._forkDlgt = zoneSpec && (zoneSpec.onFork ? parentDelegate : parentDelegate!._forkDlgt);</span><br><span class="line"> <span class="keyword">this</span>._forkCurrZone =</span><br><span class="line"> zoneSpec && (zoneSpec.onFork ? <span class="keyword">this</span>.zone : parentDelegate!._forkCurrZone);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// fork 调用时,会检查是否有 onFork 钩子回调注册,并进行调用</span></span><br><span class="line"> fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>._forkZS ? <span class="keyword">this</span>._forkZS.onFork!(<span class="keyword">this</span>._forkDlgt!, <span class="keyword">this</span>.zone, targetZone, zoneSpec) : <span class="keyword">new</span> Zone(targetZone, zoneSpec);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这便是 zone.js 中生命周期钩子的实现。有了这些钩子,我们可以做很多其他有用的事情,例如分析、记录和限制函数的执行和调用。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文我们主要介绍了 zone.js,它被设计用于解决异步编程中的执行上下文问题。</p><p>在 zone.js 中,当前区域是随所有异步操作一起传播的异步上下文,可比作 Javascript 中的<code>this</code>。通过<code>zone.fork</code>可以创建子区域,通过<code>zone.run</code>可让函数(包括函数里的异步逻辑)在指定的区域中运行。</p><p>zone.js 提供了丰富的生命周期钩子,可以使用 zone.js 的区域能力以及生命周期钩子解决前面我们提到的这些问题:</p><ul><li>异步代码执行时,上下文发生了变更,导致预期不一致:使用 Zone 来执行相关代码</li><li><code>throw Error</code>时,无法准确定位到上下文:使用生命周期钩子<code>onHandleError</code>进行处理和跟踪</li><li>测试某个函数的执行耗时,但因为函数内有异步逻辑,无法得到准确的执行时间:使用生命周期钩子配合可得到具体的耗时</li></ul><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://medium.com/ngconf/deep-dive-into-zone-js-part-1-execution-context-92166bbb957" target="_blank" rel="noopener">Deep dive into Zone.js [Part 1: Execution Context]</a></li><li><a href="https://medium.com/ngconf/deep-dive-into-zone-js-part-2-lifecycle-hooks-169da568227e" target="_blank" rel="noopener">Deep dive into Zone.js [Part 2: LifeCycle Hooks]</a></li><li><a href="https://indepth.dev/posts/1135/i-reverse-engineered-zones-zone-js-and-here-is-what-ive-found" target="_blank" rel="noopener">I reverse-engineered Zones (zone.js) and here is what I’ve found</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 Angular 中的 NgZone 核心能力,这些能力主要基于 zone.js 来实现,因此本文先介绍 zone.js。</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/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:[email protected]" target="_blank" rel="noopener">[email protected]</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>
</feed>