forked from godbasin/godbasin.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
460 lines (274 loc) · 290 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
<?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>2022-12-03T11:19:10.958Z</updated>
<id>https://godbasin.github.io/</id>
<author>
<name>被删</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>技术方案的调研和设计过程</title>
<link href="https://godbasin.github.io/2022/12/03/research-and-design-process/"/>
<id>https://godbasin.github.io/2022/12/03/research-and-design-process/</id>
<published>2022-12-03T11:18:23.000Z</published>
<updated>2022-12-03T11:19:10.958Z</updated>
<content type="html"><![CDATA[<p>技术方案设计属于架构能力中的一种,当我们开始作为某些功能/应用的 Owner 或是技术负责人来参与项目时,便会面临独立完成技术方案的调研和设计这样的工作内容。</p><a id="more"></a><p>一般来说,技术方案的调研和设计过程可以分为几个阶段:</p><ol><li>对项目的痛点、现状进行分析。</li><li>调用业界成熟的技术方案。</li><li>结合项目本身的现状和痛点,进行技术方案的选型和对比。</li></ol><h2 id="技术方案调研"><a href="#技术方案调研" class="headerlink" title="技术方案调研"></a>技术方案调研</h2><p>只有确保了技术方案的最优化、避免开发过程遇到问题需要推翻重做,从而能够快速落地并达成预期的效果。因此,在进行方案设计之前,对于项目存在的一些技术瓶颈、技术调整,我们需要先进行充分的前期调研。</p><p>在进行技术方案调研的时候,我们需要首先结合自身项目的背景、存在的痛点、现状问题进行分析,只有找到项目的问题在哪里,才可以更准确、彻底地去解决这些问题。</p><h3 id="分析项目背景,挖掘项目痛点"><a href="#分析项目背景,挖掘项目痛点" class="headerlink" title="分析项目背景,挖掘项目痛点"></a>分析项目背景,挖掘项目痛点</h3><p>技术方案的设计很多时候并不是命题作文,更多时候我们需要自己去挖掘项目的痛点,然后才是提出解决方案。</p><p>很多前端开发常常觉得自己做的项目没什么意思,认为每天都是重复的工作、繁琐的业务逻辑、糟糕的历史遗留代码。</p><p>实际上,那些会让我们觉得枯燥和重复的工作内容,也是可以去改善做好、并能从中获得成长的地方。好的业务可遇不可求,如果工作内容跟自己的预期不一样,我们就什么都不做了吗?</p><p>我们可以主动寻找项目存在的问题和痛点,并尝试去解决。不同的项目或是同一个项目的不同时期,关注的技术点都会不一样。对于一个前端项目来说,技术价值常常体现在系统性能、稳定性、可维护性、效率提升等地方,比如:</p><ul><li>对于用户量较大的项目,对系统稳定性要求较高,开发过程中需要关注是否会导致历史功能不兼容、是否会引入新的问题等;</li><li>对于大型复杂的项目,常常涉及多人协作,因此对系统可维护性要求更高,需要避免每次的改动都会导致性能和稳定性的下降,如何提升协作开发的效率等;</li><li>对于一次性的活动页面、管理端页面开发,技术挑战通常是如何提高开发效率,可以使用配置化、脚手架、自动化等手段来提升页面的开发和上线效率;</li></ul><p>找到项目的痛点之后,我们就可以进入项目的现状分析。</p><h3 id="现状分析"><a href="#现状分析" class="headerlink" title="现状分析"></a>现状分析</h3><p>项目的痛点可以转化为一个目标方向,比如:</p><ul><li>加载慢 -> 首屏加载耗时优化</li><li>开发效率低 -> 提升项目自动化程度</li><li>多人协作容易出问题 -> 提升系统稳定性</li></ul><p>确定目标之后,我们就需要进行技术方案的设计,但很多时候由于项目现状存在的问题,一些技术优化的方案并不适用,需要进行方向的调整。</p><p>假设有一个同样规模大、成员多的小程序项目,由于该项目处于快速迭代的时期,考虑到投入产出比、产品形态也在不断调整,老板说“每个功能由开发自己保证”,决定不投入测试资源。</p><p>这意味着开发不仅需要在自测的时候确保核心用例的覆盖,同时也没有足够的排期来进行自动化测试(单元测试、集成测试、端到端测试等)的开发。</p><p>一般来说,我们还可以考虑建立用例录制和自动化回归的解决方案。比如开发一个浏览器插件,来获取用户操作的一些行为(比如 Redux 中的 Action 操作),将操作行为的页面结果(状态数据,比如 Redux 的 State)保存下来。在发布之前,可以通过自动化触发相同的操作行为,并与录制的页面结果进行比较,来进行回归测试。</p><p>但对于小程序的特殊性,我们无法让其运行在浏览器中,更无法获取到它的操作行为。在这样的情况下,还有什么办法可以保证系统的稳定性呢?</p><p>考虑到一个系统的上线过程包括开发、测试、灰度和发布四个阶段,如果无法通过测试阶段来及时发现问题,那么我们还可以通过灰度过程中来及时发现并解决问题。</p><p>比如,通过全埋点覆盖各个页面的功能,灰度过程中观察埋点曲线是否有异常、及时告警和排查问题、暂停灰度或者回滚等方式,来避免给更多的用户带来不好的体验。</p><p>通过灰度的方式来保证系统稳定性,会对局部的用户造成影响,这并不是一个最优的技术方案,它是考虑到项目的现状退而求其次的解决方案,但最终也同样可以达到提升系统稳定性这样一个目的。</p><p>当我们确定了技术优化的具体方向之后,便可以进行业界方案的调研阶段了。</p><h3 id="业界方案调研"><a href="#业界方案调研" class="headerlink" title="业界方案调研"></a>业界方案调研</h3><p>当我们遇到一些技术问题并尝试解决的时候,需要提醒自己,这些问题肯定有其他人也遇到过。为了避免技术方案的设计过于局限,我们可以进行前期的调研,找一些业界相对成熟的方案作为参考,分析这些方案的优缺点、是否适用于自己的项目中。</p><p>我们可以通过几种方式去进行业界方案的调研:</p><ol><li>与有相关经验的开发进行沟通,交流技术方案,提供参考思路。</li><li>参考其他系统对外公开的方案设计。</li><li>参考开源项目的源码设计。</li></ol><p>举个例子,对于交互复杂、规模大型的应用,要如何管理各个模块间的依赖关系呢?业界相对成熟的解决方案是使用依赖注入体系,其中著名的开源项目中有 Angular 和 VsCode 都实现了依赖注入的框架,我们可以通过研究它们的相关代码,分析其中的思路以及实现方式。</p><p>开源项目源码很多,要怎么才能找到自己想看的部分呢?带着疑问有目的性地看,会简单轻松得多。比如上述的依赖注入框架,我们可以带着以下的问题进行研究:</p><ol><li>依赖注入框架是什么?</li><li>模块是怎样初始化,什么时候进行销毁的?</li><li>模块是如何获取到其它模块呢?</li><li>模块间是如何进行通信的呢?</li></ol><p>通过这样的方式阅读源码,我们可以快速掌握自己需要的一些信息。在业界方案调研完成之后,我们需要结合自身项目进行具体的技术方案设计。</p><h2 id="技术方案设计"><a href="#技术方案设计" class="headerlink" title="技术方案设计"></a>技术方案设计</h2><p>技术方案设计过程中,我们需要根据上述的调研资料进行整理,包括项目痛点、现状、业界方案等,然后进行方案的选型和对比,最终给到适合项目的解决方案。</p><h3 id="方案选型-对比"><a href="#方案选型-对比" class="headerlink" title="方案选型/对比"></a>方案选型/对比</h3><p>业界的解决方案可能有多套,这时候我们需要对这些方案进行分析比较。</p><p>除此之外,如果需要投入人力和时间成本去做一件事,我们就会面临一个问题:如何让团队认同这件事情、并愿意给到资源让我去完成它呢?梳理项目现状和痛点、提供业界认可的案例参考、进行全面的方案对比和选型,也是一种方式。</p><p>例如,假设我们最近需要针对项目进行自动化性能测试能力的支持:</p><ul><li>项目现状:项目规模大、模块多、参与开发的成员也有几十人</li><li>项目痛点:经常因为一些不同模块的变更导致项目的性能下降却没法及时发现问题,往往是等到用户反馈或是某次开发、产品或者测试发现的时候才得知</li></ul><p>调研常见的一些性能分析方案,发现有几种方式:</p><ol><li>通过 Chrome Devtools 提供的 Performace 火焰图,来定位和发现问题,但这种方式局限于开发手动分析定位。</li><li>使用 Lighthouse,该工具可以提供初步的网页优化建议,也支持自动化。但 Lighthouse 本身更专注于短时间内对网站进行较全面的评估,存在像分析不够细致和深入这些问题。</li><li>使用 Chrome Devtools 提供的 Chrome Devtools Protocol(CDP)能力,进行自动化生成火焰图需要的 JSON。但业界对该 JSON 的分析工具几乎没有,大家都通过将该 JSON 传到 Chrome Devtools 提供的一个工具来还原火焰图,无法支持全程的自动化分析。</li></ol><p>其中,第一和第二种方案都无法从根本上解决遇到的问题。如果要彻底解决这个问题,可以考虑采取第三种方案,并打算通过自行研究分析 CDP(Chrome Devtools Protocol)生成的 JSON 来达到完全的自动化目的。</p><p>方案选型和对比是技术方案设计中重要的一个环节,可以将现状和痛点分析得更加全面,同时还可以避开一些其他人踩过的坑。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>在大多数工作中,对开发的要求都不仅限于实现功能。你是否有想过,如果只是编写代码,刚毕业的应届生花几周时间也一样能做到,那么我们的优势在哪里呢?</p><p>洞察工作中的瓶颈,并有足够的能力去设计方案、排期开发、解决并复盘,这些技能更能突显我们在岗位上的价值和能力。对团队来说,更需要这样能主动发现并解决问题的成员,而不是安排什么就只做什么的螺丝钉。</p><p>技术的发展都离不开知识的沉淀、分享和相互学习,当我们遇到一些问题不知道该怎么解决的时候,可以试着站到巨人的肩膀上,说不定可以看到更多。</p>]]></content>
<summary type="html">
<p>技术方案设计属于架构能力中的一种,当我们开始作为某些功能/应用的 Owner 或是技术负责人来参与项目时,便会面临独立完成技术方案的调研和设计这样的工作内容。</p>
</summary>
<category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
<category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
</entry>
<entry>
<title>前端性能优化--项目管理篇</title>
<link href="https://godbasin.github.io/2022/11/20/front-end-performance-optimization-project/"/>
<id>https://godbasin.github.io/2022/11/20/front-end-performance-optimization-project/</id>
<published>2022-11-20T04:17:11.000Z</published>
<updated>2022-11-20T04:17:50.733Z</updated>
<content type="html"><![CDATA[<p>知晓要如何解决问题,只是真正解决问题的第一步。在工作里,我们更多时候遇到的问题不只是如何解决,而是如何有效落地。</p><a id="more"></a><p><a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>中,我给大家介绍了很多常见的前端性能优化思路和方案,核心优化思想为时间上减少耗时、空间上降低资源占用。其中耗时优化在前端性能优化中更常见,优化方案包括网络请求优化、首屏加载优化、渲染过程优化、计算/逻辑运行提速四个方面。</p><p>性能优化通常需要投入不少的人力和成本来完成,因此更多的时候我们可以将其当作是一个项目的方式来进行管理。从项目管理的角度来讲,我们的性能优化工作会拆解为以下部分内容:</p><ol><li>确定优化的目标和预期。</li><li>确定技术方案。</li><li>项目排期和执行。</li><li>进行项目复盘。</li></ol><h2 id="1-确定优化的目标和预期"><a href="#1-确定优化的目标和预期" class="headerlink" title="1. 确定优化的目标和预期"></a>1. 确定优化的目标和预期</h2><p>性能优化的第一步,就是要确定优化的目标和预期。在给出具体的数据之前,我们首先需要对一些性能数据进行定义,常见包括:</p><ul><li>网络资源请求时间</li><li>Time To Start Render(TTSR):浏览器开始渲染的时间</li><li>Dom Ready:页面解析完成的时间</li><li>Time To Interact(TTI)):页面可交互时间</li><li>Total Blocking Time (TBT):总阻塞时间,代表页面处于不可交互状态的耗时</li><li>First Input Delay(FID):从用户首次交互,到浏览器响应的时间</li></ul><p>要选择合适有效的指标进行定义,比如由于前端框架的出现,Page Load 耗时(<code>window.onload</code>事件触发的时间)已经难以用来作为页面可见时间的关键点,因此可以使用框架提供的生命周期,或者是使用 Largest Contentful Paint (LCP,关键内容加载的时间点)更为合适。</p><p>对需要关注的性能数据进行定义完成后,可以对它们进行目标和预期的确定,一般来说有两种方式:</p><ol><li>对比原先数据优化一定比例,比如 TTI 耗时减少 30%。</li><li>通过对竞品进行分析确定目标,比如比竞品耗时减少 20%。</li></ol><p>在确定了目标和预期之后,我们便可以根据预期来确定优化的方向、技术方案。</p><h2 id="2-确定技术方案"><a href="#2-确定技术方案" class="headerlink" title="2. 确定技术方案"></a>2. 确定技术方案</h2><p>根据确定的目标和预期,我们就可以选择合适的优化方案。</p><p>为什么不能将前面提到的全部技术方案都做一遍呢?显然这是不合理的。主要原因有两个:</p><ol><li>性价比。项目开发最看重的便是投入产出比,对于不同的项目来说,不同的技术优化方案需要投入人力不一样,很可能需要的投入较多但是优化效果可能不明显。</li><li>不适用,比如有些业务并不具备差异化服务。</li></ol><p>举个例子,阿猪的预期目标是客户端内打开应用 TTI 耗时减少 30%,因此他可以选择的优化方案包括:</p><ol><li>对首页数据进行分片/分屏加载。</li><li>首屏仅加载需要的资源,通过异步加载方式加载剩余资源。</li><li>使用服务端直出渲染。</li><li>使用 Tree-shaking 移除代码中无用的部分。</li><li>配合客户端进行资源预请求和预加载,比如使用预热 Web 容器。</li><li>配合客户端将资源和数据进行离线,可用于下一次页面的快速渲染。</li></ol><p>其中,5-6 需要客户端小伙伴进行支持,那么阿猪可以根据对方可以投入人力进行配合,来确定这两个优化点是否在本次方案中。</p><p>为了达成目标,对合适的技术优化点进行罗列之后,需要对每个优化点进行简单的调研,确定它们的优化效果。比如针对对首页数据进行分屏加载,可以通过简单的模拟测试,对比完整数据的 TTI 耗时,与首屏数据的 TTI 耗时,预估该技术点的优化效果如何。</p><p>最后,根据每个优化点的优化效果,以及相应的工作量评估,以预期为目标,选择性价比最优的技术方案。</p><p>在技术方案确定后,则需要对工作内容进行排期,并按计划执行。优化完成后,还需要结合目标和预期,对优化效果进行复盘,同时还可以提出未来优化的规划。</p><h2 id="3-项目排期和执行"><a href="#3-项目排期和执行" class="headerlink" title="3. 项目排期和执行"></a>3. 项目排期和执行</h2><p>这个步骤主要是排期实现,耗时最多。一般来说,需要注意的有两点:</p><ol><li>进行合理的分工排期。</li><li>对项目风险进行把控。</li></ol><h3 id="进行合理的分工排期"><a href="#进行合理的分工排期" class="headerlink" title="进行合理的分工排期"></a>进行合理的分工排期</h3><p>进行工作量评估的过程可以分为三步:</p><ol><li>确认技术方案,以及分工合作方式。</li><li>拆分具体功能模块,分别进行工作量评估,输出具体的排期时间表。</li><li>标注资源依赖情况和协作存在的风险,进行延期风险评估。</li></ol><p>当我们确认好技术方案之后,可以针对实现细节拆分具体的功能模块,分别进行工作量的预估和分工排期。具体的分工排期在多人协作的时候是必不可少的,否则可能面临分工不明确、接口协议未对齐就匆忙开工、最终因为各种问题而返工这些问题。</p><p>进行工作量评估的时候,可以精确到半天的工作量预期。对独自开发的项目来说,同样可以通过拆解功能模块这个过程,来思考具体的实现方式,也能提前发现一些可能存在的问题,并相应地进行规避。</p><p>提供完整的工作量评估和排期计划表(精确到具体的日期),可以帮助我们有计划地推进项目。在开发过程中,我们可以及时更新计划的执行情况,团队的其他人也可以了解我们的工作情况。</p><p>工作量评估和排期计划表的另外一个重要作用,是通过时间线去严格约束我们的工作效率、及时发现问题,并在项目结束后可针对时间维度进行项目复盘。</p><h3 id="对项目风险进行把控"><a href="#对项目风险进行把控" class="headerlink" title="对项目风险进行把控"></a>对项目风险进行把控</h3><p>我们在项目开发过程中,经常会遇到这样的情况:</p><ul><li>因为方案设计考虑不周,部分工作需要返工,导致项目延期</li><li>在项目进行过程中,常常会遇到依赖资源无法及时给到、依赖方因为种种原因无法按时支援等问题,导致项目无法按计划进行</li><li>团队协作方式未对齐,开发过程中出现矛盾,反复的争执和调整协作方式导致项目延期</li></ul><p>一个项目能按照预期计划进行,技术方案设计、分工和协作方式、依赖资源是否确定等,任何一各环节出现问题都可能导致整体的计划出现延误,这是我们不想出现的结果。</p><p>因此,我们需要主动把控各个环节的情况,及时推动和解决出现的一些多方协作的问题。</p><h2 id="4-进行项目复盘"><a href="#4-进行项目复盘" class="headerlink" title="4. 进行项目复盘"></a>4. 进行项目复盘</h2><p>很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目,其实他们遗漏了一个很重要的环节:复盘。</p><p>我换过好多个团队,发现大多数团队和个人,都没有养成复盘的习惯。复盘是一个特别好的习惯,对于我们个人的成长也好,项目的优化和发展也好,都有很好的作用。</p><p>当然,也有一些人会把复盘当做背锅和甩锅,这是不对的。当我们在项目过程中,常常因为有 Deadline 而不断地赶节奏,大多数情况下都只能发现一个问题解决一个问题。而在项目结束之后,我们才可以跳出项目,做更加广视角下的回顾和思考。</p><p>有效的复盘,可以达到以下的效果:</p><ol><li>及时发现自己的问题并改进,避免掉进同一个坑。</li><li>让团队成员知道每个人都在做什么,团队管理不混乱。</li><li>整理沉淀和分享项目经验,让整个团队都得到成长。</li></ol><p>对于大多数开发来说,很多时候都不屑于主动邀功,觉得自己做了些什么老板肯定都看在眼里,写什么总结和复盘都是刷存在感的表现。实际上老板们每天的事情很多,根本没法关注到每一个人,我以前也曾经跟老板们问过这样一个问题:做和说到底哪个重要?</p><p>答案是两个都重要。把一件事做好是必须的,但将这件事分享出来,可以同样给团队带来更多的成长。</p><p>通过对项目进行复盘,除了可以让团队其他人和老板知道我们做了些什么,更重要的是,我们可以及时发现自身的一些问题并改进。</p><p>项目复盘最好可以结合数据来说话,性能优化的工作可以用具体的耗时和 CPU 资源占用这些指标来做总结,工具的开发可以用接入使用的用户数量来说明效果。甚至是普普通通的项目上线,也都可以使用对比排期和实际开发,复盘各个环节的耗时和质量。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--SSR篇</title>
<link href="https://godbasin.github.io/2022/10/15/front-end-performance-ssr/"/>
<id>https://godbasin.github.io/2022/10/15/front-end-performance-ssr/</id>
<published>2022-10-15T00:40:56.000Z</published>
<updated>2022-10-15T00:59:51.765Z</updated>
<content type="html"><![CDATA[<p>SSR 也算是前端性能优化中最常用的技术方案了,能有效地缩短页面的可见时间,给用户带来很好的体验。</p><a id="more"></a><p>我们常说的 SSR 指 Server-Side Rendering,即服务端渲染,属于首屏直出渲染的一种方案。</p><h1 id="SSR-性能优化"><a href="#SSR-性能优化" class="headerlink" title="SSR 性能优化"></a>SSR 性能优化</h1><p>首先,我们来看一下 SSR 方案主要优化了哪些地方的性能。</p><h2 id="SSR-渲染方案"><a href="#SSR-渲染方案" class="headerlink" title="SSR 渲染方案"></a>SSR 渲染方案</h2><p>一般来说,我们页面加载会分为好几个步骤:</p><ol><li>请求域名,服务器返回 HTML 资源。</li><li>浏览器加载 HTML 片段,识别到有 CSS/JavaScript 资源时,获取资源并加载。</li></ol><p>现在大多数前端页面都是单页面应用,使用了一些前端框架来渲染页面,因此还会有以下的流程:</p><ol start="3"><li>加载并初始化前端框架、路由库。</li><li>根据当前页面路由配置,命中对应的页面组件并进行渲染。</li><li>页面组件如果有依赖的资源,则发起请求获取数据后,再进行渲染。</li></ol><p>到这里,用户才完整地可见到当前页面的内容,并进行操作。可见,页面启动时的加载流程比较长,对应的耗时也都无法避免。</p><p>使用 SSR 服务端渲染,可以在第 1 步中直接返回当前页面的内容,浏览器可以直接进行渲染,再加载剩余的其他资源,因此优化效果是十分明显的。除了性能上的优化,SSR 还可以带来更好的 SEO 效果,因为搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。</p><p>那一般来说 SSR 技术方案要怎么做呢?其实从上面的过程中,我们也可以推导出,需要根据页面路由和页面内容生成对应的 HTML 内容,用于首次获取 HTML 的时候直接返回。</p><h3 id="框架自带-SSR-渲染"><a href="#框架自带-SSR-渲染" class="headerlink" title="框架自带 SSR 渲染"></a>框架自带 SSR 渲染</h3><p>现在我们大多数前端项目都会使用框架,而许多开源框架也提供了 SSR 能力。由于前端框架本身就负责动态拼接和渲染 HTML 的工作,因此实现 SSR 有天然的便利性。</p><p>以 Vue 为例子,Vue 提供了 <a href="https://ssr.vuejs.org/zh/" target="_blank" rel="noopener">vue-server-renderer</a> 服务端能力,基本思想基本也是前面说过的:浏览器请求服务端时,服务端完成动态拼接 HTML 的能力,将拼接好的 HTML 直接返回给浏览器,浏览器可以直接渲染页面:</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">// 省略,可直接查看官网例子:https://ssr.vuejs.org/zh/guide/#%E5%AE%8C%E6%95%B4%E5%AE%9E%E4%BE%8B%E4%BB%A3%E7%A0%81</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 服务端收到请求时,生成 HTML 内容并返回</span></span><br><span class="line">server.get(<span class="string">"*"</span>, <span class="function">(<span class="params">req, res</span>) =></span> {</span><br><span class="line"> <span class="comment">// 使用 Vue 实例</span></span><br><span class="line"> <span class="keyword">const</span> app = <span class="keyword">new</span> Vue({</span><br><span class="line"> data: {</span><br><span class="line"> url: req.url,</span><br><span class="line"> },</span><br><span class="line"> template: <span class="string">`<div>访问的 URL 是: {{ url }}</div>`</span>,</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 使用 vue-server-renderer 将 Vue 实例生成最终的 HTML 内容</span></span><br><span class="line"> renderer.renderToString(app, context, <span class="function">(<span class="params">err, html</span>) =></span> {</span><br><span class="line"> <span class="built_in">console</span>.log(html);</span><br><span class="line"> <span class="keyword">if</span> (err) {</span><br><span class="line"> res.status(<span class="number">500</span>).end(<span class="string">"Internal Server Error"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> res.end(html);</span><br><span class="line"> });</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line">server.listen(<span class="number">8080</span>);</span><br></pre></td></tr></table></figure><p>当服务端收到请求时,生成 Vue 实例并依赖<code>vue-server-renderer</code>的能力,将 Vue 实例生成最终的 HTML 内容。该例子中,服务端直接使用现有资源就可以完成直出 HTML 的拼接.</p><p>但是在更多的前端应用场景下,通常还需要服务端动态获取其他的数据,才能完整地拼接出首屏需要的内容。一般来说,我们可以在服务端接到浏览器请求时,同时获取对应的数据,使用这些数据完成 HTML 拼接后再返回给浏览器。</p><p>在 Vue SSR 能力中,可以依赖<code>createApp</code>的能力,引入<code>Vuex</code>提前获取对应的数据并更新到 Store 中(参考<a href="https://ssr.vuejs.org/zh/guide/data.html" target="_blank" rel="noopener">数据预取和状态</a>),然后在服务端收到请求时,创建完整的 Vue 应用的能力:</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">const</span> createApp = <span class="built_in">require</span>(<span class="string">"/path/to/built-server-bundle.js"</span>);</span><br><span class="line"></span><br><span class="line">server.get(<span class="string">"*"</span>, <span class="function">(<span class="params">req, res</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> context = { url: req.url };</span><br><span class="line"></span><br><span class="line"> createApp(context).then(<span class="function">(<span class="params">app</span>) =></span> {</span><br><span class="line"> renderer.renderToString(app, <span class="function">(<span class="params">err, html</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (err) {</span><br><span class="line"> <span class="keyword">if</span> (err.code === <span class="number">404</span>) {</span><br><span class="line"> res.status(<span class="number">404</span>).end(<span class="string">"Page not found"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> res.status(<span class="number">500</span>).end(<span class="string">"Internal Server Error"</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> res.end(html);</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><h3 id="同构-SSR-渲染"><a href="#同构-SSR-渲染" class="headerlink" title="同构 SSR 渲染"></a>同构 SSR 渲染</h3><p>前面我们讲到,Vue 提供了 SSR 的能力,这意味着我们可以使用 Vue 来完成客户端和服务端渲染,因此大部分的代码都可以复用。对于这种一份代码可分别在服务器和客户端上运行,我们成为“同构”。</p><p>对比自行实现 SSR 渲染,依赖开源框架提供的同构能力,一套代码可以分别实现 CSR 和 SSR,可大大节省维护成本。</p><p>还是以 Vue 为例,使用 Vue 框架实现同构,大概的逻辑如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/786a415a-5fee-11e6-9c11-45a2cfdf085c.png" alt></p><p>不管是路由能力,还是组件渲染的能力,要保持同一套代码能分别运行在浏览器和服务端环境(Node.js)中,对于代码的编写则有一定的要求,比如 DOM 操作、window/document 对象等都需要谨慎,这些 <a href="https://ssr.vuejs.org/zh/guide/universal.html" target="_blank" rel="noopener">Vue 官方指引</a>也有介绍。</p><p>除此之外,服务端的入口逻辑显然会和客户端有差异,比如资源的获取方式、依赖的公共资源有所不一样等等。因此,在打包构建时会区分出两端的入口文件,并对通用逻辑做整合打包。这些内容也都在上面的图中有所体现。</p><h3 id="非同构-SSR-渲染"><a href="#非同构-SSR-渲染" class="headerlink" title="非同构 SSR 渲染"></a>非同构 SSR 渲染</h3><p>如果我们并没有强依赖前端框架,或是我们的项目过于复杂,此时可能要实现同构需要的成本比较大(抽离通用模块、移除环境依赖代码等)。考虑到项目的确需要 SSR 来加速页面可见,此时我们可以针对首屏渲染内容,自行实现 SSR 渲染。</p><p>SSR 核心思想前面也讲过好几遍了,因此要做的事情也比较明确:根据不同的路由,提供对于的页面首屏拼接的能力。由于不强依赖于同构,因此可以直接使用其他语言或是 ejs 来实现首屏 HTML 内容的拼接。</p><p>显然,非同构的方案实现 SSR 的成本,比同构的方案成本要高不少,并且还存在代码一致性、可维护性等一系列问题。因此,即使首屏直出的内容无法使用框架同构,大多数情况下,我们也会考虑尽量复用现有的代码,抽离核心的通用代码,并提供 SSR 服务代码编译打包的能力。</p><p>举个例子,假设我们的页面完全由 Canvas 进行渲染,显然 Canvas 是无法直出的。但正因为 Canvas 渲染前,需要加载的代码、计算渲染内容等各种流程过长,耗时较多,想要实现 SSR 渲染则可能只能考虑,针对首屏内容做一套 DOM/SVG 渲染用于 SSR。</p><p>基于这样的情况下,我们需要尽量复用计算部分的能力,抽离出通用的 Canvas/DOM/SVG 渲染接口,以尽可能实现对接口编程而不是对实现编程。</p><h2 id="SSR-利弊"><a href="#SSR-利弊" class="headerlink" title="SSR 利弊"></a>SSR 利弊</h2><p>上面主要围绕 SSR 的实现思想,介绍了开源框架 SSR、同构/非同构等 SSR 方案。</p><p>其实除了代码实现的部分以外,一个完整的 SSR 方案,还需要考虑:</p><ul><li>代码构建/部署:代码发布流程中,如何确保 SSR 部分代码的有效性,即不会因为非 SSR 部分代码的变更导致 SSR 服务异常</li><li>是否使用 Serverless:是否使用 Serverless 来部署 SSR 服务</li><li>是否使用缓存:是否可以将 SSR 部分或是最终生成的 HTML 结果进行缓存,节约服务端计算和拼接成本</li></ul><p>我们在选择一个技术方案的时候,不能只看它能带来什么收益,同时还需要评估一并带来的风险以及弊端。</p><p>对于 SSR 来说,收益是显而易见的,前面也有提到:</p><ul><li>实现更快的内容到达时间 (time-to-content)</li><li>更好的 SEO</li></ul><p>而其弊端也是客观存在的,包括:</p><ul><li>服务端资源消耗</li><li>方案需要开发成本和维护成本</li><li>可能会影响页面最终的完全可交互时间</li></ul><p>对于最后一点,有时候也会被我们忽略。因为 SSR 在最开始就提供了首屏完整的 HTML 内容,用户可见时间极大地提前了,我们常常会忘了关注页面所有功能加载完成、页面可交互的时间点。显然,由于浏览器需要在首屏时渲染完整的 HTML 内容,该过程也是需要一定的耗时的,所以后面的其他步骤完成的时间点都会有所延迟。如果首屏 HTML 内容很多/复杂的情况下,这种情况会更明显。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>SSR 的内容大概讲到这里,其实在更多的时候,SSR 方案的重点往往是文中一笔带过的弊端。实现一套同构渲染的代码,亦或是维护两套分别用于 CSR/SSR 的代码,这些方案的目的和方向都比较明确。</p><p>而 SSR 部署在什么环境、使用服务端还是 Serverless 生成,是否结合缓存实现、缓存更新策略又该是怎样的,如何保证非同构代码的渲染一致性,这些问题才是我们在将 SSR 方案落地过程中,需要反复思考和琢磨的问题。</p><p>我们在做方案调研的时候,也常常会过于关注开发成本和最终效果,从而忽略了整个项目和方案过程中的许多可能性。虽然目的的确很重要,但要记住过程也是很重要的。</p>]]></content>
<summary type="html">
<p>SSR 也算是前端性能优化中最常用的技术方案了,能有效地缩短页面的可见时间,给用户带来很好的体验。</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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端这几年--15.关于互联网寒冬</title>
<link href="https://godbasin.github.io/2022/09/17/about-front-end-15/"/>
<id>https://godbasin.github.io/2022/09/17/about-front-end-15/</id>
<published>2022-09-17T02:31:21.000Z</published>
<updated>2022-09-17T02:33:53.007Z</updated>
<content type="html"><![CDATA[<p>今年在互联网行业工作的大家,想必大概都有所感受。去年还在想方设法招聘应届生的许多公司,今年温度骤降,裁员的裁员,毁 offer 的毁 offer。</p><a id="more"></a><h2 id="所谓“冰河世纪”"><a href="#所谓“冰河世纪”" class="headerlink" title="所谓“冰河世纪”"></a>所谓“冰河世纪”</h2><p>今年来,听到了许多事、也看到了许多,看着不少认识的小伙伴一个个离开,万分感慨。</p><p>老板们强调着互联网寒冬,希望每个人都努力卷起来,最好能“当成自己的事业”来奋斗。因为冰河世纪,裁掉了好一部分人;因为冰河世纪,留下的每个人分到的事情更多;还因为冰河世纪,待遇都有所降低。</p><p>刚开始,不少人为了能留下来,的确变得更卷了。时间长了后,大部分人还是逐渐恢复了原本的节奏。毕竟要马跑,也得让马吃点草。</p><p>不少离开的人反而有了更好的去处,逃离了压抑的工作环境,同时还拿到了更好的待遇。这么一对比,其实有时候被迫脱离舒适的环境,其实也未必是件坏事,反而是帮我们下决心了。</p><h3 id="打铁还是得自身硬"><a href="#打铁还是得自身硬" class="headerlink" title="打铁还是得自身硬"></a>打铁还是得自身硬</h3><p>其实在所谓的互联网寒冬以前,我都十分重视个人的成长,因为温水煮青蛙是一件很危险的事情。不过相比于担心被淘汰,更多还是出于对自身的要求吧,我也挺喜欢不断成长的滋味的。</p><p>我一直认为,有能力的人走到哪里都不会担心。能力提升上去了,不会担心被淘汰,即使离开了也能很快找到下一份工作。因为有能力的人,哪里都缺。</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><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>其实有时候会想,这行业的领导还挺好当的。当然,好的老板很难当,但只是个老板的话,感觉还挺简单的,反正事情做好了都归功老板,出问题了底下人背锅。即使是在所谓的冰河世纪,裁员也会先裁底下员工,就算裁到 leader,也可以拿着大家做的成果出去轻松找到下一份工作。</p><p>虽然他们也常常说 leader 不好当,压力大事情多。但如果真的只是徒增压力和责任,没有其他收益,大概也不会那么多人争破头去抢这样的位置了吧。</p>]]></content>
<summary type="html">
<p>今年在互联网行业工作的大家,想必大概都有所感受。去年还在想方设法招聘应届生的许多公司,今年温度骤降,裁员的裁员,毁 offer 的毁 offer。</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>前端性能优化--容器篇</title>
<link href="https://godbasin.github.io/2022/08/14/front-end-performance-container/"/>
<id>https://godbasin.github.io/2022/08/14/front-end-performance-container/</id>
<published>2022-08-14T01:12:33.000Z</published>
<updated>2022-08-14T01:15:43.533Z</updated>
<content type="html"><![CDATA[<p>前面我们讲了很多前端应用内部的性能优化,实际上除了前端自身,我们还可结合容纳 Web 页面本身的客户端一起做优化。</p><a id="more"></a><p>首先,本文中提到的容器,基本上都是指 Web 页面的宿主,比如浏览器、APP 客户端、小程序,它们提供了 WebView 环境来运行 Web 应用。</p><h1 id="容器性能优化"><a href="#容器性能优化" class="headerlink" title="容器性能优化"></a>容器性能优化</h1><p>由于 Web 应用本身只运行在 WebView 中,而 WebView 的能力又依赖于宿主容器,因此 Web 应用本身很多能力都比较局限。如果宿主容器能配合一起做一些优化,效果要远胜于我们自身做的很多优化效果。</p><p>从性能优化的角度来说,宿主容器主要能提供的能力包括:</p><ul><li>加速页面打开</li><li>加速页面切换</li></ul><h2 id="加速页面打开"><a href="#加速页面打开" class="headerlink" title="加速页面打开"></a>加速页面打开</h2><p>对前端项目来说,我们常常会对首屏打开做很多的优化,包括尽量减少首屏需要的代码、对首屏渲染的内容进行分片等等(参考<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>)。</p><p>即使前端本身优化到极致,对于资源获取、请求数据等这些耗时占比较大的部分,还是存在的。但是如果容器能提供类似的能力,我们就可以将这部分的耗时做优化了,比如:</p><ul><li>提前下载并缓存 Web 相关资源,页面打开时直接获取缓存,比如 HTML/JavaScript/CSS</li><li>提前获取和缓存页面渲染相关的请求资源,页面请求时直接返回,或是直接从缓存中获取</li><li>提前启动 WebView 页面,并加载基础资源</li></ul><h3 id="资源准备"><a href="#资源准备" class="headerlink" title="资源准备"></a>资源准备</h3><p>我们可以在客户端即将打开某个 WebView 页面之前,提前将该页面资源下载下来,由此加快 WebView 页面加载的速度。</p><p>由于资源请求本身也会消耗一定的资源,一般来说会在比较明确使用的场景下才会使用。也就是说用户很可能会点进去该 WebView 页面,基于这样的前提来做资源准备,比如列表页进入详情页,比如底部 TAB 进入的页面等等。</p><p>这些提前下载并临时缓存的资源,可以包括:</p><ul><li>页面加载资源,包括 HTML/CSS/JavaScript 等</li><li>首屏页面内容的请求数据,比如分片数据的首片数据等</li></ul><p>资源预下载要做的时候相对简单,需要注意的是下载后的资源的管理问题,在使用完毕或是不需要的情况下需要及时的清理,如果过多的缓存会占用用户机器的资源。</p><p>其实除了依赖客户端,前端本身也有相关的技术方案,比如说可以使用 PWA 提前请求和缓存页面需要的资源。</p><h3 id="预加载"><a href="#预加载" class="headerlink" title="预加载"></a>预加载</h3><p>在需要的资源已经准备好的前提下,容器还可以提供预加载的能力,包括:</p><ul><li>容器预热:提前准备好 WebView 资源</li><li>资源加载:将已下载的 Web 资源进行加载,比如基础的 HTML/CSS/JavaScript 等资源</li></ul><p>举个例子,小程序中也有对资源预加载做处理。在小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。</p><p>小程序的启动过程也分了两个步骤:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/wxapp-66.jpg" alt></p><ol><li>页面预渲染。这是准备 WebView 页面的过程,由于小程序里是双线程的设计,因此渲染层和逻辑层都会分别进行初始化以及公共库的注入。逻辑层和渲染层是并行进行的,并不会相互依赖和阻塞。</li><li>小程序启动。当用户打开小程序后,小程序开始下载业务代码,同时会在本地创建基础 UI(内置组件)。准备完成后,就会开始注入业务代码,启动运行业务逻辑。</li></ol><p>显然,小程序基础库和环境初始化相关的资源,都被提前内置在 APP 中了,并提前准备好相关的资源,使得用户打开小程序的时候,可以快速地加载页面。除此之外,小程序还提供了预加载的能力,业务方只需要配置提前拉取的资源,微信则可以在启动的过程中,提前将相关的资源拉取回来。</p><p>很多宿主预加载的方案也类似,比如对 WebView 页面做前置的资源下载和加载,当用户点击时尽快地给到用户体验。</p><h2 id="加速页面切换"><a href="#加速页面切换" class="headerlink" title="加速页面切换"></a>加速页面切换</h2><p>除了首次打开页面的加速,在页面切换时我们也可以做很多提速的事情。</p><h3 id="容器预热"><a href="#容器预热" class="headerlink" title="容器预热"></a>容器预热</h3><p>前面讲到,在打开小程序前,其实微信已经提前准备好了一个 WebView 层,由此减少小程序的加载耗时。</p><p>而当这个预备的 WebView 层被使用之后,一个新的 WebView 层同样地会被提前准备好。这样当开发者跳转到新页面时,就可以快速渲染页面了。这个过程也可以理解为容器的前置预热。</p><p>在这个例子中,小程序针对不同的页面使用了不同的 WebView 进行渲染,因此不管是首次打开,还是跳转/切换新页面,都会准备多一个 WebView 用来快速加载。</p><p>但多准备一个 WebView 本身也是对客户端的一种资源消耗,所以其实我们还可以考虑另外一种方案:容器切换。</p><h3 id="容器切换"><a href="#容器切换" class="headerlink" title="容器切换"></a>容器切换</h3><p>容器切换方案指当页面切换时复用同一个 WebView 资源,可以理解为前端单应用类似的方式在 APP 中做资源切换。</p><p>由于需要复用同一个 WebView,因此该方案对资源的管理要求较高,包括:</p><ul><li>对页面应用的生命周期管理完善,自顶向下实现初始化、更新和销毁的能力</li><li>页面切换时,需要及时清理原有逻辑和资源,比如定时器、页面遗留的 UI 和事件监听等</li><li>资源占用、内存泄露等问题,会随着 WebView 复用次数而积累</li></ul><p>要达到不同页面和前端应用之间的资源复用,要求比直接准备一个新的 WebView 容器要高很多。即使是不同的页面,也需要有统一的生命周期管理,约定好页面的一些销毁行为,并能执行到每个模块和组件中。</p><p>但如果项目架构和设计做得好,效果要远胜于容器预热,因为在进行页面切换的时候,很多资源可以直接复用,比如:</p><ul><li>通用的框架库,比如使用了 Vue/React 等前端框架、Antd 等组件库,就可以免去获取和加载这些资源的耗时</li><li>公共库的复用,项目中自行封装的一些工具库,也可以直接复用</li><li>模块复用,通用的模块比如顶部栏、底部栏、工具栏、菜单栏等功能,可以在页面切换时选择性保留,直接省略这部分模块的加载和页面渲染</li></ul><p>看到这里或许有些人会疑惑,如果是这样的话为什么不直接用单页面呢?要知道我们讨论的场景是客户端打开的场景,也就是说 WebView 页面的退出,大多数情况下是会先回到 APP 原生页面中。当用户进入到另外一个 WebView 页面时,才会重新打开 WebView,此时才考虑是用新预热的 WebView,还是直接复用刚才的 WebView。</p><p>总的来说,容器切换是一个设计要求高、副作用强、但优化效果好的方案。</p><h3 id="客户端直出渲染"><a href="#客户端直出渲染" class="headerlink" title="客户端直出渲染"></a>客户端直出渲染</h3><p>在有容器提供资源的基础上,我们还可以在 WebView 页面关闭前,对当前页面做截屏或是 HTML 保存处理。</p><p>在下一次用户进入到相同的页面中时,可以先使用上一次浏览的图片或是页面片段先预览,当页面加载完成后,再将预览部分移除。这种预加载(预览)的方案,由于是客户端提供的直出渲染能力,因此也被称为客户端直出渲染。</p><p>当然,相对于在页面关闭前保存,其实也可以直接实现直出渲染的能力,这样不管是否已经打开过某个页面,都可以通过容器预热时提前计算出直出渲染的内容,当页面打开时直接进行渲染。</p><p>这种方案有一个比较麻烦的地方:当缓存的页面内容发生变化时,需要及时更新直出渲染的内容。</p><p>因此,及时用户并不在页面内,也需要定期去获取最新的资源,并生成直出渲染的内容。当需要预渲染的页面多了,维护这些页面的实时性也需要消耗不少的资源,因此更适用于维护成本较低的页面。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>其实,容器的作用不只是加速页面打开速度,由于结合了原生 APP 的能力,我们甚至可以给 WebView 提供完整的离线加载能力。比如在网络离线的情况下,通过提前将资源下载并缓存,用户依然可以正常访问 APP 里的页面。</p><p>当然,每一项技术方案都是有利有弊,容器提供了更优的能力,也需要消耗一定的资源,我们可以结合自己项目本身的情况来做取舍。</p>]]></content>
<summary type="html">
<p>前面我们讲了很多前端应用内部的性能优化,实际上除了前端自身,我们还可结合容纳 Web 页面本身的客户端一起做优化。</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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--Canvas篇</title>
<link href="https://godbasin.github.io/2022/07/09/front-end-performance-canvas/"/>
<id>https://godbasin.github.io/2022/07/09/front-end-performance-canvas/</id>
<published>2022-07-09T11:00:01.000Z</published>
<updated>2022-07-09T11:03:51.017Z</updated>
<content type="html"><![CDATA[<p>Canvas 渲染在前端应用中的使用场景不算多,但在大多数用到的场景下,也常常需要考虑性能瓶颈。</p><a id="more"></a><p>Canvas 的使用场景可能少一些(比如游戏、复杂图形、复杂排版等),本来想将 Canvas 渲染放在<a href="https://godbasin.github.io/2022/05/15/front-end-performance-render/">《前端性能优化——渲染篇》</a>一起介绍。后来想了下,Canvas 本身有许多优化点,可以结合自己在项目中的一些经验再详细地做介绍。</p><h1 id="Canvas-性能优化"><a href="#Canvas-性能优化" class="headerlink" title="Canvas 性能优化"></a>Canvas 性能优化</h1><p>其实对于 Canvas 的优化,<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas" target="_blank" rel="noopener">WDN</a> 上也有一些介绍。如果你在网上搜索相关内容,或许有许多的优化方向都和本文有些相像。</p><p>这是当然的,因为我们在做 Canvas 优化的时候,也同样会去找业界的方案做调研,结合自身项目的情况再做方案设计。</p><p>那么,这里整理下我了解到以及实践中的一些 Canvas 优化方案吧。</p><h2 id="Canvas-上下文切换"><a href="#Canvas-上下文切换" class="headerlink" title="Canvas 上下文切换"></a>Canvas 上下文切换</h2><p>Canvas 绘制 API 都是在上下文<code>context</code>上进行调用,<code>context</code>不是一个普通的对象,当我们对其赋值的时候,性能开销远大于普通对象。我们可以尝试将每个赋值操作执行一百万次,来看看其耗时:</p><table><thead><tr><th>赋值属性</th><th>耗时(ms)</th><th>耗时(非法赋值)(ms)</th></tr></thead><tbody><tr><td><code>font</code></td><td>200+</td><td>1500+</td></tr><tr><td><code>fillStyle</code></td><td>80+</td><td>800+</td></tr><tr><td><code>strokeStyle</code></td><td>50+</td><td>800+</td></tr><tr><td><code>lineWidth</code></td><td>30+</td><td>500+</td></tr></tbody></table><p>可见,频繁对 Canvas 上下文属性修改赋值是有一定的性能开销的。这是因为当我们调用<code>context.lineWidth = 2</code>时,浏览器会需要立刻地做一些事情,这样在下一次绘制的时候才能以最新的状态绘制。这意味着,在绘制两段不同字体大小的文本的时候,需要设置两次不同的字体,也就是需要进行两次<code>context</code>上下文状态的切换。</p><p>在大多数情况下,我们的 Canvas 绘制内容的样式不会太多。但是在绘制内容数量大、样式多的场景下,我们应该考虑如何减少上下文<code>context</code>的切换。</p><p>可以考虑使用先将相同样式的绘制内容收集起来,结合享元的方式将其维护起来。在绘制的时候,则可以针对每种样式做切换,切换后批量绘制相同样式的所有内容。</p><p>举个例子,我们绘制俄罗斯方块,可以考虑所有方块的信息收集起来,相同样式的放在一个数据中,切换上下文后遍历绘制。比如,边框信息放在一个数组中,背景色相同的放在一个数组中。</p><h2 id="Canvas-拆分"><a href="#Canvas-拆分" class="headerlink" title="Canvas 拆分"></a>Canvas 拆分</h2><p>一般来说,我们在 Canvas 里绘制的内容,都可以根据变更频率来拆分,简称动静分离。</p><p>Canvas 拆分的关键点在于:尽量避免进行不必要的渲染,减少频繁变更的渲染范围。</p><p>比如在游戏中,状态栏(血条、当前关卡说明等)相对动作/动画内容来说,这部分内容的变更不会太频繁,可以将其拆出到一个单独的 Canvas 来做绘制。再假设该游戏有个静态的复杂背景,如果我们每次更新内容都需要重新将这个背景再绘制一遍,显然开销也是不小的,那么这个背景我们也可以用单独的 Canvas 来绘制。</p><p>Canvas 拆分的前提是更新频率的内容分离,而在拆分的时候也有两个小技巧:</p><ol><li>根据绘制范围拆分。</li><li>根据堆叠层次关系拆分。</li></ol><h3 id="绘制范围的拆分"><a href="#绘制范围的拆分" class="headerlink" title="绘制范围的拆分"></a>绘制范围的拆分</h3><p>绘制范围的拆分要怎么理解呢?简单说就是将画布划分不同的区域,然后根据不同的区域更新频率,来进行 Canvas 拆分。</p><p>举个例子,假设我们现在需要实现 Web 端 VsCode,而整个界面都是由 Canvas 绘制(当然这样不大合理,这里假设只是为了更好地举例)。</p><p>我们可以简单地将 VsCode 拆分成几个区域:顶部栏、左侧栏、底部栏、编辑区。显然这个几个区域的变更频率、触发变更的前提都不一致,我们可以将其做拆分。</p><h3 id="堆叠层次的拆分"><a href="#堆叠层次的拆分" class="headerlink" title="堆叠层次的拆分"></a>堆叠层次的拆分</h3><p>如果说绘制范围的拆分是二维角度,那么堆叠层次更像是三维的 y 轴方向的拆分。</p><p>前面提到的游戏画布拆分,其实背景图片便是堆叠在其余内容的下面。我们可以考虑更复杂的场景,比如我们要实现 Web 版的 Excel/Word,那么我们也可考虑按照堆叠顺序来做拆分:背景色、文字、边框线等等。</p><p>对于有堆叠顺序的绘制来说,Canvas 拆分的优化效果更好。因为如果是二维角度的内容,我们可以只擦除和重绘某个 x/y 轴范围的内容就可以。</p><p>但是涉及到绘制内容的堆叠,如果不做 Canvas 的拆分,意味着我们其中任何一个层级的内容变更,都需要将所有层级的内容擦除并且重绘。比如在 Excel 场景下,某个区域的格子背景颜色变更,我们需要将该区域的格子全部擦除,再重新分别绘制背景色、文字、边框线、其他内容等等。</p><p>实际上,结合前面提到的<code>context</code>上下文的性能开销可知,我们在绘制的时候,很可能并不是以单个格子为单位来进行顺序堆叠的绘制,而是整个画布所有格子一起做顺序绘制(意思是,先绘制所有格子的背景色,再绘制所有格子的文字和边框线等等)。</p><p>在这样的情况下,如果没有做 Canvas 堆叠顺序的拆分,意味着每一个小的变更,我们都需要将整个表格的内容进行重绘。</p><h3 id="Canvas-拆分的开销"><a href="#Canvas-拆分的开销" class="headerlink" title="Canvas 拆分的开销"></a>Canvas 拆分的开销</h3><p>需要注意的是,Canvas 本身的维护也会存在一定的开销,并不是说我们拆的越多越好。</p><p>可以根据项目的实际情况,结合 Canvas 拆离后的效果,确定 Canvas 拆分的最终方案。</p><h2 id="离屏渲染"><a href="#离屏渲染" class="headerlink" title="离屏渲染"></a>离屏渲染</h2><p>对于离屏渲染的概念,大多数情况是指:使用一个不可见(或是屏幕外)的 Canvas 对即将渲染的内容的某部分进行提前绘制,然后频繁地将屏幕外图像渲染到主画布上,避免重复生成该部分内容的步骤。</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><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">// 在离屏 canvas 上绘制</span></span><br><span class="line"><span class="keyword">var</span> canvasOffscreen = <span class="built_in">document</span>.createElement(<span class="string">"canvas"</span>);</span><br><span class="line">canvasOffscreen.width = dw;</span><br><span class="line">canvasOffscreen.height = dh;</span><br><span class="line">canvasOffscreen</span><br><span class="line"> .getContext(<span class="string">"2d"</span>)</span><br><span class="line"> .drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在绘制每一帧的时候,绘制这个图形</span></span><br><span class="line">context.drawImage(canvasOffscreen, x, y);</span><br></pre></td></tr></table></figure><h3 id="各种离屏渲染场景"><a href="#各种离屏渲染场景" class="headerlink" title="各种离屏渲染场景"></a>各种离屏渲染场景</h3><p>关于离屏渲染,其实结合不同的使用场景,还可以达到不同的效果。比如:</p><p>(1) 使用离屏 Canvas 提前绘制特定内容。</p><p>这就是前面说到的提前绘制好需要的内容,避免每次重复生成的开销。</p><p>(2) 使用双 Canvas 交替绘制。</p><p>考虑 Canvas 滚动的场景,比如分页绘制,离屏 Canvas 可以提前绘制下一页/下一屏的内容,在切换的时候可以直接使用提前绘制好的内容。</p><p>通过这样的方式,可以加快 Canvas 的绘制,可以理解为预渲染的效果。</p><p>(3) 使用 OffscreenCanvas 达到真正的离屏。</p><p>通过 OffscreenCanvas API,真正地将离屏 Canvas 完整地运行在 worker 线程,有效减少主线程的性能开销。</p><h3 id="OffscreenCanvas-API-能力"><a href="#OffscreenCanvas-API-能力" class="headerlink" title="OffscreenCanvas API 能力"></a>OffscreenCanvas API 能力</h3><p>要达到将 Canvas 运行在 web worker 线程中,需要依赖 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas" target="_blank" rel="noopener">OffscreenCanvas API</a> 提供的能力。</p><blockquote><p>需要注意的是,该 API 同样可以运行在主线程中。即使是在主线程中运行,其开销也比普通 Canvas 要小。</p></blockquote><p><code>OffscreenCanvas</code>提供了一个可以脱离屏幕渲染的 Canvas 对象,可运行在在窗口环境和 web worker 环境。但是该 API 已知具有兼容性问题(比如 Safari 和 IE,以及部分安卓 Webview),需要考虑不兼容情况下的降级方案。关于此能力现有的技术方案和文档较少,可参考:</p><ul><li><a href="https://zhuanlan.zhihu.com/p/34698375" target="_blank" rel="noopener">OffscreenCanvas - 概念说明及使用解析</a></li><li><a href="https://developers.google.com/web/updates/2018/08/offscreen-canvas" target="_blank" rel="noopener">OffscreenCanvas — Speed up Your Canvas Operations with a Web Worker</a></li></ul><p>对于该 API,核心的优势在于:当主线程繁忙时,依然可以通过 OffscreenCanvas 在 worker 中更新画布内容,避免给用户造成页面卡顿的体验。</p><p>除此之外,还可以进一步考虑在兼容性支持的情况下,通过将局部计算运行在 worker 中,减少渲染层的计算耗时,提升渲染层的渲染性能。</p><h2 id="其他-Canvas-优化方式"><a href="#其他-Canvas-优化方式" class="headerlink" title="其他 Canvas 优化方式"></a>其他 Canvas 优化方式</h2><p>上面介绍了几种较大的 Canvas 优化方案,实际上我们在项目中还需要考虑:</p><ul><li>做内容的增量更新渲染,避免频繁地绘制大范围的内容</li><li>避免浮点数的坐标点,浏览器为了达到抗锯齿的效果会做额外的运算,建议用整数取而代之</li><li>使用 CSS transform 代替 Canvas 计算缩放(CSS transforms 使用 GPU,因此速度更快)</li><li>过于复杂的计算逻辑,可以考虑做任务的拆分,避免长时间计算造成页面卡顿</li></ul><p>这里简单提一下增量渲染。</p><h3 id="增量渲染"><a href="#增量渲染" class="headerlink" title="增量渲染"></a>增量渲染</h3><p>增量渲染需要对内容的变更做计算,将变更的内容局限在某个特定范围,从而避免频繁地绘制大范围的内容。</p><p>举个例子,假设我们的画布内容支持向下滚动,那么我们在滚动的时候可以考虑:</p><ul><li>根据滚动的距离,将上一帧可复用的内容做裁剪保存</li><li>在下一帧绘制中,先将上一帧中重复的内容在新的位置绘制</li><li>原有内容绘制完成后,新增的部分内容再进行重新绘制</li></ul><p>通过这样的方式,可以节省掉一部分的内容绘制和生成过程,提升每次渲染的速度。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>使用 Canvas 绘制,我们则脱离了浏览器自身的绘制过程,因此更加要注意性能问题,避免卡顿和耗时较大的计算。</p><p>至于耗时长的计算和卡顿的优化,我会在另外一篇文章中做详细的介绍(参见<a href="https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/">前端性能优化——卡顿篇</a>)。</p><blockquote><p>我有一个游戏梦,Canvas 做游戏应该也很好玩吧。</p></blockquote>]]></content>
<summary type="html">
<p>Canvas 渲染在前端应用中的使用场景不算多,但在大多数用到的场景下,也常常需要考虑性能瓶颈。</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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--卡顿篇</title>
<link href="https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/"/>
<id>https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/</id>
<published>2022-06-04T12:36:25.000Z</published>
<updated>2022-06-04T12:37:54.609Z</updated>
<content type="html"><![CDATA[<p>如果页面中存在耗时较长的计算任务,那么卡顿也是需要关注的一个性能优化点。</p><a id="more"></a><p>前面我有给大家整体地讲过<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>,其实里面已经囊括了大多数场景下的一些性能优化的方向。</p><p>当我们开始讨论卡顿时,往往意味着页面中有较大的逻辑运算,该计算任务耗时太长,阻塞了浏览器的主线程,导致用户的一些操作无法及时响应。因此,我们今天卡顿优化的重点在于如何优化耗时较长的计算。</p><h1 id="卡顿优化"><a href="#卡顿优化" class="headerlink" title="卡顿优化"></a>卡顿优化</h1><p>还是那句话,对于大多数的渲染场景,我们都可以使用浏览器的 Performance 来录制和分析性能问题,Performance 适用于针对某个具体、可复现的问题做分析。</p><p>卡顿问题同样也是,我们可以在火焰图中看到一些长耗时的任务,然后再逐个分析具体的耗时问题出现在哪里,逐一解决。</p><p>这里介绍一些耗时任务的优化方案。</p><h2 id="赋值和取值"><a href="#赋值和取值" class="headerlink" title="赋值和取值"></a>赋值和取值</h2><p>其实大多数情况下,我们都很少会去在意一些变量的取值和赋值。</p><p>但是在一些复杂的计算场景下,比如深层次的遍历中,需要考虑的点就很多很细,比如:</p><ul><li>尽量将不需要执行的逻辑前置,提前判断做<code>return</code></li><li>减少<code>window</code>对象或是深层次对象上的取值,可以将其保存为临时变量使用</li><li>减少不必要的遍历,<code>Array.filter()</code>这种语法也是一次遍历,需要注意</li><li>对复杂数据结构的数据查询,可以考虑优化数据结构</li></ul><p>一些简单的问题,在重复上百万次的计算之后,都会被无数放大。即使是从<code>window</code>对象上获取某个值,然后做计算生成 DOM 这样的操作,如果将它放在多层遍历的最里层去做,同样会造成性能问题。</p><p>如果你的项目中有使用 Canvas,且重度依赖画布绘制,你会发现 ctx 的上下文切换开销也不低,后面也会单独对 Canvas 的一些性能问题做补充说明。</p><p>这也告诉我们,平时的代码习惯也要好,比如副作用、全局对象等,都可以考虑做更好的设计。</p><h2 id="优化计算性能-内存"><a href="#优化计算性能-内存" class="headerlink" title="优化计算性能/内存"></a>优化计算性能/内存</h2><p>除了上面提到的一些基础场景(比如取值赋值),很多时候我们提升计算性能,还依赖于使用更好的算法和数据结构。</p><p>其实大多数时候,前端都很少涉及到算法和数据结构相关的设计,但是在极端复杂的场景下,也需要考虑做一些优化。</p><p>讲一个经典例子,在 VSCode 的 1.21 发布版本中包含了一项重大改进:<a href="https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation" target="_blank" rel="noopener">全新的文本缓冲区实现</a>,在内存和速度方面都有大幅的性能提升。</p><p>在这次优化中,VSCode 引入了红黑树的数据结构,替代了原有的线性阵列,优化了内存避免了内存爆炸,同时也优化了查询的时间复杂度。</p><p>其实,除了计算耗时过长,如果出现内存占用过多的情况下,同样会造成浏览器频繁的 GC。如果你有仔细观察 Performance,便会发现浏览器的 GC 本身也需要不小的耗时。</p><p>所以,我们还需要时常关注内存情况,考虑:</p><ul><li>使用享元的方式来优化数据存储,减少内存占用</li><li>及时地清理不用的资源,比如定时器</li><li>避免内存泄露等问题</li></ul><h2 id="大任务拆解"><a href="#大任务拆解" class="headerlink" title="大任务拆解"></a>大任务拆解</h2><p>对于一些计算耗时较长的任务,我们可以考虑将任务做拆解,分成一个个的小任务,做异步执行。</p><p>比如,考虑将任务执行耗时控制在 50 ms 左右。每执行完一个任务,如果耗时超过 50 ms,将剩余任务设为异步,放到下一次执行,给到页面响应用户操作和更新渲染的时间。</p><p>我们都知道 React 框架有使用虚拟 DOM 的设计。实际上,虽然虚拟 DOM 解决了页面被频繁更新和渲染带来的性能问题,但传统虚拟 DOM 依然有以下性能瓶颈:</p><ul><li>在单个组件内部依然需要遍历该组件的整个虚拟 DOM 树</li><li>在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费</li><li>递归遍历和更新逻辑容易导致 UI 渲染被阻塞,用户体验下降</li></ul><p>对此,React 中还设计了协调器(Reconciler)与渲染器(Renderer)来优化页面的渲染性能。而在 React16 中,还新增了调度器(Scheduler)。</p><p>调度器能够把可中断的任务切片处理,能够调整优先级,重置并复用任务。调度器会根据任务的优先级去分配各自的过期时间,在过期时间之前按照优先级执行任务,可以在不影响用户体验的情况下去进行计算和更新。通过这样的方式,React 可在浏览器空闲的时候进行调度并执行任务。</p><p>这便是将大任务做拆解方案中,很好的一个例子。</p><h2 id="其他计算优化"><a href="#其他计算优化" class="headerlink" title="其他计算优化"></a>其他计算优化</h2><p>除了上述的一些优化方案,我们还可以考虑:</p><p>(1) 使用 Web Worker。</p><p>如今 Web Worker 已经是前端应用中比较常用的一个能力了,对于一些耗时较长、相对独立的计算任务,我们可以使用 Web Worker 来进行计算。</p><p>当然,由于这些计算任务已经不在主线程了,那么通信的耗时、数据的同步、Worker 兼容性等问题也需要考虑,做好兜底和兼容方案,保证核心能力的使用。</p><p>(2) 使用 WebAssembly。</p><p>WebAssembly 的运行性能接近原生,因此在许多计算耗时的场景上会被使用来优化,比如文件上传、文件/视频内容识别等等。</p><p>(3) 使用 AOT 技术。</p><p>使用 AOT 技术,通过将计算过程提前,减少计算等待时长。</p><p>举个例子,在 Angular 框架中,提供了预编译(AOT)能力,无须等待应用首次编译,以及通过预编译的方式移除不需要的库代码、减少体积,还可以提早检测模板错误。</p><h1 id="卡顿的监控和定位"><a href="#卡顿的监控和定位" class="headerlink" title="卡顿的监控和定位"></a>卡顿的监控和定位</h1><p>出现卡顿问题的时候,往往难以定位,因为这个时候页面常常已经卡死,无法做更多的调试操作。</p><h2 id="Performance"><a href="#Performance" class="headerlink" title="Performance"></a>Performance</h2><p>定位一个页面的运行是否有卡顿,最简单又直接的方式是录制 Performance。Performance 会把耗时长的任务直接标记为红色,我们可以根据这些任务,查找和分析具体产生耗时的脚本是哪些,然后去做优化。</p><p>但是,Performance 仅对开发者来说比较方便,在真实用户的使用场景里,未必有条件能提供 Performance 的录制。更多的时候,我们只能粗略地监控用户的卡顿情况,发现这样的场景,并尝试去解决。</p><h2 id="requestAnimationFrame"><a href="#requestAnimationFrame" class="headerlink" title="requestAnimationFrame"></a>requestAnimationFrame</h2><p>一般来说我们监控卡顿,可以考虑使用<code>window.requestAnimationFrame</code>方法。该方法会在绘制下一帧绘制前被调用,这意味着当前的同步计算任务即将结束。</p><p>前面也有说到,卡顿大多数是因为长耗时的计算任务导致的。那么,我们就可以考虑在某个函数执行之前记下时间戳,而在<code>window.requestAnimationFrame</code>的时候再取其中的时间差,判断当前函数的执行耗时是否合理。</p><p>当然,该方案并不是完全准确,因为我们常常会在一个函数中间调用另外一个函数,还可能会同步抛出事件通知,执行其他的计算任务。</p><p>不过,考虑到真实的线上用户里无法直接使用 Performance,这也算是一个能做卡顿监控的方案。我们可以配合日志、其他不同的监控和上报等,来做更多的问题定位。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>大多数的卡顿场景,都是由于页面渲染掉帧导致的。因此针对页面的更新渲染,不管是 DOM 渲染还是 Canvas 渲染,需要注意将帧率保持在 50~60 FPS 的范围内,这样用户的体验会流程很多。</p><p>当然,如果我们的代码里写了死循环,造成页面直接卡死了,也是卡顿的一种情况,但这就又是另外一个故事了。</p><blockquote><p>愿天下所有的开发同学不再遇到卡顿~</p></blockquote>]]></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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--渲染篇</title>
<link href="https://godbasin.github.io/2022/05/15/front-end-performance-render/"/>
<id>https://godbasin.github.io/2022/05/15/front-end-performance-render/</id>
<published>2022-05-15T05:46:21.000Z</published>
<updated>2022-05-15T05:49:51.417Z</updated>
<content type="html"><![CDATA[<p>对于内容复杂和变更频繁的前端应用,页面渲染也常常是性能优化的核心场景。</p><a id="more"></a><p>前面我有给大家整体地讲过<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–方案归纳篇》</a>,其实里面已经囊括了大多数场景下的一些性能优化的方向。关于加载流程相关的优化,也有在<a href="https://godbasin.github.io/2022/04/09/front-end-performance-startup/">《前端性能优化–加载流程篇》</a>一文中进行详细的介绍。</p><p>本文主要围绕页面渲染相关的内容,来进行性能优化分析。</p><h1 id="首屏渲染"><a href="#首屏渲染" class="headerlink" title="首屏渲染"></a>首屏渲染</h1><p>说到页面渲染,首屏的渲染显然是最首要的。其实前面在归纳篇也有介绍,首屏加载优化核心点在于:<strong>将页面内容尽快展示给用户,减少页面白屏时间。</strong></p><p>首屏渲染包括了首屏内容的加载和渲染两个过程。</p><h2 id="首屏内容加载"><a href="#首屏内容加载" class="headerlink" title="首屏内容加载"></a>首屏内容加载</h2><p>对于首屏加载过程,我们可以通过以下方式进行优化:</p><ul><li>使用骨架屏进行预渲染</li><li>对页面进行分片/分屏加载,将页面可见/可交互时间提前</li><li>优化资源加载的顺序和粒度,仅加载需要的资源,通过异步加载方式加载剩余资源</li><li>使用差异化服务,比如读写分离,对于不同场景按需加载所需要的模块</li><li>使用服务端直出渲染,减少页面二次请求和渲染的耗时</li><li>使用秒看技术,通过预览的方式(比如图片)提前将页面内容提供给用户</li><li>配合客户端进行资源预请求和预加载,比如使用预热 Web 容器</li><li>配合客户端将资源和数据进行离线,可用于下一次页面的快速渲染</li></ul><p>这里提到了很多的方向,但是否每个优化点都适用于自身的项目中,需要结合项目本身做调研和验证。举个简单的例子,最后两条优化点明显是基于有自研客户端的前提下,需要配合客户端一起做优化才可以实现。</p><p>实际上,对于首屏内容的优化,前端开发在项目中更常用的点是骨架屏、数据分片/分屏加载、SSR DOM 直出渲染这几种,因为这几个优化点相对来说方向明确、效果明确、实现相对简单。如果是想要对项目做差异化服务、做资源的拆分和优化,则可能随着项目的复杂度增加,方案难度提升、实现成本也增长。</p><h2 id="首屏内容渲染"><a href="#首屏内容渲染" class="headerlink" title="首屏内容渲染"></a>首屏内容渲染</h2><p>对于首屏内容渲染的过程,更多时候我们是指浏览器渲染 HTML 的过程。该过程可以优化的点也是我们常常提及的,浏览器渲染页面的优化过程,比如:</p><ul><li>将 CSS 放在<code><head></code>里,可用来避免浏览器渲染的重复计算</li><li>将 JavaScript 脚本放在<code><body></code>的最后面,避免资源阻塞页面渲染</li><li>减少 DOM 数量,减少浏览器渲染过程中的计算耗时</li><li>通过合理使用浏览器 GPU 合成,提升浏览器渲染效率</li></ul><p>以上这些,是我们在做首屏渲染时考虑渲染过程的优化点。虽然这些优化点属于前端基础和共识,也常常会出现在基础面试中。</p><p>很多时候我们为了准备面试而学习了很多的知识和原理,却容易在将知识和实践结合的过程中忘记。越是基础和简单的点,反而往往会在实际写代码的时候被忽略,直到性能出现了问题,这些基础的优化点才会被注意到。</p><p>当然,首屏性能的提升,除了渲染相关的,也还有上一篇我们提到的<a href="https://godbasin.github.io/2022/04/09/front-end-performance-startup/">加载流程相关的优化</a>。</p><h1 id="页面更新"><a href="#页面更新" class="headerlink" title="页面更新"></a>页面更新</h1><p>除了首屏内容需要尽快加载和渲染以外,当页面内容需要更新的时候,我们也需要尽可能地减少更新内容渲染的耗时。</p><p>一般来说,页面更新场景我们常常会关注用户操作和页面渲染。</p><h2 id="用户操作"><a href="#用户操作" class="headerlink" title="用户操作"></a>用户操作</h2><p>页面内容的更新,一般有两种情况:</p><ol><li>用户自身操作(点击、输入、拖拽等)的页面响应。</li><li>实时内容的变更(比如聊天室的消息提醒、弹幕等等)。</li></ol><p>如果是用户自身的操作,则我们需要及时地更新页面内容,让用户感受到操作生效了。该过程应该是优先级最高的,一般需要同步进行。因为如果有别的任务在执行而导致主线程阻塞,就容易造成页面卡顿的体验。关于卡顿相关的,我会另外再起一篇文章介绍,这里就不过多展开啦。</p><p>至于实时内容的变更,优先级更多会比用户操作稍微低一些,也基本上都是异步进行的。我们还可以考虑对变更内容做合并、批量更新,也可以考虑定时拉取最新内容更新的方式。</p><h3 id="事件委托"><a href="#事件委托" class="headerlink" title="事件委托"></a>事件委托</h3><p>对于用户交互频繁的场景,我们还得注意事件的绑定。相信很多人都了解过事件委托,如果在列表数量内容较大的时候,对成千上万节点进行事件监听,也是不小的性能消耗。使用事件委托的方式,通过将事件绑定在父元素上,我们可以大量减少浏览器对元素的监听,也是在前端性能优化中比较简单和基础的一个做法。</p><p>事件委托是很常见的优化方式,需要注意的是,如果我们直接在<code>document.body</code>上进行事件委托,可能会带来额外的问题。由于浏览器在进行页面渲染的时候会有合成的步骤,合成的过程会先将页面分成不同的合成层,而用户与浏览器进行交互的时候需要接收事件。</p><p>如果我们在<code>document.body</code>上被绑定了事件,这时候整个页面都会被标记。即使我们的页面不关心某些部分的用户交互,合成器线程也必须与主线程进行通信,并在每次事件发生时进行等待。此时可以使用<code>passive: true</code>选项来解决。</p><h2 id="页面渲染"><a href="#页面渲染" class="headerlink" title="页面渲染"></a>页面渲染</h2><p>我们在页面内容更新的时候,一般也可以考虑以下优化点:</p><ul><li>减少/合并 DOM 操作,减少页面更新的内容范围,减少浏览器渲染过程中的计算耗时</li><li>对于页面动画,可以使用 CSS transition 能力,减少 DOM 属性的修改</li><li>使用资源预加载,在空闲时间,提前将用户可能需要用到的资源进行获取并加载(比如下一页的内容)</li></ul><h3 id="DOM-操作合并"><a href="#DOM-操作合并" class="headerlink" title="DOM 操作合并"></a>DOM 操作合并</h3><p>说到 DOM 操作的合并和减少,目前大多数前端框架都提供了虚拟 DOM 的能力(比如 Vue 和 React)。虚拟 DOM 本身就有对 DOM 操作和更新做优化,通过使用 JavaScript 对象模拟 DOM 元素,并在页面需要更新时对更新的部分做 DOM Diff,尽可能地减少内容的更新频率和范围。</p><p>虽然现在大多数前端项目都离不开前端框架,也正因为这些框架本身已经做了很多的优化,所以我们常常会忘记和忽略掉这些注意事项。</p><p>但也从侧面论证了,即使是很基础的优化点也需要重视,即使是简单的优化点也可以做出很棒的设计。</p><h3 id="页面滚动渲染"><a href="#页面滚动渲染" class="headerlink" title="页面滚动渲染"></a>页面滚动渲染</h3><p>考虑到页面滚动的场景,可能会出现性能问题的地方常常是长列表/页面的渲染。</p><p>由于页面内容过多,页面的 DOM 元素数量也很多,容易造成页面渲染的卡顿。在这样的情况下,我们可以考虑仅渲染可见区域的部分,比如页面内容超出滚动范围之外,就可以进行销毁,将页面的 DOM 数量保持在一定范围内。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>本文主要围绕页面渲染和更新的过程,介绍了一些性能优化的方向。其实如果你有注意到,就会发现本文的内容大多数还是基础和简单的前端知识点。</p><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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--加载流程篇</title>
<link href="https://godbasin.github.io/2022/04/09/front-end-performance-startup/"/>
<id>https://godbasin.github.io/2022/04/09/front-end-performance-startup/</id>
<published>2022-04-09T13:53:02.000Z</published>
<updated>2022-05-15T05:49:38.864Z</updated>
<content type="html"><![CDATA[<p>对于前端应用的性能优化,大多数时候我们都是从加载流程开始优化起。</p><a id="more"></a><p>前面我有给大家整体地讲过<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>,其实里面已经囊括了大多数场景下的一些性能优化的方向。</p><p>越是交互复杂、用户量大的业务,对性能的要求就越是严格。大多数的前端性能优化,都是从页面的启动和加载流程开始梳理和定位,对于功能复杂的业务来说,这样的梳理尤为重要。</p><blockquote><p>注意:前面说过性能优化分为时间和空间两个角度,本文中提及的性能优化更多是指时间角度(即耗时)的优化。</p></blockquote><h1 id="常见的页面加载流程"><a href="#常见的页面加载流程" class="headerlink" title="常见的页面加载流程"></a>常见的页面加载流程</h1><p>其实我们在性能优化的归纳篇有简单说过,页面加载的过程其实跟我们常常提起的浏览器页面渲染流程几乎一致:</p><ol><li>网络请求,服务端返回 HTML 内容。</li><li>浏览器一边解析 HTML,一边进行页面渲染。</li><li>解析到外部资源,会发起 HTTP 请求获取,加载 Javascript 代码时会暂停页面渲染。</li><li>根据业务代码加载过程,会分别进入页面开始渲染、渲染完成、用户可交互等阶段。</li><li>页面交互过程中,会根据业务逻辑进行逻辑运算、页面更新。</li></ol><p>那么,我们可以针对其中的每个步骤做优化,主要包括:资源获取、资源加载、页面可见、页面可交互。</p><h2 id="资源获取"><a href="#资源获取" class="headerlink" title="资源获取"></a>资源获取</h2><p>资源获取主要可以围绕两个角度做优化:</p><ul><li>资源大小</li><li>资源缓存</li></ul><h3 id="资源大小"><a href="#资源大小" class="headerlink" title="资源大小"></a>资源大小</h3><p>一般来说,前端都会在打包的时候做资源大小的优化,资源类型包括 HTML、JavaScript、CSS、图片等。优化的方向包括:</p><p>(1) 合理的对资源进行分包。</p><p>首次渲染时只保留当前页面渲染需要的资源,将可以异步加载、延迟加载的资源拆离。通常我们会在代码编译打包的时候做处理,比如<a href="https://webpack.docschina.org/guides/code-splitting/" target="_blank" rel="noopener">使用 Webpack 将代码拆到不同的 bundle 包中</a>。</p><p>(2) 移除不需要的代码。</p><p>我们项目中常常会引入许多开源代码,同时我们自己也会实现很多的工具方法,但是实际上并不是全部相关的代码都是最终需要执行的代码,所以我们可以在打包的时候移除不需要的代码。现在基本大多数的打包工具都提供了类似的能力,比如 Tree-shaking。</p><p>除此之外,如果我们的项目较大,使用和依赖了多个不同的仓库。如果在不同的代码仓库里,都依赖了同样的 npm 代码包,那么我们可能会遇到打包时引入多次同样的 npm 包的情况。一般来说,我们在管理依赖包的时候,可以使用<code>peerDependency</code>来进行管理,避免多次安装依赖、以及版本不一致导致的多次打包和安装等情况。</p><p>(3) 资源压缩和合并。</p><p>代码压缩也常常是在打包阶段进行的,包括 JavaScript 和 CSS 等代码,在一些情况下也可以使用图片合并(雪碧图的生成)。通常也是使用的打包工具以及插件自带的压缩能力,开启压缩后的代码可能比较难定位,可以配合 Sorce Mapping 来进行问题定位。</p><p>除了打包时的压缩,我们在页面加载的时候也可以启用 HTTP 的 gzip 压缩,可以减少资源 HTTP 请求的耗时。</p><h3 id="资源缓存"><a href="#资源缓存" class="headerlink" title="资源缓存"></a>资源缓存</h3><p>资源缓存的优化,其实更多时候跟我们的资源获取的链路有关,包括:</p><ul><li>减少 DNS 查询时间,比如使用浏览器 DNS 缓存、计算机 DNS 缓存、服务器 DNS 缓存</li><li>合理地使用 CDN 资源,有效地减少网络请求耗时</li><li>对请求资源进行缓存,包括但不限于使用浏览器缓存、HTTP 缓存、后台缓存,比如使用 Service Worker、PWA 等技术</li></ul><p>其实,我们观察资源获取的链路,获取除了大小和缓存的角度以外,还可以做更多的优化,比如:</p><ul><li>使用 HTTP/2、HTTP/3,提升资源请求速度</li><li>对请求进行优化,比如对多个请求进行合并,减少通信次数</li><li>对请求进行域名拆分,提升并发请求数量</li></ul><h2 id="资源加载"><a href="#资源加载" class="headerlink" title="资源加载"></a>资源加载</h2><p>资源加载步骤中,我们一般也有以下的优化角度:</p><ul><li>加载流程拆分</li><li>资源懒加载</li><li>资源预加载</li></ul><h3 id="加载流程拆分"><a href="#加载流程拆分" class="headerlink" title="加载流程拆分"></a>加载流程拆分</h3><p>页面的加载过程,常常分为两个阶段:页面可见、页面可交互。</p><p>前面我们讲了对资源做拆分,在页面启动加载的时候仅加需要的资源,拆分的过程则可以结合上述的两个阶段来做处理。</p><p>(1) 页面可见。</p><p>页面可见可以分为部分可见以及内容完全可见。</p><p>对于部分可见,一般来说可以做 loading 的展示或是直出,让用户知道页面正在加载中,而非无响应。</p><p>对于内容完全可见,则是用户可视区域内的内容完全渲染完毕。除此之外,当前可视范围以外的内容,则可以拆离出首屏的分包,通过预加载或是懒加载的方式进行异步加载。</p><p>(2) 页面可交互。</p><p>同样的,页面可交互也可以分为部分可交互以及完全可交互。</p><p>一般来说,组件的样式渲染仅需要 HTML 和 CSS 加载完成即可,而组件的功能则可能需要加载具体的功能代码。对于复杂或是依赖资源较多的功能,加载的耗时可能相对较长。在这样的情况下,我们可以选择将该部分的资源做异步加载。</p><p>在初始的内容加载完毕之后,剩下的资源需要延迟加载。对于页面功能完全可交互,同样依赖于分包资源延迟加载。加载流程的优化,不管是页面可见,还是页面可交互,都离不开延迟加载。</p><p>延迟加载可分为两种方式进行加载:懒加载和预加载。因此,资源懒加载和预加载也是加载流程中很重要的一部分。</p><h3 id="资源懒加载"><a href="#资源懒加载" class="headerlink" title="资源懒加载"></a>资源懒加载</h3><p>我们常说的懒加载其实又被称为按需加载,顾名思义就是需要用到的时候才会进行加载。通过将非必要功能进行懒加载的方式,可以有效地减少页面的初始加载速度,提升页面加载的性能。</p><p>常见的场景比如某些组件在渲染时不具备完整的功能,当用户点击的时候,才进行对应逻辑的获取和加载。遇到点击时未加载完成的情况下,可以通过适当的方式提示用户功能正在加载中。</p><p>资源懒加载常常也是跟资源分包一起进行,大多数前端框架(比如 Vue、React、Angular)也都提供了懒加载的能力,也可以<a href="https://webpack.docschina.org/guides/lazy-loading/" target="_blank" rel="noopener">配合 Webpack 打包</a>做处理。</p><h3 id="资源预加载"><a href="#资源预加载" class="headerlink" title="资源预加载"></a>资源预加载</h3><p>资源预加载也称为闲时加载,很多时候我们可以在页面空闲的时候,对一些用户可能会用到的资源做提前加载,以加快后续渲染或者操作的时间。</p><p>仔细一看,资源预加载和资源懒加载都比较相似,都会通过将资源拆离的方式做成异步延迟的方式加载。两者的区别在于:</p><ul><li>懒加载的功能只会在需要的时候才进行加载,因为一些功能用户可能不会使用到,比如帮助中心、反馈功能等等</li><li>预加载的功能则是在不阻塞核心功能的时候,尽可能利用空闲的资源提前加载,这部分的功能则是用户很可能会使用到,比如获取下一屏页面的内容数据</li></ul><h1 id="复杂场景下的加载流程"><a href="#复杂场景下的加载流程" class="headerlink" title="复杂场景下的加载流程"></a>复杂场景下的加载流程</h1><p>在页面到达可交互状态之后,后续的加载流程也可以根据业务场景做后续的优化。对于一些复杂的业务,我们可以结合业务的特点做更进一步的性能优化。</p><h2 id="复杂加载流程管理"><a href="#复杂加载流程管理" class="headerlink" title="复杂加载流程管理"></a>复杂加载流程管理</h2><p>对于页面初始化流程过于复杂的应用来说,我们可以对加载流程做任务的拆分,分阶段地进行加载。</p><p>举个例子,假设我们需要在 Web 端加载 VsCode,那么我们可能需要考虑以下各个功能的加载:</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><span class="line">10</span><br><span class="line">11</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><span class="line">- 搜索功能</span><br><span class="line">- 插件功能</span><br></pre></td></tr></table></figure><p>以上只是我按照自己想法粗略拆分的功能,我们可以简单分成几个加载阶段:</p><ol><li>页面整体框架加载完成。此时可以看到各个功能区域的分布,包括顶部菜单栏、左侧工具栏、底部状态栏、项目内容区域等等,但这些区域的内容未必都完全加载完成。</li><li>通用功能加载完成。比如顶部菜单栏、左侧工具栏、底部状态栏等等,一些具体的菜单或是工具的功能可以做按需加载和预加载,比如搜索功能。</li><li>项目内容相关框架加载完成。此时可以看到项目相关的内容区域,比如文件目录、当前文件的内容详情等等。</li><li>插件功能。用户安装的插件,在核心功能都加载完成之后再获取和加载。</li></ol><p>当我们根据项目的具体加载过程做了阶段划分之后,则可以将我们的代码做任务拆分,可以拆分成串行和并行的任务。串行的任务比如按照阶段划分的大任务,并行的任务则可以是某个阶段内的小任务,其中也可以包括一些异步执行的任务,或是延迟加载的任务。</p><h2 id="长耗时任务的拆离"><a href="#长耗时任务的拆离" class="headerlink" title="长耗时任务的拆离"></a>长耗时任务的拆离</h2><p>如果我们的应用中会有耗时较长的计算任务,比如拉取回来的数据需要计算处理后才能渲染,那么我们可以对这些耗时较长的任务做任务拆分。</p><p>同样的,我们还是回到 Web 端加载 VsCode 的场景。假设我们在加载某个特别大的文件,则可以考虑分别对该文件的内容获取、数据转换做任务拆分,比如分片获取该文件的内容,根据分片的内容做渲染的计算,计算过程如果耗时较长,也可以做异步任务的拆分,甚至可以结合 Web Worker 和 WebAssembly 等技术做更多的优化。</p><h2 id="读写分离"><a href="#读写分离" class="headerlink" title="读写分离"></a>读写分离</h2><p>对于交互复杂、需要加载的资源较多的情况下,如果用户的权限只是可读,那么对于编辑相关的功能可以做资源拆离,对于有权限的用户才进行编辑能力的加载。</p><p>读写分离其实属于资源拆分的一种具体场景,我们可以结合业务的具体场景做具体的功能拆分,比如管理员权限相关的管理功能,也是类似的优化场景。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--归纳篇</title>
<link href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/"/>
<id>https://godbasin.github.io/2022/03/06/front-end-performance-optimization/</id>
<published>2022-03-06T03:24:13.000Z</published>
<updated>2022-04-09T13:55:48.299Z</updated>
<content type="html"><![CDATA[<p>对于前端开发来说,性能优化老生常谈了。不管是日常工作中,还是涉及到晋级答辩,性能都是频繁被我们提及的一个话题。</p><p>性能优化不是一劳永逸的解决方案,项目在发展过程,代码不断地迭代和变更。我们在某个阶段优化过的代码,过段时间性能又会慢慢下降,这也是前端开发常把性能挂在嘴边的原因。</p><a id="more"></a><p>当页面加载时间过长、交互操作不流畅时,会给用户带来很糟糕的体验。越是使用时间越长的产品,用户对体验的要求越高,如果出现卡顿或是加载缓慢,最坏的情况下会导致用户的流失。</p><p>对于性能优化,其实解决方案也比较常见和通用了,但是基本上也只有指导思想,实施起来还得具体项目具体分析。</p><h1 id="常见的性能优化方案"><a href="#常见的性能优化方案" class="headerlink" title="常见的性能优化方案"></a>常见的性能优化方案</h1><p>对于前端应用来说,网络耗时、页面加载耗时、脚本执行耗时、渲染耗时等耗时情况会影响用户的等待时长,而 CPU 占用、内存占用、本地缓存占用等则可能会导致页面卡顿甚至卡死。</p><p>因此,性能优化可以分别从<strong>耗时和资源占用</strong>两方面来解决,我个人也比较喜欢将其称为“时间”和“空间”两个维度。</p><h2 id="时间角度优化:减少耗时"><a href="#时间角度优化:减少耗时" class="headerlink" title="时间角度优化:减少耗时"></a>时间角度优化:减少耗时</h2><p>我们知道浏览器在页面加载过程中,会进行以下的步骤:</p><ul><li>网络请求相关(发起 HTTP 请求从服务端获取页面资源,包括 HTML/CSS/JS/图片资源等)</li><li>浏览器解析 HTML 和渲染页面</li><li>加载 Javascript 代码时会暂停页面渲染(包括解析到外部资源,会发起 HTTP 请求获取并加载)</li></ul><p>在浏览器的首次加载和渲染完成之后,不代表用户就可以马上交互和操作。根据业务代码加载过程,页面还会分别进入页面开始渲染、渲染完成、用户可交互等阶段。除此之外,页面交互过程中,会根据业务逻辑进行逻辑运算、页面更新。</p><blockquote><p>题外话:为什么我们常常说要理解原理呢?性能优化便是个很好的例子,如果你不知道这个过程具体发生了什么,就很难找到地方下手去进行优化。</p></blockquote><p>根据这个过程,我们可以从四个方面进行耗时优化:</p><ol><li>网络请求优化。</li><li>首屏加载优化。</li><li>渲染过程优化。</li><li>计算/逻辑运行提速。</li></ol><p>在前端性能优化实践中,网络请求优化和首屏加载优化方案使用频率最高,因为不管项目规模如何、各个模块和逻辑是否复杂,这两个方向的耗时优化方案都是比较通用的。相比之下,对于页面内容较多、交互逻辑/运算逻辑复杂的项目,才需要针对性地进行渲染过程优化和计算/逻辑运行提速。</p><p>一起来看看~</p><h3 id="1-网络请求优化"><a href="#1-网络请求优化" class="headerlink" title="1. 网络请求优化"></a>1. 网络请求优化</h3><p>网络请求优化的目标在于减少网络资源的请求和加载耗时,如果考虑 HTTP 请求过程,显然我们可以从几个角度来进行优化:</p><ol><li>请求链路:DNS 查询、部署 CDN 节点、缓存等。</li><li>数据大小:代码大小、图片资源等。</li></ol><p>对于请求链路,核心的方案常常包括使用缓存,比如 DNS 缓存、CDN 缓存、HTTP 缓存、后台缓存等等,前端的话还可以考虑使用 Service Worker、PWA 等技术。使用缓存并非万能药,很多使用由于缓存的存在,我们在功能更新修复的时候还需要考虑缓存的情况。除此之外,还可以考虑使用 HTTP/2、HTTP/3 等提升资源请求速度,以及对多个请求进行合并,减少通信次数;对请求进行域名拆分,提升并发请求数量。</p><p>数据大小则主要考对请求资源进行合理的拆分(CSS、Javascript 脚本、图片/音频/视频等)和压缩,减少请求资源的体积,比如使用 Tree-shaking、代码分割、移除用不上的依赖项等。</p><p>在请求资源返回后,浏览器会进行解析和加载,这个过程会影响页面的可见时间,通过对首屏加载的优化,可有效地提升用户体验。</p><h3 id="2-首屏加载优化"><a href="#2-首屏加载优化" class="headerlink" title="2. 首屏加载优化"></a>2. 首屏加载优化</h3><p>首屏加载优化核心点在于两部分:</p><ol><li>将页面内容尽快地展示给用户,减少页面白屏时间。</li><li>将用户可操作的时间尽量提前,避免用户无法操作的卡顿体验。</li></ol><p>减少白屏时间除了我们常说的首屏加载耗时优化,还可以考虑使用一些过渡的动画,让用户感知到页面正在顺利加载,从而避免用户对于白屏页面或是静止页面产生烦躁和困惑。除了技术侧的优化,很多时候产品策略的调整,给用户带来的体验优化效果不低于技术手段优化,因此我们也需要重视。</p><p>整体的优化思路包括:尽可能提前页面可见,以及将用户可交互的时间提前。一般来说,我们需要尽可能地降低首屏需要的代码量和执行耗时,可以通过以下方式进行:</p><ul><li>对页面的内容进行分片/分屏加载</li><li>仅加载需要的资源,通过异步或是懒加载的方式加载剩余资源</li><li>使用骨架屏进行预渲染</li><li>使用差异化服务,比如读写分离,对于不同场景按需加载所需要的模块</li><li>使用服务端直出渲染,减少页面二次请求和渲染的耗时</li></ul><p>有些时候,我们的页面也需要在客户端进行展示,此时可充分利用客户端的优势:</p><ul><li>配合客户端进行资源预请求和预加载,比如使用预热 Web 容器</li><li>配合客户端将资源和数据进行离线,可用于下一次页面的快速渲染</li><li>使用秒看技术,通过生成预览图片的方式提前将页面内容提供给用户</li></ul><p>除了首屏渲染以外,用户在浏览器页面过程中,也会触发页面的二次运算和渲染,此时需要进行渲染过程的优化。</p><h3 id="3-渲染过程优化"><a href="#3-渲染过程优化" class="headerlink" title="3. 渲染过程优化"></a>3. 渲染过程优化</h3><p>渲染过程的优化要怎么定义呢?我们可以将其理解为首屏加载完成后,用户的操作交互触发的二次渲染。</p><p>主要思路是减少用户的操作等待时间,以及通过将页面渲染帧率保持在 60FPS 左右,提升页面交互和渲染的流畅度。包括但不限于以下方案:</p><ul><li>使用资源预加载,提升空闲时间的资源利用率</li><li>减少/合并 DOM 操作,减少浏览器渲染过程中的计算耗时</li><li>使用离屏渲染,在页面不可见的地方提前进行渲染(比如 Canvas 离屏渲染)</li><li>通过合理使用浏览器 GPU 能力,提升浏览器渲染效率(比如使用 css transform 代替 Canvas 缩放绘制)</li></ul><p>以上这些,是对常见的 Web 页面渲染优化方案。对于运算逻辑复杂、计算量较大的业务逻辑,我们还需要进行计算/逻辑运行的提速。</p><h3 id="4-计算-逻辑运行提速"><a href="#4-计算-逻辑运行提速" class="headerlink" title="4. 计算/逻辑运行提速"></a>4. 计算/逻辑运行提速</h3><p>计算/逻辑运行速度优化的主要思路是“拆大为小、多路并行”,方式包括但不限于:</p><ul><li>通过将 Javscript 大任务进行拆解,结合异步任务的管理,避免出现长时间计算导致页面卡顿的情况</li><li>将耗时长且非关键逻辑的计算拆离,比如使用 Web Worker</li><li>通过使用运行效率更高的方式,减少计算耗时,比如使用 Webassembly</li><li>通过将计算过程提前,减少计算等待时长,比如使用 AOT 技术</li><li>通过使用更优的算法或是存储结构,提升计算效率,比如 VSCode 使用红黑树优化文本缓冲区的计算</li><li>通过将计算结果缓存的方式,减少运算次数</li></ul><p>以上便是<strong>时间</strong>维度的性能优化思路,还有<strong>空间</strong>维度的资源优化情况。</p><h2 id="空间角度优化:降低资源占用"><a href="#空间角度优化:降低资源占用" class="headerlink" title="空间角度优化:降低资源占用"></a>空间角度优化:降低资源占用</h2><p>提到性能优化,大多数我们都在针对页面加载耗时进行优化,对资源占用的优化会更少,因为资源占用常常会直接受到用户设备性能和适应场景的影响,大多数情况下优化效果会比耗时优化局限,因此这里也只能说一些大概的思路。</p><p>资源占用常见的优化方式包括:</p><ul><li>合理使用缓存,不滥用用户的缓存资源(比如浏览器缓存、IndexDB),及时进行缓存清理</li><li>避免存在内存泄露,比如尽量避免全局变量的使用、及时解除引用等</li><li>避免复杂/异常的递归调用,导致调用栈的溢出</li><li>通过使用数据结构享元的方式,减少对象的创建,从而减少内存占用</li></ul><p>说到底,我们在做性能优化的时候,其实很多情况下会依赖时间换空间、空间换时间等方式。性能优化没有银弹,只能根据自己项目的实际情况做出取舍,选择相对合适的一种方案去进行优化。</p><p>对于页面耗时和资源占用的性能优化分析,大部分情况都可以使用 Chrome 开发者工具进行针对性的分析和优化。</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>实际上,除了遇到问题的时候进行优化,更优的方案是在工作流中搭建一个监控性能指标的步骤,每次变更发布前都跑一遍,发现性能下降之后进行及时的告警,推动开发者解决。对于这块,之前我也有简单描述过,可以参考<a href="https://godbasin.github.io/front-end-playground/front-end-basic/deep-learning/front-end-performance-analyze.html">《补齐 Web 前端性能分析的工具盲点》</a>一文。</p><p>对于性能优化,其实本文只整理和归纳了一些常见的思路,至于实际上在项目中要怎么处理和使用,等有空的时候我再来跟大家讲一下~~</p>]]></content>
<summary type="html">
<p>对于前端开发来说,性能优化老生常谈了。不管是日常工作中,还是涉及到晋级答辩,性能都是频繁被我们提及的一个话题。</p>
<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/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端这几年--答辩晋级这件事</title>
<link href="https://godbasin.github.io/2022/02/27/about-updating/"/>
<id>https://godbasin.github.io/2022/02/27/about-updating/</id>
<published>2022-02-27T07:31:34.000Z</published>
<updated>2022-02-27T07:39:55.933Z</updated>
<content type="html"><![CDATA[<p>最近又是答辩季,程序员最讨厌写的 PPT 又到了不得不写的时候了。之前也有在帮一些小伙伴做准备,所以顺便给大家分享一些晋级答辩的思考和技巧吧~</p><a id="more"></a><p>关于答辩晋级这个内容,最开始我是直接做的视频放在 B 站分享,参考<a href="https://www.bilibili.com/video/BV1tu411X7qn/" target="_blank" rel="noopener">《程序员日志–晋级答辩这件事》</a>。</p><blockquote><p>关于做视频和写文章,感觉自己从最初的只会写文章,到现在已经慢慢也会做一些视频了。视频的表达和文章相差很远,我自己的感受是,对于需要反复阅读、技术深度的内容,还是更适合用文章来记录。而视频更适合写一些需要录屏和讲解的模式,加上 PPT 本身的结构化,更容易去给大家梳理清楚逻辑架构,但是很多细节就很难讲清楚了。</p></blockquote><h2 id="如何对待晋级答辩这件事"><a href="#如何对待晋级答辩这件事" class="headerlink" title="如何对待晋级答辩这件事"></a>如何对待晋级答辩这件事</h2><p>对于很多大公司的程序员来说,晋级答辩关乎着是否可以升职加薪,而答辩成功与否常常会对个人的工作态度和心态造成较大的影响。</p><p>我个人的看法是:<strong>认真对待它,但不要过分依赖它。</strong></p><p>这句话怎么理解呢?如果你仔细观察身边其他同事,大多数会分成两类:</p><ol><li>平时工作只是完成工作本身,不希望受到答辩影响,但是到答辩的时候却临时抱佛脚。</li><li>过分看重答辩,在平日工作里就抢一些方便答辩的活,如果没有的话甚至自己造各种轮子,而不在乎这些轮子是否合适。</li></ol><p>以上两种态度都可以改善,我们可以在平时就认真地把手上的每件事做好,而到了答辩的时候也要认真地对待,但是不要因为答辩这件事影响了自己原本该有的工作态度和对待项目质量的要求。</p><p>或许有些人会疑惑,造不合适的轮子,为什么答辩能通过呢?</p><p>其实答辩这件事,本身也有认知偏差和主观因素。由于陈述内容是由答辩人自身提供的,所以很多时候都会只把好的一面呈现出来,而使用的技术栈或是造的一些轮子给原有项目造成的影响,或是带来的技术债务,或许就只有项目内的其他成员知道了。</p><p>除此之外,因为答辩是由评委来评分的,因此主观上如果评委比较感兴趣的内容,会更容易通过;而如果是评委熟悉的领域,则会被问到很深入和核心的问题,这样的可能性会更高。</p><p>所以,更多的时候,我认为答辩是否能通过是很需要运气的,包括我自己通过的几次答辩,都有不小的运气成分在里面。这也是为什么,我想跟大家说不要过分依赖晋级答辩,因为如果你过分看重和孤注一掷,那么不管成功与否,都会对你以后的工作心态产生影响。</p><p>那么,正如我视频里所说的,关于答辩这件事,你需要知道:</p><ol><li>答辩是由 70% 的努力 + 30% 的运气组成的。</li><li>答辩考核的除了工作内容,还有工作方式和答辩技巧。</li><li>答辩是结果,不是目的。</li></ol><p>既然我们还是需要认真对待答辩这件事,该怎么去进行准备呢?</p><h2 id="如何准备答辩"><a href="#如何准备答辩" class="headerlink" title="如何准备答辩"></a>如何准备答辩</h2><p>其实,答辩本身也属于项目复盘的一种方式,所以其实我们在平时工作里,就可以用更优的工作方式和节奏,去把事情做好。</p><h3 id="平时工作要做好"><a href="#平时工作要做好" class="headerlink" title="平时工作要做好"></a>平时工作要做好</h3><p>如果我们在平时工作中,就有认真地思考每一个项目,更加结构化地去关注项目中的每个阶段的话,相比答辩本身能给我们自身带来更多的成长。</p><p>我们会常常看到,需要开发在工作的时候基本上是线性的工作方式,即:遇到问题 -> 解决问题 -> 结束。</p><p>实际上,我们可以在每个问题上思考更多:</p><p><strong>(1) 做一件事的目的,需要贯穿全过程。</strong></p><p>很多时候,我们在遇到一个问题的时候,马上就开始找解决方案了。其实我们可以先暂停,去思考下这个问题是如何产生的,我们需要解决的到底是什么程度的问题,做这件事的目的是什么。</p><p>而在问题处理完成之后,同样需要回顾当初这个问题的目的是否已经达成,是否还遗留有待解决的问题,等等。</p><p><strong>(2) 拓展自身的思维,更加结构化地去做事。</strong></p><p>比如,在寻找解决方案的时候,当我们找到一个解决方向的时候,可以先不着急去马上解决,而是需要考虑是否还有其他解决方案?当前方案是否最优?解决方案是否存在局限?是否有更多的探索可能性?</p><p>充分做好前期调研之后,再对多个方案进行对比,结合自身项目的情况,找到最适合用于项目中的一个解决方案。</p><p><strong>(3) 将一件事情的价值最大化。</strong></p><p>很多时候,我们处理完一个问题,这个事情就结束了。对于团队来说,这样的方式其实效率很低,因为不同的团队成员很可能会遇到相同的问题,如果每个人都花费这些时间去获得差不多的结论,那么团队的成长会很慢。</p><p>我们可以选择将每次处理问题的过程和解决方案进行总结沉淀,然后分享给其他人。这样,团队内就可以共享每个人努力的成果,这对于团队来说成长是很快的,而对团队中的每个人来说亦是如此。</p><p>而沉淀和总结本身,也可以促进个人的成长。在开发的职业生涯是,是否具备这样的能力和认识,是十分关键的。</p><h3 id="答辩-项目内容结构"><a href="#答辩-项目内容结构" class="headerlink" title="答辩/项目内容结构"></a>答辩/项目内容结构</h3><p>对于答辩本身,我们首先要知道:要能让评委认可你的能力,首先得高效地让评委理解项目中的各个过程。</p><p>因此,大多数时候我们的答辩内容都可以分为以下结构:</p><ol><li>项目背景/问题描述。讲清楚做这个项目的背景情况和目的,这是最起码的铺垫。</li><li>难点/挑战点。如果评委感受不到项目中的难点,那么这个项目又怎么证明你的能力呢?</li><li>方案调研/方案对比。工作方式中,做好前期足够的调研和准备,认真对比得到的解决方案,才可以说是合适的方案。</li><li>(解决过程)。过程大多数时候无关紧要,但是如果同样存在难点,也可以一并描述。</li><li>项目结果(最终效果/数据论证)。如果有足够的证据佐证,那么这个项目的成果便是无可置疑的。</li><li>展望:遗留问题/后续计划/产生更多价值。从点到面发散这个项目,是否可以做更多?</li><li>个人影响力。</li></ol><p>这里就不过多描述了,其实如果你有认真思考以上的点,基本就可以说是有认真对待一个项目,同时自己也能从中获得足够多的成长和沉淀了。</p><p>除去答辩本身,以上的这些内容其实在我们日常的工作里,同样需要进行思考和去完成的。也就是说,这样的结构点,并不只是答辩所需,而是需要贯彻到我们工作的每个项目/每个遇到的问题里,这样才能更好地脚踏实地,同时也不需要再为答辩专门做更多的处理了。</p><h3 id="答辩技巧"><a href="#答辩技巧" class="headerlink" title="答辩技巧"></a>答辩技巧</h3><p>如果说我们在平时就已经把工作结构化地做好了,是否意味着答辩就能一定顺利呢?</p><p>除了内容本身需要踏实以外,我们还需要掌握一定的答辩技巧,比如:</p><ul><li>PPT 思路清晰,可以参考上述答辩内容结构来进行梳理</li><li>适当使用动画,突出重点。动画的用处在于让对方注意力聚焦在自己讲的内容上,所以要避免过分浮夸的动画</li><li>陈述足够熟练/脱稿,自己或是找同事多练几遍</li><li>思考项目中的不足/可能提问的问题,准备到如何回答</li></ul><p>以上等等。</p><p>如果你有时间,可以来看看这个视频(<a href="https://www.bilibili.com/video/BV1tu411X7qn/" target="_blank" rel="noopener">也可以直接去 B 站看原视频哦</a>):</p><div style="position: relative; padding: 30% 45%;"><br><br><iframe style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;" src="https://player.bilibili.com/player.html?aid=509223755&bvid=BV1tu411X7qn&cid=512396881&page=1&high_quality=1" frameborder="no" scrolling="no"><br><br></iframe><br><br></div><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>在工作中,我看到过不少由于晋级失败、拿了差考核而开始怀疑自我的小伙伴。我想说的是,工作只是人生的一部分,并不代表着全部,也不可以因为工作的不顺利而否定或是认定自己的一生。</p><p>实际上,失败才是大多数人一生的主旋律,我们要尽早学会如何与失望和意外相处,要接受不完美的自己,学会认可自己。世界上有无数的人,失败或是成功,但是只有一个自己,要学会爱上这个自己。</p>]]></content>
<summary type="html">
<p>最近又是答辩季,程序员最讨厌写的 PPT 又到了不得不写的时候了。之前也有在帮一些小伙伴做准备,所以顺便给大家分享一些晋级答辩的思考和技巧吧~</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/2022/02/07/front-end-engineering/"/>
<id>https://godbasin.github.io/2022/02/07/front-end-engineering/</id>
<published>2022-02-07T07:22:23.000Z</published>
<updated>2022-02-07T07:33:20.409Z</updated>
<content type="html"><![CDATA[<p>前端工程化这个词出现的频率越来越高,一直没有明确的定义,有些人认为是模块化和自动化的工具,比如 Webpack/Gulp、脚手架、组件化等,但工具只是一些辅助手段,并不能作为工程化来理解。</p><a id="more"></a><p>个人认为,前端工程化致力于提升工程的开发效率、协作效率、项目质量,贯穿项目设计、开发、测试、上线、维护的整个过程。</p><p>那么,工程化要解决的问题,便是我们在这些过程中遇到的一些问题了。</p><h1 id="前端项目开发常见问题"><a href="#前端项目开发常见问题" class="headerlink" title="前端项目开发常见问题"></a>前端项目开发常见问题</h1><p>相信大家的日常工作中也能感受到,相比于从 0 到 1 搭建项目,我们的大部分工作都是在维护项目,基于现有的项目上进行迭代和开发。正所谓铁打的营盘流水的兵,每个项目都会经历很多开发的参与、协作和交接,在这个过程中常常会遇到很多的问题,这些问题可以分为两类:<strong>系统质量的下降</strong>,以及<strong>开发效率的下降</strong>。</p><h2 id="系统质量"><a href="#系统质量" class="headerlink" title="系统质量"></a>系统质量</h2><blockquote><p>“没有 BUG 的系统是不存在的。” – 《被删的开发手册》</p></blockquote><h3 id="系统质量的下降"><a href="#系统质量的下降" class="headerlink" title="系统质量的下降"></a>系统质量的下降</h3><p>BUG 的出现有很多的可能性,比如需求设计不严谨、代码实现的逻辑有漏洞、不在预期之内的异常逻辑分支,等等。除了方案设计和思考的经验不足,BUG 很多时候也会因为对项目的不熟悉、对系统的理解不深入引入,这意味着以下的过程会导致 BUG 的增加:</p><ol><li>项目频繁地调整(新增或者更换)开发人员,由于不熟悉项目,每个新加入的小伙伴都可能会埋下新的 BUG。</li><li>系统功能新增和迭代、不断壮大,各个模块间的耦合增加、复杂度增加。如果没法掌握系统的所有细节,很可能牵一发而动全身,产生自己认知以外的 BUG。</li></ol><p>对于处于快速迭代、不断拓展阶段的项目来说,不管是人员的变动、还是项目的拓展都是无法避免的。除此之外,为了降低系统的复杂度,当项目发展到一定阶段的时候,会对系统进行局部或是整体的架构调整,比如模块的拆分、各个模块间的依赖解耦、引入新的状态管理工具、重复逻辑进行抽象和封装等等。</p><p>新技术的引入会缓解系统复杂度带来的稳定性问题,但同时也可能会引入新的问题,比如:</p><ul><li>部分功能无法与新技术兼容,成为历史遗留问题</li><li>较大范围的架构调整影响面很广,可能埋下难以发现的 BUG</li></ul><p>可见,一个项目不断发展的过程中,都会面临系统质量下降的问题。</p><h3 id="提升系统质量"><a href="#提升系统质量" class="headerlink" title="提升系统质量"></a>提升系统质量</h3><p>为了提升系统质量,我们需要对项目进行合理的架构调整,提升系统的可读性、可维护性、可测试行、稳定性,从而提升系统发布的稳定性。</p><p>我们在进行架构设计时,需要根据项目的预期和现状来设计,保留拓展性的同时,避免过度设计。因此,随着项目不断发展,原有的架构设计可能不再适合,此时我们需要进行优化和调整,比如:</p><ul><li>引入新的技术和工具</li><li>团队成员增加,沟通成本和对规范的理解出现差异</li><li>项目代码量和文件数的增加</li><li>进行自动化测试能力的覆盖</li><li>搭建完善的监控和告警体系</li></ul><p>在这个过程中,我们可能分别引入了新的代码构建、代码规范和自动化测试工具,搭建了新的监控系统、发布系统、流程控制等,这些都属于前端工程化的一部分。</p><p>但是对于开发来说,开发流程变得繁琐,意味着工作内容更复杂,同时还增加了很多新工具和系统的熟悉成本。那么,我们还可以通过优化项目的研发和发布流程,来提升项目的开发效率。</p><h2 id="开发效率"><a href="#开发效率" class="headerlink" title="开发效率"></a>开发效率</h2><blockquote><p>“今天又要加班了,因为今天的代码还没开始写。” – 《被删的开发手册》</p></blockquote><h3 id="开发效率的下降"><a href="#开发效率的下降" class="headerlink" title="开发效率的下降"></a>开发效率的下降</h3><p>系统上线之后,开发的工作内容重心,会从功能开发逐渐转向其它内容。除了新功能的评审和设计以外,还会包括:</p><ul><li>用户反馈问题跟进和定位</li><li>线上 BUG 修复和紧急发布</li><li>处理系统的监控告警,排查异常问题</li><li>新功能灰度发布过程,自测、产品验证功能、提测、修复 BUG、灰度发布等各个流程都需要人工操作和主动关注</li><li>为了保证系统质量,需要完善自动化测试能力,包括单元测试、UI 测试、集成测试等</li><li>项目成员的调整,需要进行工作的交接、指导对方的工作内容等</li></ul><p>开发的工作内容变得复杂,需要关注的事情也更多,对于各个系统(监控告警系统、日志系统、测试系统、发布系统等)也都需要熟悉成本和操作成本。在各个工作内容之间切换,也常常容易出现步骤的遗漏,导致一些流程上的问题,比如:</p><ol><li>系统灰度到一半,处理其它事情忘了全量。</li><li>系统发布之后,去处理紧急 BUG、忘记看监控,直到收到大量的用户反馈。</li><li>线上紧急 BUG 修复了,急着发布忘了进行自动化测试。</li></ol><p>随着项目规模变大,系统的复杂度也随之上升,上面所提到的工作量也都会增加,开发效率会肉眼可见地受到影响。以前一天工作量的功能开发,如今需要三天时间才能完成,因为每天只有三分之一的时间(甚至更少)可以用来开发新功能。</p><p>在这个项目阶段,开发每天的杂事太多、效率太低、浑浑噩噩不知道都做了些什么,团队面临着项目复杂度上升、系统质量不稳定、技术债务越来越多、团队工作效率下降等问题。</p><h3 id="提升开发效率"><a href="#提升开发效率" class="headerlink" title="提升开发效率"></a>提升开发效率</h3><p>项目研发和发布流程优化的核心点在于:将一切需要手动操作和关注的内容自动化。</p><p>那么,我们先来梳理下项目开发和发布过程中,到底有多少繁琐的工作可以进行自动化。一般来说,开发在接到产品需求单后,会涉及到分支管理、代码构建、环境部署、测试/验证、问题修复、灰度发布、监控告警、需求单状态扭转等各个流程。</p><p>每一次功能发布,都需要花费很多的精力在各个流程步骤上。我们可以将这些步骤都转为自动化,就可以让开发的精力聚焦在功能的设计和实现上。对于流程自动化,业界比较成熟的解决方案是使用持续集成(continuous integration,简称 CI)和持续部署(continuous deployment,简称 CD):</p><ul><li>持续集成(CI):目的是让产品可以快速迭代,同时还能保持高质量</li><li>持续部署(CD):目的是代码在任何时刻都是可部署、可进入生产阶段</li></ul><p>CI 在项目中的实践,指团队成员频繁(一天多次)地提交代码到主干分支,每次提交后会自动触发自动化验证的任务集合,以便尽可能地提前发现问题;CD 在项目中的实践,指代码通过评审以后,可自动化、可控、多批次地部署到生产环境,CD 的前提是能自动化完成测试、构建、部署等步骤。</p><p>CI/CD 在项目中的落地,很多时候会表现为流水线的开发模式:通过建立完整的 CI/CD 流水线,涵盖整个研发流程,可有效地提高效率。一般来说,我们可以搭建这样的 CI/CD 流水线:</p><ul><li>需求单分配:分配并自动拉取相应 Git 分支</li><li>代码提交:代码规范检查 + 自动化测试 + 部署测试环境 + 根据需求单配置通知相应的产品和测试</li><li>产品验证/功能测试:BUG 单自动关联需求单和分支 + 验证完成后,根据需求单通知开发侧</li><li>BUG 修复:代码规范检查 + 自动化测试 + 部署测试环境 + 根据分支 BUG 单和需求单通知测试 + 验证完成后,根据需求单通知开发侧</li><li>代码合入主干:向团队成员发起代码 Review + Review 通过后代码合入主干</li><li>日常发布:定时器发起发布流程 + 预发布环境部署 + 进行自动化测试 + 测试通过后进入灰度过程</li><li>灰度发布:根据配置比例进行灰度 + 灰度过程中自动化进行监控 + 可选择性进入快速回滚流程</li><li>全量发布:自动扭转需求单状态,并将版本进行归档(Git Tag)</li></ul><p>通过将以上流程自动化,可以节省开发的很多人工操作时间、提升开发效率的同时,也避免了人工操作容易出现的遗漏和失误。将自动化流水线与通知/告警机器人、工作群、需求单系统、BUG 系统、代码管理系统、发布系统、监控系统结合,实现全研发和发布流程的自动化,开发可从各种杂事中释放,专注于功能开发的实现。</p><p>越是大规模、系统建设完备的团队,开发流程中消耗在多人协作和各个系统的操作中的精力越多,搭建 CI/CD 后更能体会到自动化流程带来的便利。</p><p>当然,搭建 CI/CD 的过程中,也需要投入不少的人力精力。因此,很多时候我们可以考虑性价比,从对研发效能影响最大的痛点开始进行建设,可以最快速和有效地提升团队的开发效率,让更多的人愿意参与到 CI/CD 的建设中。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><blockquote><p>“能用机器解决的问题,就不要依赖人。” – 《被删的开发手册》</p></blockquote><p>项目维护阶段的最大痛点,其实在于开发无法聚焦自身的工作内容,常常需要在各种系统中进行操作和切换,从而带来开发效率的下降,以及注意力分散、无法更全面的思考导致了不合理的设计、新的 BUG 引入,而影响了系统的质量。</p><p>前端工程化的出现,正是为了解决系统质量和效率低下的问题。但前端工程化并不只是局限于代码构建和流水线,可以将其理解为解决项目开发过程中遇到的所有问题,目的在于提升系统质量和开发效率。</p>]]></content>
<summary type="html">
<p>前端工程化这个词出现的频率越来越高,一直没有明确的定义,有些人认为是模块化和自动化的工具,比如 Webpack/Gulp、脚手架、组件化等,但工具只是一些辅助手段,并不能作为工程化来理解。</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>Angular框架解读--Ivy编译器之变更检测</title>
<link href="https://godbasin.github.io/2022/01/09/angular-design-ivy-6-detect-change/"/>
<id>https://godbasin.github.io/2022/01/09/angular-design-ivy-6-detect-change/</id>
<published>2022-01-09T12:01:48.000Z</published>
<updated>2022-01-09T12:10:04.321Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中变更检测的过程。</p><a id="more"></a><p>上一篇<a href="https://godbasin.github.io/2021/12/05/angular-design-ivy-5-incremental-dom/">《Angular框架解读–Ivy编译器之增量DOM》</a>中,我介绍了 Ivy 编译器中使用了增量 DOM 的设计。在 Ivy 中,通过编译器将模板编译为<code>template</code>渲染函数,该过程会将对模板的解析编译成增量 DOM 相关的指令。其中,在<code>elementStart()</code>执行时,我们可以看到会通过<code>createElementNode()</code>方法来创建 DOM。</p><p>而增量 DOM 中的变更检测、Diff 和更新 DOM 等能力,都与<code>elementStart()</code>方法紧紧关联着。</p><h2 id="Ivy-中的变更检测"><a href="#Ivy-中的变更检测" class="headerlink" title="Ivy 中的变更检测"></a>Ivy 中的变更检测</h2><h3 id="ngZone-的自动变更检测"><a href="#ngZone-的自动变更检测" class="headerlink" title="ngZone 的自动变更检测"></a>ngZone 的自动变更检测</h3><p>在<a href="https://godbasin.github.io/2021/05/30/angular-design-zone-ngzone/">《Angular框架解读–Zone区域之ngZone》</a>一文中,我们介绍了默认情况下,所有异步操作都在 Angular Zone 内。该逻辑在创建 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></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>tick</code>方法中,核心的逻辑是调用了<code>view.detectChanges()</code>来检测更新。该接口来自<code>ChangeDetectorRef</code>,它提供变更检测功能的基类。</p><p>变更检测树收集所有要检查变更的视图,可以使用方法从树中添加和删除视图,启动更改检测,并将视图显式标记为<code>_dirty_</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="keyword">export</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> markForCheck(): <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">// 与 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><br><span class="line"> <span class="comment">// 检查此视图及其子视图</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><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><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><p>在上述的<code>ChangeDetectorRef</code>中,变更检测<code>detectChanges()</code>中,核心逻辑调用了<code>refreshView()</code>。</p><h3 id="refreshView-视图更新处理"><a href="#refreshView-视图更新处理" class="headerlink" title="refreshView 视图更新处理"></a>refreshView 视图更新处理</h3><p><code>refreshView()</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></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">refreshView</span><<span class="title">T</span>>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> tView: TView, lView: LView, templateFn: ComponentTemplate<{}>|<span class="literal">null</span>, context: T</span>) </span>{</span><br><span class="line"> ngDevMode && assertEqual(isCreationMode(lView), <span class="literal">false</span>, <span class="string">'Should be run in update mode'</span>);</span><br><span class="line"> <span class="keyword">const</span> flags = lView[FLAGS];</span><br><span class="line"> enterView(lView);</span><br><span class="line"> <span class="keyword">const</span> isInCheckNoChangesPass = isInCheckNoChangesMode();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> resetPreOrderHookFlags(lView);</span><br><span class="line"></span><br><span class="line"> setBindingIndex(tView.bindingStartIndex);</span><br><span class="line"> <span class="keyword">if</span> (templateFn !== <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">// 1. 执行 template 模板函数</span></span><br><span class="line"> executeTemplate(tView, lView, templateFn, RenderFlags.Update, context);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2. 执行预处理钩子,包括 OnInit、OnChanges、DoCheck</span></span><br><span class="line"> <span class="keyword">if</span> (!isInCheckNoChangesPass) {</span><br><span class="line"> <span class="keyword">if</span> (hooksInitPhaseCompleted) {</span><br><span class="line"> executeCheckHooks(lView, preOrderCheckHooks, <span class="literal">null</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, <span class="literal">null</span>);</span><br><span class="line"> incrementInitPhaseFlags(lView, InitPhaseState.OnInitHooksToBeRun);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 首先将在此 lView 中声明的移植视图标记为需要在其插入点刷新</span></span><br><span class="line"> <span class="comment">// 这是为了避免模板在这个 LView 中定义但它的声明出现在插入组件之后的情况</span></span><br><span class="line"> markTransplantedViewsForRefresh(lView);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 遍历嵌入式视图(通过 ViewContainerRef API 创建的视图)并通过执行关联的模板函数刷新它们</span></span><br><span class="line"> refreshEmbeddedViews(lView);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3. 在调用内容钩子之前,必须刷新内容查询结果</span></span><br><span class="line"> <span class="keyword">if</span> (tView.contentQueries !== <span class="literal">null</span>) {</span><br><span class="line"> refreshContentQueries(tView, lView);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 执行内容钩子,包括 AfterContentInit, AfterContentChecked</span></span><br><span class="line"> <span class="keyword">if</span> (!isInCheckNoChangesPass) {</span><br><span class="line"> <span class="keyword">if</span> (hooksInitPhaseCompleted) {</span><br><span class="line"> executeCheckHooks(lView, contentCheckHooks);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> executeInitAndCheckHooks(lView, contentHooks, InitPhaseState.AfterContentInitHooksToBeRun);</span><br><span class="line"> incrementInitPhaseFlags(lView, InitPhaseState.AfterContentInitHooksToBeRun);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 4. 设置 host 绑定</span></span><br><span class="line"> processHostBindingOpCodes(tView, lView);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 5. 刷新子组件视图</span></span><br><span class="line"> <span class="keyword">const</span> components = tView.components;</span><br><span class="line"> <span class="keyword">if</span> (components !== <span class="literal">null</span>) {</span><br><span class="line"> refreshChildComponents(lView, components);</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">// 如果视图查询在子组件刷新之前执行,则模板可能尚未插入</span></span><br><span class="line"> <span class="keyword">const</span> viewQuery = tView.viewQuery;</span><br><span class="line"> <span class="keyword">if</span> (viewQuery !== <span class="literal">null</span>) {</span><br><span class="line"> executeViewQueryFn(RenderFlags.Update, viewQuery, context);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 执行视图钩子,包括 AfterViewInit, AfterViewChecked</span></span><br><span class="line"> <span class="keyword">if</span> (!isInCheckNoChangesPass) {</span><br><span class="line"> <span class="keyword">if</span> (hooksInitPhaseCompleted) {</span><br><span class="line"> executeCheckHooks(lView, viewCheckHooks);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> executeInitAndCheckHooks(lView, viewHooks, InitPhaseState.AfterViewInitHooksToBeRun);</span><br><span class="line"> incrementInitPhaseFlags(lView, InitPhaseState.AfterViewInitHooksToBeRun);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 我们需要确保我们只在成功的 refreshView 上翻转标志</span></span><br><span class="line"> <span class="keyword">if</span> (tView.firstUpdatePass === <span class="literal">true</span>) {</span><br><span class="line"> tView.firstUpdatePass = <span class="literal">false</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="comment">// 例如:在 ngAfterViewInit 钩子中将 OnPush 组件标记为脏组件以刷新 NgClass 绑定应该可以工作</span></span><br><span class="line"> <span class="keyword">if</span> (!isInCheckNoChangesPass) {</span><br><span class="line"> lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (lView[FLAGS] & LViewFlags.RefreshTransplantedView) {</span><br><span class="line"> lView[FLAGS] &= ~LViewFlags.RefreshTransplantedView;</span><br><span class="line"> updateTransplantedViewCount(lView[PARENT] <span class="keyword">as</span> LContainer, <span class="number">-1</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> leaveView();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,<code>refreshView()</code>的处理包括按特定顺序执行的多个步骤:</p><ol><li>在更新模式下,执行<code>template</code>模板函数。</li><li>执行钩子。</li><li>刷新 Query 查询。</li><li>设置 host 绑定。</li><li>刷新子(嵌入式和组件)视图。</li></ol><p>除此之外,在变更检测的最开始执行了<code>enterView()</code>,此时 Angular 会用新的<code>LView</code>交换当前的<code>LView</code>。这样的处理主要出于性能原因,通过将<code>LView</code>存储在模块的顶层,最大限度地减少了要读取的属性数量。</p><p><code>LView</code>用于存储从模板调用指令时处理指令所需的所有信息,在<a href>《Angular框架解读–Ivy编译器的视图数据和依赖解析》</a>中有介绍。</p><p>每个嵌入视图和组件视图都有自己的<code>LView</code>。在处理特定视图时,我们将<code>viewData</code>设置为该<code>LView</code>。当该视图完成处理后,<code>viewData</code>被设置回原始<code>viewData</code>之前的任何内容(父<code>LView</code>)。</p><p>在<code>refreshView()</code>处理中,每当进入新视图时会存储<code>LView</code>以备后用。我们也可以看到当退出视图时,通过执行<code>leaveView()</code>离开当前的<code>LView</code>,恢复原来的状态。</p><p>以上便是变更检测过程中的视图处理逻辑。</p><h3 id="创建与更新视图的处理"><a href="#创建与更新视图的处理" class="headerlink" title="创建与更新视图的处理"></a>创建与更新视图的处理</h3><p>我们可以对比下创建视图的过程,处理视图创建的过程在<code>renderView()</code>中实现。</p><p><code>renderView()</code>用于在创建模式下处理视图,该过程包括按特定顺序执行的多个步骤:</p><ol><li>创建视图查询函数(如果有)。</li><li>在创建模式下,执行<code>template()</code>模板函数。</li><li>更新静态 Query 查询(如果有)。</li><li>创建在给定视图中定义的子组件。</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { Component, Input } <span class="keyword">from</span> <span class="string">"@angular/core"</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span>({</span><br><span class="line"> selector: <span class="string">"greet"</span>,</span><br><span class="line"> template: <span class="string">"<div> Hello, {{name}}! </div>"</span>,</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> GreetComponent {</span><br><span class="line"> <span class="meta">@Input</span>() name: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>经<code>ngtsc</code>编译后,产物会大概长这个样子:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">GreetComponent.ɵcmp = i0.ɵɵdefineComponent({</span><br><span class="line"> type: GreetComponent,</span><br><span class="line"> tag: <span class="string">"greet"</span>,</span><br><span class="line"> factory: <span class="function"><span class="params">()</span> =></span> <span class="keyword">new</span> GreetComponent(),</span><br><span class="line"> template: <span class="function"><span class="keyword">function</span> (<span class="params">rf, ctx</span>) </span>{</span><br><span class="line"> <span class="comment">// 创建模式下</span></span><br><span class="line"> <span class="keyword">if</span> (rf & RenderFlags.Create) {</span><br><span class="line"> i0.ɵɵelementStart(<span class="number">0</span>, <span class="string">"div"</span>);</span><br><span class="line"> i0.ɵɵtext(<span class="number">1</span>);</span><br><span class="line"> i0.ɵɵelementEnd();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 更新模式下</span></span><br><span class="line"> <span class="keyword">if</span> (rf & RenderFlags.Update) {</span><br><span class="line"> i0.ɵɵadvance(<span class="number">1</span>);</span><br><span class="line"> i0.ɵɵtextInterpolate1(<span class="string">"Hello "</span>, ctx.name, <span class="string">"!"</span>);</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>可以看到,创建模式下的模板函数逻辑,与更新视图模式下的模板函数逻辑是有区别的。在创建模式下,<code>elementStart</code>、<code>elementEnd</code>我们在上一篇文章中有详细地介绍了。而在更新模式下,<code>textInterpolate1</code>表示当文本节点有 1 个内插值时,使用由其他文本包围的单个绑定值更新文本内容:</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="function"><span class="keyword">function</span> <span class="title">interpolation1</span>(<span class="params">lView: LView, prefix: <span class="built_in">string</span>, v0: <span class="built_in">any</span>, suffix: <span class="built_in">string</span></span>): <span class="title">string</span>|</span></span><br><span class="line"><span class="function"> <span class="title">NO_CHANGE</span> </span>{</span><br><span class="line"> <span class="keyword">const</span> different = bindingUpdated(lView, nextBindingIndex(), v0);</span><br><span class="line"> <span class="keyword">return</span> different ? prefix + renderStringify(v0) + suffix : NO_CHANGE;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以见到,在具体的模板函数指令中,会自行进行变更的检查,如果有发生了变化,则进行更新。<code>bindingUpdated()</code>方法会在需要更改时更新绑定,然后返回是否已更新。</p><p>而对于视图更新时,除了<code>textInterpolate1</code>这种比较简单的场景下的模板更新,子组件通过<code>refreshComponent</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="function"><span class="keyword">function</span> <span class="title">refreshComponent</span>(<span class="params">hostLView: LView, componentHostIdx: <span class="built_in">number</span></span>): <span class="title">void</span> </span>{</span><br><span class="line"> ngDevMode && assertEqual(isCreationMode(hostLView), <span class="literal">false</span>, <span class="string">'Should be run in update mode'</span>);</span><br><span class="line"> <span class="keyword">const</span> componentView = getComponentLViewByIndex(componentHostIdx, hostLView);</span><br><span class="line"> <span class="comment">// 仅应刷新 CheckAlways 或 OnPush 且 Dirty 的附加组件</span></span><br><span class="line"> <span class="keyword">if</span> (viewAttachedToChangeDetector(componentView)) {</span><br><span class="line"> <span class="keyword">const</span> tView = componentView[TVIEW];</span><br><span class="line"> <span class="keyword">if</span> (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {</span><br><span class="line"> <span class="comment">// 此处检测组件是否被标记为 CheckAlways 或者 Dirty,此时才进行该组件的视图更新</span></span><br><span class="line"> refreshView(tView, componentView, tView.template, componentView[CONTEXT]);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 仅应刷新 CheckAlways 或 OnPush 且脏的附加组件</span></span><br><span class="line"> refreshContainsDirtyView(componentView);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同样的,在处理子组件的时候,需要检查子组件是否被标记为 CheckAlways 或者 Dirty,才进入组件视图并处理其绑定、查询等来刷新组件。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>以上,便是 Angular Ivy 中的变更检测了。</p><p>可以看到,在 Angular 中将被标记为 CheckAlways 或者 Dirty 的组件进行视图刷新,在每个变更周期中,会执行<code>template()</code>模板函数中的更新模式下逻辑。而在<code>template()</code>模板函数中的具体指令逻辑中,还会根据原来的值和新的值进行比较,有差异的时候才会进行更新。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://indepth.dev/posts/1271/angular-ivy-change-detection-execution-are-you-prepared" target="_blank" rel="noopener">Angular Ivy change detection execution: are you prepared?</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>2021 年度总结--冲啊打工人</title>
<link href="https://godbasin.github.io/2021/12/25/my-2021/"/>
<id>https://godbasin.github.io/2021/12/25/my-2021/</id>
<published>2021-12-25T08:00:13.000Z</published>
<updated>2021-12-25T08:01:10.447Z</updated>
<content type="html"><![CDATA[<p>工作很忙的时候,我们总以为自己除了忙碌以外,什么都没有。但当你仔细去回顾,就会发现:其实每一刻每一秒,我们都没有辜负。</p><a id="more"></a><h2 id="总的来说"><a href="#总的来说" class="headerlink" title="总的来说"></a>总的来说</h2><p>总结一下我的 2021,细细看发现也做了不少事情:</p><ol><li>和别人合写的<a href="https://item.jd.com/13289582.html" target="_blank" rel="noopener">《程序开发原理与实战》</a>这本书历经 2 年终于出版了!!</li><li>自己最喜欢的<a href="https://www.ituring.com.cn/book/2942" target="_blank" rel="noopener">《前端的进击》</a>这本书,最终遗憾地以电子书的方式出版了(::sad::)。</li><li>在编辑的邀请下,第一次尝试做了课程<a href="https://kaiwu.lagou.com/course/courseInfo.htm?courseId=822" target="_blank" rel="noopener">《前端的进击笔记》</a>。</li><li>挤了时间研究自己最喜欢的 Angular 框架,并写了一系列的<a href="http://www.godbasin.com/angular/deep-into-angular/angular-design-0-prestart.html" target="_blank" rel="noopener">《Angular 框架解读》</a>。</li><li>开始玩 B 站(<a href="https://space.bilibili.com/42233366" target="_blank" rel="noopener">id: 被删</a>),尝试做一些前端入门和深入的讲解视频。</li><li>帮忙拍摄《递归》系列视频,主题为<a href="https://mp.weixin.qq.com/s/_BySol8lXoU5Bre-40Lskw" target="_blank" rel="noopener">《保持生长不焦虑,非科班程序媛的进击》</a>。</li><li>给我家猫猫画表情包——<a href="https://sticker.weixin.qq.com/cgi-bin/mmemoticon-bin/emoticonview?oper=single&t=shop/detail&productid=aL2PCfwK/89qO7sF6/+I+UDhfwEjhec2ZNvdnLLJRd/Mc1uPro/vaqwIvQ4/JvfucH1+P4XxvWH+nmdDocf83TL09YaAo13vnpYiiZLoocgY=" target="_blank" rel="noopener">牧羊猪的打工日记系列</a>。</li></ol><p>嗯,大概这就是我的 2021,工作中和工作外都有不少的收获和成长。那么下面,如果你感兴趣的话,听我细细道来呀~</p><h2 id="关于工作"><a href="#关于工作" class="headerlink" title="关于工作"></a>关于工作</h2><h3 id="1-工作经历"><a href="#1-工作经历" class="headerlink" title="1. 工作经历"></a>1. 工作经历</h3><p>这一年的工作经历,和以往有一个共同点:<strong>遇到过新的问题,然后有了新的体会和感受</strong>。</p><p>从 2014 年毕业,后裸辞工作之后自学前端,然后开始慢慢深入学习和开发,一直到如今 2021 年,我几乎每年都有工作变动,几乎都是自己主动发起的调整。</p><p>越来越发现,刚毕业的时候我们都充满热情,到最后大家却逐渐地对“妥协”二字妥协了。这几年互联网行业的确很卷,竞争力和压力也增加了不少,很多人都充满了迷茫和焦虑,而我也增加了不少的疑惑。</p><p>今年的主要思考是:</p><ul><li>作为一名前端开发/程序员,我想要走向哪里?</li><li>职业发展上常说的广度和深度,是不是伪命题?</li><li>团队管理中,一个开发能做的有多少?</li></ul><p>关于这些,每一个点讲起来都可以长篇大论了,我之前在博客有讲相关的内容,包括《关于一年一换的魔咒》、《技术开发的门槛高吗》、《关于技术开发的职业发展》、《技术深度是伪命题吗》,这些你可以在我的<a href="http://www.godbasin.com/front-end-work/front-end-days/about-front-end-11.html" target="_blank" rel="noopener">“被删前端游乐场–前端这几年”</a>分享里找到。</p><h3 id="2-项目经历"><a href="#2-项目经历" class="headerlink" title="2. 项目经历"></a>2. 项目经历</h3><p>这一年的工作经历,和以往也有不同点:第一次接触大型前端项目的难题。</p><p>目前在文档团队,在线文档的编辑和协同对前端来说有不小的挑战。之前也有简单地整理了一篇了解在线文档的文章:<a href="http://www.godbasin.com/front-end-basic/deep-learning/why-spreadsheet-app-excited.html" target="_blank" rel="noopener">《在线 Excel 项目到底有多刺激》</a>,简单来说会包括:</p><ul><li>协同过程中的冲突处理算法</li><li>多人协作时的版本管理和维护</li><li>大文档下的加载和渲染性能、卡顿问题</li><li>文档数据结构的设计和算法</li><li>Canvas 渲染和 DOM 渲染的一致性</li><li>排版引擎的设计和优化</li></ul><p>除了文档本身功能逻辑的难题之外,这样的项目还涉及到代码量过大(100W+)、开发团队人员过多、协作开发和管理等各种各样的难题,包括:</p><ul><li>如何对模块之间进行功能解耦</li><li>如何进行大项目的代码组织和架构设计</li><li>大型前端项目的代码加载流程如何优化</li><li>大团队里多人协作导致的问题和解决方案</li><li>如何保证大型项目的开发效率/可维护性/可读性</li></ul><p>团队里优秀的小伙伴很多,真就每天都能学到不少的知识。即使到今天,我已经来这个团队一年多了,依然对整个项目还有许多地方了解得比较浅。总的来说,<strong>非常有幸参与到这样的项目里,让我可以在前端领域工作的第 6 年里,依然有无数种让自己获得成长的方式</strong>。</p><p>以上便是是工作相关的,虽然今年也有介于团队调整空白期的懈怠,但在即将结束的 2021 年底前,顺利地将自己的状态调整过来,这是值得开心的事情。</p><h3 id="3-工作外的技术成长"><a href="#3-工作外的技术成长" class="headerlink" title="3. 工作外的技术成长"></a>3. 工作外的技术成长</h3><p>主要有三点:</p><ol><li>技术博客的更新–<a href="https://github.com/godbasin/front-end-playground" target="_blank" rel="noopener">被删的前端游乐场</a>。</li><li>技术书的出版。</li><li>技术课程的制作–<a href="https://kaiwu.lagou.com/course/courseInfo.htm?courseId=822" target="_blank" rel="noopener">《前端的进击笔记》</a>。</li><li>技术视频的制作–<a href="https://space.bilibili.com/42233366/channel/seriesdetail?sid=372770" target="_blank" rel="noopener">《前端开发那些事》</a>。</li></ol><p>2021 年,我的技术博客一如既往地在更新,今年在业余时间去研究了下自己很喜欢的 Angular 框架,并写了一系列的<a href="http://www.godbasin.com/angular/deep-into-angular/angular-design-0-prestart.html" target="_blank" rel="noopener">《Angular 框架解读》</a>。除此之外,我也写了一些工作上的思考内容,更新了好久没写的前端工作系列。</p><p>今年出版了两本书,一本纸质书和一本电子书,算上之前写的一本开源书,目前我一个写了三本技术书了:</p><ul><li><a href="https://item.jd.com/13289582.html" target="_blank" rel="noopener">纸质书《程序开发原理与实战》</a></li><li><a href="https://www.ituring.com.cn/book/2942" target="_blank" rel="noopener">电子书《前端的进击》</a></li><li><a href="http://www.godbasin.com/vue-ebook/" target="_blank" rel="noopener">开源书《深入理解 Vue.js 实战》</a></li></ul><p>其中,我最喜欢的书是《前端的进击》这一本。最开始为什么想写这本书呢?主要是因为自己这几年的工作经历也比较折腾,认识和学到了很多。但反观身边的很多小伙伴,尤其是刚毕业的应届生们,他们会存在很多很多的疑惑,也没有人告诉他们该怎么做,很多时候会陷入自我怀疑的困境。</p><p>他们遇到的这些问题,有些只需要调整下自身的工作方式和状态,有一些需要通过有效的沟通去解决,还有一些则是大环境下的常见问题。职场工作和校园学习相差很远,刚开始工作的那几年,很可能就决定了以后对工作、对这个行业的认知和价值观。</p><p><strong>很多很多的事情,它们都没有标准答案,都需要每个人自己去进行探索和思考。</strong></p><p>因此,我把自己的工作方法和思考写下来,希望能对一些正感到困惑的人给到帮助。这就是这本书的初衷,我非常希望在遇到一些“不对劲”的事情时,他们能少一些的自我怀疑,接受预期之外的事情发生,同时能坚持住自己的初心。</p><p>后来,在编辑的鼓励下,我给这本书画了很多的插画,包括这本书封面的猫猫也是我画的:</p><p><img src=<a href="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" target="_blank" rel="noopener">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</a> width=60% /></p><p>再后来,这本书因为审核时出版社考虑成本的原因,无法进行纸质书的销售,尝试加上了加一些硬技能的内容,变成了三大部分:前端基础和入门、提升硬实力、必备软实力。但还是无法出版纸质书,最终以电子书的方式出版了,这大概是我和编辑小姐姐都特别遗憾的事情了。</p><blockquote><p>如果你对这本书的写作过程感兴趣,也可以来看看<a href="http://www.godbasin.com/front-end-work/front-end-days/a-book-with-one-story.html" target="_blank" rel="noopener">《一本书和一个故事》</a>。</p></blockquote><h2 id="生活中的新尝试"><a href="#生活中的新尝试" class="headerlink" title="生活中的新尝试"></a>生活中的新尝试</h2><p>如果要概括 2021 年的生活,主题大概是:<strong>多去尝试做一些新的事情</strong>。</p><p>今年这些事情包括:拍视频、画插画、做视频、画表情包,这些都是我以前没有尝试过去做的,但是做的时候觉得特别开心。</p><h3 id="1-画的表情包和插画"><a href="#1-画的表情包和插画" class="headerlink" title="1. 画的表情包和插画"></a>1. 画的表情包和插画</h3><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/%E5%86%8D%E8%A7%812020_LOGO_%E7%94%BB%E6%9D%BF%201_%E7%94%BB%E6%9D%BF%201.png" alt></p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/biaoqingbao123.jpg" alt></p><h3 id="2-做了很多视频"><a href="#2-做了很多视频" class="headerlink" title="2. 做了很多视频"></a>2. 做了很多视频</h3><ul><li><a href="https://space.bilibili.com/42233366/channel/detail?cid=182293" target="_blank" rel="noopener">前端开发那些事</a>:主要是一些入门和深入的技术路线,比较推荐前端进阶路线、前端算法等内容</li><li><a href="https://space.bilibili.com/42233366/channel/detail?cid=186484" target="_blank" rel="noopener">程序员段子</a>:主要是一些日常工作里的灵感段子,自己配音常常笑到肚子疼</li><li><a href="https://space.bilibili.com/42233366/channel/detail?cid=190754" target="_blank" rel="noopener">程序员日志</a>:主要是自己的工作相关的心路历程和思考</li><li><a href="https://space.bilibili.com/42233366/channel/detail?cid=197803" target="_blank" rel="noopener">Angular 冷知识</a>:介绍前端 Angular 框架中比较有意思的设计和实现原理,基于最近在研究的 Angular 源码整理讲的,会比博客上的文章容易理解一些</li><li><a href="https://space.bilibili.com/42233366/channel/detail?cid=184764" target="_blank" rel="noopener">牧羊猪猫猫</a>:我家猫猫的日常,特别可爱欢迎在线吸猫哈哈哈哈</li></ul><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>2021 我的超人:猪猪!!</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/368f7230e349a45b3ff48e789f1f516.jpg" alt></p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/0b0c53771f81f51344e82bce9fb4bf0.jpg" alt></p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>原本我以为,今年过得好像有点浑浑噩噩,没什么成长和长进。</p><p>但是当我开始这么一点点回顾和记录 2021 这一年来的事情时,我发现自己其实还是做了很多事情的。下周就会迎来新的一年了,希望明年也能保持这样一个劲头,多去尝试多去体验,做一个开开心心的自己!!</p><p>最后祝各位 2022 年一切都顺利!!!</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>前端这几年--14.技术深度是伪命题吗</title>
<link href="https://godbasin.github.io/2021/12/12/about-front-end-14/"/>
<id>https://godbasin.github.io/2021/12/12/about-front-end-14/</id>
<published>2021-12-12T06:55:12.000Z</published>
<updated>2021-12-12T07:29:30.132Z</updated>
<content type="html"><![CDATA[<p>最近在思考,我们常说的技术广度和深度,在实际工作中到底指代什么?怎样的工作是有深度?怎样的技术是广度呢?</p><a id="more"></a><h2 id="技术深度到底是指什么?"><a href="#技术深度到底是指什么?" class="headerlink" title="技术深度到底是指什么?"></a>技术深度到底是指什么?</h2><p><a href="https://godbasin.github.io/2021/11/28/about-front-end-13/">上一篇</a> 聊技术开的职业发展,其实也有讲到技术深度和广度的问题,但我觉得这个问题可以再进行更多的探讨。</p><p>在过去工作的这么多年来,我一直认为:只有在某个技术领域达到足够的深度,才能保持自己在该行业的竞争力。这也是我在某段职业发展规划中的方向,其中的几次换工作,都往“更大的平台和团队”、“更复杂的业务”这样的方向去走。</p><p>实际上,在各个团队和项目中的一些经历,让我重新思考起来,我们常说的所谓技术深度,到底是指什么呢?</p><h3 id="复杂的业务真的复杂吗?"><a href="#复杂的业务真的复杂吗?" class="headerlink" title="复杂的业务真的复杂吗?"></a>复杂的业务真的复杂吗?</h3><p>我们经常会调侃自己的工作,比如切图仔、CRUD 工程师、调参工程师。很多时候,当我们掌握了当前工作涉及的技术和技巧之后,剩余的常常只有重复枯燥的工作日常,比如查 BUG、写 BUG、和产品同学扯皮、和测试同学吵架、用夸张手法写汇报,等等。</p><p>当我在做一些自认为简单的业务时,就会向往复杂的业务。在我的经历中,在业务场景比较简单的时候,大家为了晋级和考核,都倾向于将简单的事情变复杂,然后再用“有难度”的解决方案去解决,正所谓“没事找事”。那时候我觉得,如果业务本身足够复杂,就会有足够多的事情值得去解决,而不需要凭空捏造出这些复杂的场景,更不需要为了让解决方案看起来复杂,而特意让业务逻辑变得复杂。</p><p>在复杂的业务团队里,的确会有特别多的新知识和技术可以学习,也可以接触到大的业务场景下不同的领域模块。但实际上,对于大多数开发的日常工作,依然是基于某块业务的开发和维护,或是由于业务过于复杂,每天都被各种模块间的耦合相互影响、依赖各种上下游、莫名其妙出现的 BUG 等等,也没有足够的时间去研究。</p><p>而当我开始尝试解决以前认为足够复杂的业务场景时,发现再复杂的问题,也依然可以将其梳理并一一拆解,然后再逐个击破去解决。在工程化的业务里,我们用到的 99% 的技术方案,几乎都是通过现有的一些技术方案,进行适配、改造、调整后,尝试在业务中落地。以前我觉得涉及到算法和数据结构的业务,可能会面临较大的挑战、有足够的技术深度。但实际上,我们也还是在参考业界的方案,或是研究已发布的论文,结合业务的痛点去尝试解决。</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><h3 id="技术深度是伪命题吗?"><a href="#技术深度是伪命题吗?" class="headerlink" title="技术深度是伪命题吗?"></a>技术深度是伪命题吗?</h3><p>最近会跟一些朋友讨论这个话题:技术深度到底是不是伪命题?</p><p>我的想法是,所谓的技术深度,大多数时候都是由自己的工作经历和项目经验决定的。有些开发的工作中接触到的技术范围较广,所以会有“技术广度”;有些开发在工作中一直接触某个领域的业务,那么他便会拥有该垂直领域的“技术深度”。</p><p>有个朋友举了个例子,说他认为技术深度的确会存在,因为他们曾经遇到过一个大家都觉得“莫名其妙”的问题,是一位 P8 的技术专家一层层剖析解开,最终发现是某个十分底层系统中使用的网络包存在缺陷。他认为,大多数的开发能力是不足以定位到如此深入和细致的问题的。</p><p>我的看法不大一样,或许是这位技术专家以前有过相关经验,不管是曾经遇到过类似的问题也好,还是他对问题定位有自己成熟的经验和方法,这都是由他过去的工作经历决定的。一个一直基于某个领域深挖的技术开发,只要不止步于前,经验和时间的沉淀都会给他带来在这个领域足够的深度。同样的,如果在各个领域间不断转换和研究的技术开发,也可以在广度方向上有足够多的经验和沉淀。当然,对比如今很多只说不做、用漂亮的汇报话术和 PPT 成为技术专家的开发来说,这样能在关键时刻替大家解决问题、而不是指指点点的开发才称得上为真正的专家。</p><p>只能说,如今的行业状态是,很多人可以凭借“表现”和“包装”获得好的考核和职级,因此总有一些技术管理或是技术专家并不能真正地让人信服。相比之下,凭借扎实的项目经验和解决问题能力往上走的开发,很多时候则是被大家成为“技术很强”的专家了。大多数时候,我们追求的所谓“技术深度”,大概也便是这样了。</p><p>当然,广度和深度的要求还是有差别的。大多数时候追求广度容易“泛而不精”,而追求深度则可能领域外“一窍不通”,对于每个希望成为技术专家的人来说,做好广度和深度的平衡也是职业发展中重要的一环。</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><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%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>Angular框架解读--Ivy编译器之增量DOM</title>
<link href="https://godbasin.github.io/2021/12/05/angular-design-ivy-5-incremental-dom/"/>
<id>https://godbasin.github.io/2021/12/05/angular-design-ivy-5-incremental-dom/</id>
<published>2021-12-05T02:25:13.000Z</published>
<updated>2021-12-05T02:53:13.531Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中的增量 DOM 设计。</p><a id="more"></a><p>在介绍前端框架的时候,我常常会介绍到模板引擎。对于模板引擎的渲染过程,像 Vue/React 这样的框架里,使用了虚拟 DOM 这样的设计。</p><p>在 Angular Ivy 编译器中,并没有使用虚拟 DOM,而且使用了增量 DOM。</p><h2 id="增量-DOM"><a href="#增量-DOM" class="headerlink" title="增量 DOM"></a>增量 DOM</h2><p>前面在<a href="https://godbasin.github.io/2021/08/15/angular-design-ivy-0-design/">《Angular 框架解读–Ivy 编译器整体设计》</a>一文中,我有介绍在 Ivy 编译器里,模板编译后的产物与 View Engine 不一样了,这是为了支持单独编译、增量编译等能力。</p><p>比如,<code><span>My name is </span></code>这句模板代码,在 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>可以看到,相比于 View Engine 中的<code>elementDef(0,null,null,1,'span',...),</code>,<code>elementStart()</code>、<code>elementEnd()</code>这些 API 显得更加清爽,它们使用的便是增量 DOM 的设计。</p><h3 id="增量-DOM-vs-虚拟-DOM"><a href="#增量-DOM-vs-虚拟-DOM" class="headerlink" title="增量 DOM vs 虚拟 DOM"></a>增量 DOM vs 虚拟 DOM</h3><p>虚拟 DOM 想必大家都已经有所了解,它的核心计算过程包括:</p><ol><li>用 JavaScript 对象模拟 DOM 树,得到一棵虚拟 DOM 树。</li><li>当页面数据变更时,生成新的虚拟 DOM 树,比较新旧两棵虚拟 DOM 树的差异。</li><li>把差异应用到真正的 DOM 树上。</li></ol><p>虽然虚拟 DOM 解决了页面被频繁更新和渲染带来的性能问题,但传统虚拟 DOM 依然有以下性能瓶颈:</p><ul><li>在单个组件内部依然需要遍历该组件的整个虚拟 DOM 树</li><li>在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费</li><li>递归遍历和更新逻辑容易导致 UI 渲染被阻塞,用户体验下降</li></ul><p>针对这些情况,React 和 Vue 等框架也有更多的优化,比如 React 中分别对 tree diff、component diff 以及 element diff 进行了算法优化,同时引入了任务调度来控制状态更新的计算和渲染。在 Vue 3.0 中,则将虚拟 DOM 的更新从以前的整体作用域调整为树状作用域,树状的结构会带来算法的简化以及性能的提升。</p><p>而不管怎样,虚拟 DOM 的设计中存在一个无法避免的问题:每个渲染操作分配一个新的虚拟 DOM 树,该树至少大到足以容纳发生变化的节点,并且通常更大一些,这样的设计会导致更多的一些内存占用。当大型虚拟 DOM 树需要大量更新时,尤其是在内存受限的移动设备上,性能可能会受到影响。</p><p>增量 DOM 的设计核心思想是:</p><ol><li>在创建新的(虚拟)DOM 树时,沿着现有的树走,并在进行时找出更改。</li><li>如果没有变化,则不分配内存;</li><li>如果有,改变现有树(仅在绝对必要时分配内存)并将差异应用到物理 DOM。</li></ol><p>这里将(虚拟)放在括号中是因为,当将预先计算的元信息混合到现有 DOM 节点中时,使用物理 DOM 树而不是依赖虚拟 DOM 树实际上已经足够快了。</p><p>与基于虚拟 DOM 的方法相比,增量 DOM 有两个主要优势:</p><ul><li>增量特性允许在渲染过程中显着减少内存分配,从而实现更可预测的性能</li><li>它很容易映射到基于模板的方法。控制语句和循环可以与元素和属性声明自由混合</li></ul><p>增量 DOM 的设计由 Google 提出,同时他们也提供了一个开源库 <a href="https://github.com/google/incremental-dom" target="_blank" rel="noopener">google/incremental-dom</a>,它是一个用于表达和应用 DOM 树更新的库。JavaScript 可用于提取、迭代数据并将其转换为生成 HTMLElements 和 Text 节点的调用。</p><p>但新的 Ivy 引擎没有直接使用它,而是实现了自己的版本。</p><h2 id="Ivy-中的增量-DOM"><a href="#Ivy-中的增量-DOM" class="headerlink" title="Ivy 中的增量 DOM"></a>Ivy 中的增量 DOM</h2><p>Ivy 引擎基于增量 DOM 的概念,它与虚拟 DOM 方法的不同之处在于,diff 操作是针对 DOM 增量执行的(即一次一个节点),而不是在虚拟 DOM 树上执行。基于这样的设计,增量 DOM 与 Angular 中的脏检查机制其实能很好地搭配。</p><h3 id="增量-DOM-元素创建"><a href="#增量-DOM-元素创建" class="headerlink" title="增量 DOM 元素创建"></a>增量 DOM 元素创建</h3><p>增量 DOM 的 API 的一个独特功能是它分离了标签的打开(<code>elementStart</code>)和关闭(<code>elementEnd</code>),因此它适合作为模板语言的编译目标,这些语言允许(暂时)模板中的 HTML 不平衡(比如在单独的模板中,打开和关闭的标签)和任意创建 HTML 属性的逻辑。</p><p>在 Ivy 中,使用<code>elementStart</code>和<code>elementEnd</code>创建一个空的 Element 实现如下(在 Ivy 中,<code>elementStart</code>和<code>elementEnd</code>的具体实现便是<code>ɵɵelementStart</code>和<code>ɵɵelementEnd</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="function"><span class="keyword">function</span> ɵɵ<span class="title">element</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> index: <span class="built_in">number</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> name: <span class="built_in">string</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> attrsIndex?: <span class="built_in">number</span> | <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> localRefsIndex?: <span class="built_in">number</span></span></span></span><br><span class="line"><span class="function"><span class="params"></span>): <span class="title">void</span> </span>{</span><br><span class="line"> ɵɵelementStart(index, name, attrsIndex, localRefsIndex);</span><br><span class="line"> ɵɵelementEnd();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中,<code>ɵɵelementStart</code>用于创建 DOM 元素,该指令后面必须跟有<code>ɵɵelementEnd()</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">elementStart</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> index: <span class="built_in">number</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> name: <span class="built_in">string</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> attrsIndex?: <span class="built_in">number</span> | <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> localRefsIndex?: <span class="built_in">number</span></span></span></span><br><span class="line"><span class="function"><span class="params"></span>): <span class="title">void</span> </span>{</span><br><span class="line"> <span class="keyword">const</span> lView = getLView();</span><br><span class="line"> <span class="keyword">const</span> tView = getTView();</span><br><span class="line"> <span class="keyword">const</span> adjustedIndex = HEADER_OFFSET + index;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> renderer = lView[RENDERER];</span><br><span class="line"> <span class="comment">// 此处创建 DOM 元素</span></span><br><span class="line"> <span class="keyword">const</span> native = (lView[adjustedIndex] = createElementNode(</span><br><span class="line"> renderer,</span><br><span class="line"> name,</span><br><span class="line"> getNamespace()</span><br><span class="line"> ));</span><br><span class="line"> <span class="comment">// 获取 TNode</span></span><br><span class="line"> <span class="comment">// 在第一次模板传递中需要收集匹配</span></span><br><span class="line"> <span class="keyword">const</span> tNode = tView.firstCreatePass ?</span><br><span class="line"> elementStartFirstCreatePass(</span><br><span class="line"> adjustedIndex, tView, lView, native, name, attrsIndex, localRefsIndex) :</span><br><span class="line"> tView.data[adjustedIndex] <span class="keyword">as</span> TElementNode;</span><br><span class="line"> setCurrentTNode(tNode, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> mergedAttrs = tNode.mergedAttrs;</span><br><span class="line"> <span class="comment">// 通过推断的渲染器,将所有属性值分配给提供的元素</span></span><br><span class="line"> <span class="keyword">if</span> (mergedAttrs !== <span class="literal">null</span>) {</span><br><span class="line"> setUpAttributes(renderer, native, mergedAttrs);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 将 className 写入 RElement</span></span><br><span class="line"> <span class="keyword">const</span> classes = tNode.classes;</span><br><span class="line"> <span class="keyword">if</span> (classes !== <span class="literal">null</span>) {</span><br><span class="line"> writeDirectClass(renderer, native, classes);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 将 cssText 写入 RElement</span></span><br><span class="line"> <span class="keyword">const</span> styles = tNode.styles;</span><br><span class="line"> <span class="keyword">if</span> (styles !== <span class="literal">null</span>) {</span><br><span class="line"> writeDirectStyle(renderer, native, styles);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> ((tNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached) {</span><br><span class="line"> <span class="comment">// 添加子元素</span></span><br><span class="line"> appendChild(tView, lView, native, tNode);</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">// 以便稍后可以使用任何元素发现实用程序方法检查元素</span></span><br><span class="line"> <span class="keyword">if</span> (getElementDepthCount() === <span class="number">0</span>) {</span><br><span class="line"> attachPatchData(native, lView);</span><br><span class="line"> }</span><br><span class="line"> increaseElementDepthCount();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 对指令 Host 的处理</span></span><br><span class="line"> <span class="keyword">if</span> (isDirectiveHost(tNode)) {</span><br><span class="line"> createDirectivesInstances(tView, lView, tNode);</span><br><span class="line"> executeContentQueries(tView, tNode, lView);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 获取本地名称和索引的列表,并将解析的本地变量值按加载到模板中的相同顺序推送到 LView</span></span><br><span class="line"> <span class="keyword">if</span> (localRefsIndex !== <span class="literal">null</span>) {</span><br><span class="line"> saveResolvedLocalsInData(lView, tNode);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,在<code>ɵɵelementStart</code>创建 DOM 元素的过程中,主要依赖于<code>LView</code>、<code>TView</code>和<code>TNode</code>。</p><p>在 Angular Ivy 中,使用了<code>LView</code>和<code>TView.data</code>来管理和跟踪渲染模板所需要的内部数据。对于<code>TNode</code>,在 Angular 中则是用于在特定类型的所有模板之间共享的特定节点的绑定数据(享元)。关于视图数据相关内容,之前在<a href="https://godbasin.github.io/2021/09/19/angular-design-ivy-1-view-data-and-node-injector/">《Angular 框架解读–Ivy 编译器的视图数据和依赖解析》</a>一节便介绍过了,因此这里不再做详细的介绍。</p><p><code>ɵɵelementEnd()</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">export</span> <span class="function"><span class="keyword">function</span> ɵɵ<span class="title">elementEnd</span>(<span class="params"></span>): <span class="title">void</span> </span>{}</span><br></pre></td></tr></table></figure><p>对于<code>ɵɵelementEnd()</code>的详细实现不过多介绍,基本上主要包括一些对 Class 和样式中<code>@input</code>等指令的处理,循环遍历提供的<code>tNode</code>上的指令、并将要运行的钩子排入队列,元素层次的处理等等。</p><h3 id="组件创建与增量-DOM-指令"><a href="#组件创建与增量-DOM-指令" class="headerlink" title="组件创建与增量 DOM 指令"></a>组件创建与增量 DOM 指令</h3><p>在增量 DOM 中,每个组件都被编译成一系列指令。这些指令创建 DOM 树并在数据更改时就地更新它们。</p><p>Ivy 在运行时编译一个组件的过程中,会创建模板解析相关指令:</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="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">compileComponentFromMetadata</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> meta: R3ComponentMetadata,</span></span></span><br><span class="line"><span class="function"><span class="params"> constantPool: ConstantPool,</span></span></span><br><span class="line"><span class="function"><span class="params"> bindingParser: BindingParser</span></span></span><br><span class="line"><span class="function"><span class="params"></span>): <span class="title">R3ComponentDef</span> </span>{</span><br><span class="line"> <span class="comment">// 其他暂时省略</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 创建一个 TemplateDefinitionBuilder,用于创建模板相关的处理</span></span><br><span class="line"> <span class="keyword">const</span> templateBuilder = <span class="keyword">new</span> TemplateDefinitionBuilder(</span><br><span class="line"> constantPool, BindingScope.createRootScope(), <span class="number">0</span>, templateTypeName, <span class="literal">null</span>, <span class="literal">null</span>, templateName,</span><br><span class="line"> directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,</span><br><span class="line"> meta.relativeContextFilePath, meta.i18nUseExternalIds);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 创建模板解析相关指令,包括:</span></span><br><span class="line"> <span class="comment">// 第一轮:创建模式,包括所有创建模式指令(例如解析侦听器中的绑定)</span></span><br><span class="line"> <span class="comment">// 第二轮:绑定和刷新模式,包括所有更新模式指令(例如解析属性或文本绑定)</span></span><br><span class="line"> <span class="keyword">const</span> templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 提供这个以便动态生成的组件在实例化时,知道哪些投影内容块要传递给组件</span></span><br><span class="line"> <span class="keyword">const</span> ngContentSelectors = templateBuilder.getNgContentSelectors();</span><br><span class="line"> <span class="keyword">if</span> (ngContentSelectors) {</span><br><span class="line"> definitionMap.set(<span class="string">"ngContentSelectors"</span>, ngContentSelectors);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 生成 ComponentDef 的 consts 部分</span></span><br><span class="line"> <span class="keyword">const</span> { constExpressions, prepareStatements } = templateBuilder.getConsts();</span><br><span class="line"> <span class="keyword">if</span> (constExpressions.length > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">let</span> constsExpr: o.LiteralArrayExpr|o.FunctionExpr = o.literalArr(constExpressions);</span><br><span class="line"> <span class="comment">// 将 consts 转换为函数</span></span><br><span class="line"> <span class="keyword">if</span> (prepareStatements.length > <span class="number">0</span>) {</span><br><span class="line"> constsExpr = o.fn([], [...prepareStatements, <span class="keyword">new</span> o.ReturnStatement(constsExpr)]);</span><br><span class="line"> }</span><br><span class="line"> definitionMap.set(<span class="string">"consts"</span>, constsExpr);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 生成 ComponentDef 的 template 部分</span></span><br><span class="line"> definitionMap.set(<span class="string">"template"</span>, templateFunctionExpression);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可见,在组件编译时,会被编译成一系列的指令,包括<code>const</code>、<code>vars</code>、<code>directives</code>、<code>pipes</code>、<code>styles</code>、<code>changeDetection</code>等等,当然也包括<code>template</code>模板里的相关指令。最终生成的这些指令,会体现在编译后的组件中,比如前面<a href>《Angular 框架解读–Ivy 编译器之心智模型》</a>中提到的这样一个<code>Component</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">import</span> { Component, Input } <span class="keyword">from</span> <span class="string">"@angular/core"</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span>({</span><br><span class="line"> selector: <span class="string">"greet"</span>,</span><br><span class="line"> template: <span class="string">"<div> Hello, {{name}}! </div>"</span>,</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> GreetComponent {</span><br><span class="line"> <span class="meta">@Input</span>() name: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>经<code>ngtsc</code>编译后,产物包括该组件的<code>.js</code>文件:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> i0 = <span class="built_in">require</span>(<span class="string">"@angular/core"</span>);</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">GreetComponent</span> </span>{}</span><br><span class="line">GreetComponent.ɵcmp = i0.ɵɵdefineComponent({</span><br><span class="line"> type: GreetComponent,</span><br><span class="line"> tag: <span class="string">"greet"</span>,</span><br><span class="line"> factory: <span class="function"><span class="params">()</span> =></span> <span class="keyword">new</span> GreetComponent(),</span><br><span class="line"> template: <span class="function"><span class="keyword">function</span> (<span class="params">rf, ctx</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (rf & RenderFlags.Create) {</span><br><span class="line"> i0.ɵɵelementStart(<span class="number">0</span>, <span class="string">"div"</span>);</span><br><span class="line"> i0.ɵɵtext(<span class="number">1</span>);</span><br><span class="line"> i0.ɵɵelementEnd();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (rf & RenderFlags.Update) {</span><br><span class="line"> i0.ɵɵadvance(<span class="number">1</span>);</span><br><span class="line"> i0.ɵɵtextInterpolate1(<span class="string">"Hello "</span>, ctx.name, <span class="string">"!"</span>);</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>其中,<code>elementStart()</code>、<code>text()</code>、<code>elementEnd()</code>、<code>advance()</code>、<code>textInterpolate1()</code>这些都是增量 DOM 相关的指令。在实际创建组件的时候,其<code>template</code>模板函数也会被执行,相关的指令也会被执行。</p><p>正因为在 Ivy 中,是由组件来引用着相关的模板指令。如果组件不引用某个指令,则我们的 Angular 中永远不会使用到它。因为组件编译的过程发生在编译过程中,因此我们可以根据引用到指令,来排除未引用的指令,从而可以在 Tree-shaking 过程中,将未使用的指令从包中移除,这便是增量 DOM 可树摇的原因。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>现在,我们已经知道在 Ivy 中,是通过编译器将模板编译为<code>template</code>渲染函数,其中会将对模板的解析编译成增量 DOM 相关的指令。其中,在<code>elementStart()</code>执行时,我们可以看到会通过<code>createElementNode()</code>方法来创建 DOM。实际上,增量 DOM 的设计远不止只是创建 DOM,还包括变化检测等各种能力,关于具体的渲染过程,我们会在下一讲中进行介绍。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://medium.com/google-developers/introducing-incremental-dom-e98f79ce2c5f" target="_blank" rel="noopener">Introducing Incremental DOM</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://blog.nrwl.io/understanding-angular-ivy-incremental-dom-and-virtual-dom-243be844bf36" target="_blank" rel="noopener">Understanding Angular Ivy: Incremental DOM and Virtual DOM</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中的增量 DOM 设计。</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>前端这几年--13.关于技术开发的职业发展</title>
<link href="https://godbasin.github.io/2021/11/28/about-front-end-13/"/>
<id>https://godbasin.github.io/2021/11/28/about-front-end-13/</id>
<published>2021-11-28T06:05:03.000Z</published>
<updated>2021-11-28T06:08:49.570Z</updated>
<content type="html"><![CDATA[<p>日经贴:程序员的 35 岁是天花板吗?35 岁以上的程序员都在做些什么?技术开发的职业路线要怎么规划?</p><a id="more"></a><h2 id="技术开发的职业发展道路到底该怎么走?"><a href="#技术开发的职业发展道路到底该怎么走?" class="headerlink" title="技术开发的职业发展道路到底该怎么走?"></a>技术开发的职业发展道路到底该怎么走?</h2><p>现在的互联网团队虽然很多,但也有很多人在不断涌入这个行业,导致竞争日益激烈。对于很多企业来说,钱少能熬的年轻人似乎是更好的选择,因此程序员群体中也流传着“35 岁就会被淘汰”的说法。</p><p>曾经有一段时间,我遇到了一些比较难过的坎,于是认真学习了一些关于心理健康的知识,尝试好好调整自己。那时候便想,如今的社会发展和变化的速度太快,大多数事情都被要求“快速”、“高效”,未来很多人会选择进入企业打工而不是自己创业,各种精神上的压力都不小,焦虑和抑郁的情绪也逐渐变多,或许心理健康也会越来越受到重视,且被未来的社会高度依赖了。</p><p>焦虑和压力,如今成了当代快餐文化的赠品,技术开发也是如此。或许国外不少的程序员可以一直作为前线开发,而国内情况则不同,很多时候大家对于工作经验更多的技术开发的要求,不再满足于完成一线开发的工作,而是要求他给企业和团队带来更大的价值,比如做技术架构、技术管理、技术输出、影响力建设等等。</p><p>那么,面临这样的大背景,作为走在路上都会被人群淹没的普通开发,留给我们的机会又有哪些呢?</p><h3 id="技术开发的深度和广度"><a href="#技术开发的深度和广度" class="headerlink" title="技术开发的深度和广度"></a>技术开发的深度和广度</h3><p>很多技术开发在谈到职业规划的时候,都会考虑到一个点:技术的广度和深度到底哪个更重要更适合自己?</p><p>过去的我觉得,走深度还是广度,和一个人的喜好与规划有关系,比如,如果以后想当独立开发显然各种技术都要有所涉猎,如果想在某个技术领域扎根则应该要深挖。</p><p>我在之前的团队有接触过一些全栈开发,实际上不同领域的知识体系有差异,但大多数在工作中也做各自领域中比较普遍的事情(比如前端写页面调样式,后台写 CURD 逻辑),而全栈开发更是难有精力和心思去解决深入的问题。我也因为技术深度的原因来到了现在的团队,现在的业务场景的确已经是前端领域中复杂性排名很前的,慢慢发现其实再复杂问题,也都可以将其梳理并一一拆解,然后再逐个击破去解决。</p><p>追求技术广度,在工作中接触的技术领域会很多很杂,其挑战点在于是否可以对不同领域的技术知识进行足够的思考和归纳,找出不同技术的共通点以及各自的特点,并能选择合适的技术去解决不同的问题。追求技术深度,则需要在某个足够复杂的领域中,将其逐一拆解,逐一解决后,还能将新获得的知识再次归纳整合,优化在该领域的技术网络。</p><p>所以,我们常说开发的技术能力,其实更多时候是由各自的开发经验和项目经历决定,不管是广度还是深度,都有其可以出彩的地方,也有或许会让人觉得无趣的时候。而作为技术开发,唯一要避免的是 1 年的工作经验当 10 年用,这样即使工作了 10 年,也只是原地踏步。</p><p>而<a href="https://godbasin.github.io/2021/11/12/about-front-end-12/">上一篇</a>我有讲到关于开发的技术门槛,其实在大多数开发的工作中,往往被低估的能力不是技术能力,而是工作能力(如沟通能力、理解能力、复盘能力、表达能力等等),不管技术能力如何,工作能力会更加直接地影响到我们的工作效果。</p><h3 id="大公司的生存策略"><a href="#大公司的生存策略" class="headerlink" title="大公司的生存策略"></a>大公司的生存策略</h3><p>我有时候会思考,像如今所谓 BAT、TMDJ 这些比较大的公司,到底喜欢怎样的员工呢?</p><p>一开始,我认为他们不喜欢特立独行的员工,实际上也大多如此。大公司里有很多部门和团队,但不管在怎样的团队,除了某些情况下会遇到技术突破的场景,大多数时候对技术开发的要求都是“快速”实现产品/老板需求,此时“听话”的员工会更“配合”和“响应”这样的快速变更。</p><p>但光“努力”和“听话”这样的品质显然是不够的。我遇到过好几个自己还挺喜欢的开发,他们认真负责、好学努力,也十分听从组织安排,却常常被甩锅和嫌弃不够“机灵”而被打低考核/开除。在我看来,他们只是缺乏一些职场经验,不够“油滑”,不懂得拒绝和保护自己而已,并不存在所谓“不够机灵”的情况。</p><p>我见过很多团队在招人的时候,都喜欢说要招“聪明”的开发。我不喜欢用是否“聪明”这样的词语去描述其他人,我也不认为我们有资格去给别人贴这样的标签。</p><p>这样算来,大公司的团队对开发的要求,不仅需要“努力”和“听话”,还需要足够“机灵”和“聪明”。这其中甚至没有多少是与“技术能力”有关系的,当然前面我也有讲过,对于开发来说,技术能力已经属于必备能力了。</p><p>除此之外,<a href="https://godbasin.github.io/2021/10/10/about-front-end-11/">前面</a>我也有提到过,大多数大公司内的技术开发,相比真正做好一个产品,更多时候会更优先考虑自身晋级/考核的情况,常常会事与愿违地做一些未必最适合业务场景,但更适合拿出去讲(吹水)的技术方案。</p><h3 id="小团队的技术天花板"><a href="#小团队的技术天花板" class="headerlink" title="小团队的技术天花板"></a>小团队的技术天花板</h3><p>以前我觉得大团队很厉害,比如光前端开发就有一百多号人,肯定有不少的技术积累和沉淀,也能学到不少的东西。一般来说,复杂场景的业务才需要用到这么多某个领域的开发,实际上大团队对团队管理和氛围的要求更高,毕竟鸟儿大了什么林子都有。</p><p>我有时候也会想,是不是选一个好点的小团队问题就少一些了,不过大多数小团队在业务发展迅速的时候,最终都会演变成大团队,能将团队控制在小团队协作的情况不多。如果只想呆在小团队里,可能只有少量的垂直领域业务、发展平稳的业务,或者运气好的话,就不会被各种疯狂变动卷到。</p><p>小团队开发的好处,在于少了很多不必要的沟通和协作,工作效率会好很多。当然,其实这也跟团队和业务有密切的关系,比如团队人员是否频繁变更,业务方向是否来回反复,这些都和团队和业务管理方式有直接的关系。从职业发展方向来说,如果遇上团队快速发展,(如果你希望的话)小团队可以有更多的机会往技术管理方向走。</p><p>很多人都觉得小团队肯定有技术天花板,认为大团队的技术能力肯定更好,其实这没有必然的联系。</p><p>对于想往管理方向的开发来说,小团队可能的确只能管理较少的人(除非快速扩招),而大团队则可以继续往上升。对于一线开发来说,其实团队的大小对个人影响不是非常大,直接影响到个人体验的,大概除了工资待遇,便是工作氛围、加班情况、开发效率和节奏等等,而这些未必和团队的规模有直接的关系。</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>说了很多,不管是技术路线该往深度还是广度走,还是该去大团队还是小团队,似乎都没有最优解。实际上,做技术开发这一行的很多人都未必出于热爱,我们也经常见到有人辞职去创业、考公务员、做老师。</p><p>如果问我,我觉得不管是被行业淘汰了、或对行业失去热情了,还是浑浑噩噩地一直焦虑地工作着,或是有了新的方向。不管什么时候,都要摆正自己的心态,只要人还在,只要还愿意继续尝试,一切都可以重新开始。不用太在意当前的状况,也不用着急和其他人比较,我们的人生还有很长的路在走,还有无数的机会让它变得有趣而精彩。</p>]]></content>
<summary type="html">
<p>日经贴:程序员的 35 岁是天花板吗?35 岁以上的程序员都在做些什么?技术开发的职业路线要怎么规划?</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框架解读--Ivy编译器之AOT/JIT</title>
<link href="https://godbasin.github.io/2021/11/21/angular-design-ivy-4-aot-jit/"/>
<id>https://godbasin.github.io/2021/11/21/angular-design-ivy-4-aot-jit/</id>
<published>2021-11-21T07:18:34.000Z</published>
<updated>2021-11-21T07:21:17.040Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要介绍 Angular 中的 AOT 和 JIT 相关设计。</p><a id="more"></a><p>Angular 应用主要由组件及其 HTML 模板组成。由于浏览器无法直接理解 Angular 所提供的组件和模板,因此 Angular 应用程序需要先进行编译才能在浏览器中运行。</p><p>在 Angular 中,提供了两种方式编译 Angular 应用:</p><ul><li>即时编译 (JIT,Just in time):它会在运行期间在浏览器中编译你的应用</li><li>预先编译(AOT,Ahead of Time):它会在构建时编译你的应用和库</li></ul><h2 id="JIT"><a href="#JIT" class="headerlink" title="JIT"></a>JIT</h2><p>在 Angular 8 及更早版本中,默认情况下,在应用程序执行期间,将对模板进行编译,这便是 JIT 编译。</p><h3 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h3><p>JIT 编译相对 AOT 而言比较简单,核心逻辑在<code>JitCompiler</code>中。<code>JitCompiler</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><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">export</span> <span class="keyword">class</span> JitCompiler {</span><br><span class="line"> <span class="comment">// 编译过程中的一些解析内容缓存</span></span><br><span class="line"> <span class="keyword">private</span> _compiledTemplateCache = <span class="keyword">new</span> Map<Type, CompiledTemplate>();</span><br><span class="line"> <span class="keyword">private</span> _compiledHostTemplateCache = <span class="keyword">new</span> Map<Type, CompiledTemplate>();</span><br><span class="line"> <span class="keyword">private</span> _compiledDirectiveWrapperCache = <span class="keyword">new</span> Map<Type, Type>();</span><br><span class="line"> <span class="keyword">private</span> _compiledNgModuleCache = <span class="keyword">new</span> Map<Type, object>();</span><br><span class="line"> <span class="keyword">private</span> _sharedStylesheetCount = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> _addedAotSummaries = <span class="keyword">new</span> Set<<span class="function"><span class="params">()</span> =></span> <span class="built_in">any</span>[]>();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模块编译相关方法</span></span><br><span class="line"> compileModuleSync(moduleType: Type): object {}</span><br><span class="line"> compileModuleAsync(moduleType: Type): <span class="built_in">Promise</span><object> {}</span><br><span class="line"> compileModuleAndAllComponentsSync(</span><br><span class="line"> moduleType: Type</span><br><span class="line"> ): ModuleWithComponentFactories {}</span><br><span class="line"> compileModuleAndAllComponentsAsync(</span><br><span class="line"> moduleType: Type</span><br><span class="line"> ): <span class="built_in">Promise</span><ModuleWithComponentFactories> {}</span><br><span class="line"> getComponentFactory(component: Type): object {}</span><br><span class="line"> loadAotSummaries(summaries: <span class="function"><span class="params">()</span> =></span> <span class="built_in">any</span>[]) {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于运行时编译,Angular 会传递并编译所有模块,因此在编译模块过程中,还需要为所有嵌套模块加载声明的指令/管道。</p><p>实际上,在 JIT 中编译这些模块的过程中,需要依赖模块、组件、指令等装饰器的元数据,该过程在 AOT 中是构建时便完成了编译,在 JIT 中由于组件是动态加载和编译的,因此也需要在模板编译过程进行解析和维护。对装饰器中元数据的编译和管理,可参考<a href="https://godbasin.github.io/2021/03/27/angular-design-metadata/">《Angular 框架解读–元数据和装饰器》</a>一文。</p><h3 id="JIT-优势"><a href="#JIT-优势" class="headerlink" title="JIT 优势"></a>JIT 优势</h3><p>在运行时编译代码,这意味着它不会在构建时进行编译,而是在调用该组件时编译。JIT 在本地调试的情况下,会更有优势:</p><ol><li>在 JIT 模式下,并非所有代码都会在初始时间编译。只有在应用程序启动时需要的必要组件才会被编译,如果项目中需要某功能并且它不在已编译的代码中,才会编译该功能或组件。</li><li>JIT 有助于减轻 CPU 的负担,并使应用程序渲染速度更快。</li><li>使用 JIT 模式和映射文件编译代码,可以在检查模式下查看并链接到源代码。</li></ol><p>在执行时,Angular 编译器会将这些模板转换为 JavaScript 函数。在一个简单的应用程序中,JIT 编译将生成两个包:</p><ul><li>main.bundle.js : 63k (21k 缩小)</li><li>vendor.bundle.js : 3321k (960k 缩小)</li></ul><p>对<code>vendor.bundle.js</code>文件(使用<code>source-map-explorer</code>)的分析表明,Angular 编译器占总包大小的 35%。这种机制有两个缺点:</p><ol><li>JavaScript 包太重(显然是因为应用程序源需要在文件<code>vendor.bundle.js</code>中包含编译器)。</li><li>应用程序将在运行时编译模板,这会影响渲染时间。</li></ol><p>因此,Angular 提供了 AOT 编译,并在 Angular 9 及后续版本中将其设置为默认值。</p><h2 id="AOT"><a href="#AOT" class="headerlink" title="AOT"></a>AOT</h2><p>在浏览器下载和运行代码之前的编译阶段,Angular 预先(AOT)编译器会先把 Angular HTML 和 TypeScript 代码转换成高效的 JavaScript 代码。</p><h3 id="工作原理-1"><a href="#工作原理-1" class="headerlink" title="工作原理"></a>工作原理</h3><p>实际上,前面我们介绍的 Ivy 编译器中心智模型(参考<a href="https://godbasin.github.io/2021/11/06/angular-design-ivy-3-mental-model/">《Angular 框架解读–Ivy 编译器之心智模型》</a>),便是 AOT 的主要工作原理。主要包括:</p><ul><li>Angular AOT 编译器会提取元数据,来解释应由 Angular 管理的应用程序部分</li><li>通过在装饰器(例如<code>@Component()</code>)中显式指定元数据,也可以在被装饰的类的构造函数声明中隐式指定元数据</li><li>元数据告诉 Angular 要如何构造应用程序类的实例并在运行时与它们进行交互</li></ul><p>至于对装饰器中元数据的处理和编译过程,主要是通过将 Angular 编译集成到 TypeScript 编译器的编译流程中来实现。前面的<a href="https://godbasin.github.io/2021/08/15/angular-design-ivy-0-design/">Angular 框架解读–Ivy 编译器</a>系列文章有介绍,因此这里也不过多展开。</p><p>同样,我们可以找到<code>AotCompiler</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> AotCompiler {</span><br><span class="line"> <span class="keyword">private</span> _templateAstCache =</span><br><span class="line"> <span class="keyword">new</span> Map<StaticSymbol, {template: TemplateAst[], pipes: CompilePipeSummary[]}>();</span><br><span class="line"> <span class="keyword">private</span> _analyzedFiles = <span class="keyword">new</span> Map<<span class="built_in">string</span>, NgAnalyzedFile>();</span><br><span class="line"> <span class="keyword">private</span> _analyzedFilesForInjectables = <span class="keyword">new</span> Map<<span class="built_in">string</span>, NgAnalyzedFileWithInjectables>();</span><br><span class="line"></span><br><span class="line"> analyzeModulesSync(rootFiles: <span class="built_in">string</span>[]): NgAnalyzedModules {}</span><br><span class="line"> analyzeModulesAsync(rootFiles: <span class="built_in">string</span>[]): <span class="built_in">Promise</span><NgAnalyzedModules> {}</span><br><span class="line"> findGeneratedFileNames(fileName: <span class="built_in">string</span>): <span class="built_in">string</span>[] {}</span><br><span class="line"></span><br><span class="line"> emitBasicStub(genFileName: <span class="built_in">string</span>, originalFileName?: <span class="built_in">string</span>): GeneratedFile {}</span><br><span class="line"> emitTypeCheckStub(genFileName: <span class="built_in">string</span>, originalFileName: <span class="built_in">string</span>): GeneratedFile|<span class="literal">null</span> {}</span><br><span class="line"> loadFilesAsync(fileNames: <span class="built_in">string</span>[], tsFiles: <span class="built_in">string</span>[]): <span class="built_in">Promise</span><</span><br><span class="line"> {analyzedModules: NgAnalyzedModules, analyzedInjectables: NgAnalyzedFileWithInjectables[]}> {}</span><br><span class="line"> loadFilesSync(fileNames: <span class="built_in">string</span>[], tsFiles: <span class="built_in">string</span>[]):</span><br><span class="line"> {analyzedModules: NgAnalyzedModules, analyzedInjectables: NgAnalyzedFileWithInjectables[]} {}</span><br><span class="line"></span><br><span class="line"> emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: <span class="built_in">string</span>|<span class="literal">null</span>): MessageBundle {}</span><br><span class="line"> emitAllPartialModules2(files: NgAnalyzedFileWithInjectables[]): PartialModule[] {}</span><br><span class="line"> emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] {}</span><br><span class="line"></span><br><span class="line"> listLazyRoutes(entryRoute?: <span class="built_in">string</span>, analyzedModules?: NgAnalyzedModules): LazyRoute[] {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从对外提供的方法来看,相比于<code>JitCompiler</code>,显然<code>AotCompiler</code>并没有什么编译的过程,更多是解析文件并创建组件。两个<code>Compiler</code>相差很远,但我们可以找到同样包含的一个<code>_compileModule</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> AotCompiler {</span><br><span class="line"> <span class="keyword">private</span> _compileModule(outputCtx: OutputContext, ngModule: CompileNgModuleMetadata): <span class="built_in">void</span> {</span><br><span class="line"> <span class="keyword">const</span> providers: CompileProviderMetadata[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>._options.locale) {</span><br><span class="line"> <span class="keyword">const</span> normalizedLocale = <span class="keyword">this</span>._options.locale.replace(<span class="regexp">/_/g</span>, <span class="string">'-'</span>);</span><br><span class="line"> providers.push({</span><br><span class="line"> token: createTokenForExternalReference(<span class="keyword">this</span>.reflector, Identifiers.LOCALE_ID),</span><br><span class="line"> useValue: normalizedLocale,</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">this</span>._options.i18nFormat) {</span><br><span class="line"> providers.push({</span><br><span class="line"> token: createTokenForExternalReference(<span class="keyword">this</span>.reflector, Identifiers.TRANSLATIONS_FORMAT),</span><br><span class="line"> useValue: <span class="keyword">this</span>._options.i18nFormat</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>._ngModuleCompiler.compile(outputCtx, ngModule, providers);</span><br><span class="line"> }</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> JitCompiler {</span><br><span class="line"> <span class="keyword">private</span> _compileModule(moduleType: Type): object {</span><br><span class="line"> <span class="keyword">let</span> ngModuleFactory = <span class="keyword">this</span>._compiledNgModuleCache.get(moduleType)!;</span><br><span class="line"> <span class="keyword">if</span> (!ngModuleFactory) {</span><br><span class="line"> <span class="keyword">const</span> moduleMeta = <span class="keyword">this</span>._metadataResolver.getNgModuleMetadata(moduleType)!;</span><br><span class="line"> <span class="keyword">const</span> extraProviders = <span class="keyword">this</span>.getExtraNgModuleProviders(moduleMeta.type.reference);</span><br><span class="line"> <span class="keyword">const</span> outputCtx = createOutputContext();</span><br><span class="line"> <span class="keyword">const</span> compileResult = <span class="keyword">this</span>._ngModuleCompiler.compile(outputCtx, moduleMeta, extraProviders);</span><br><span class="line"> ngModuleFactory = <span class="keyword">this</span>._interpretOrJit(</span><br><span class="line"> ngModuleJitUrl(moduleMeta), outputCtx.statements)[compileResult.ngModuleFactoryVar];</span><br><span class="line"> <span class="keyword">this</span>._compiledNgModuleCache.set(moduleMeta.type.reference, ngModuleFactory);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ngModuleFactory;</span><br><span class="line"> }</span><br><span class="line">|</span><br></pre></td></tr></table></figure><p>可以看到:</p><ul><li><code>AotCompiler</code>中更多是直接将作用域/上下文、元数据信息直接用于模块的创建,少了编译过程</li><li><code>JitCompiler</code>中会在运行时创建作用域、上下文,并通过编译过程获取需要的元数据,然后再进行模块的创建</li></ul><p>我们来分别看看 AOT 编译的三个阶段。</p><h3 id="AOT-编译阶段"><a href="#AOT-编译阶段" class="headerlink" title="AOT 编译阶段"></a>AOT 编译阶段</h3><p>AOT 编译分为三个阶段:</p><p><strong>一、代码分析。</strong>在此阶段,TypeScript 编译器和 AOT 收集器会创建源码的表现层。</p><p>TypeScript 编译器会做一些初步的分析工作,它会生成类型定义文件<code>.d.ts</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> StaticSymbolResolverHost {</span><br><span class="line"> <span class="comment">// 返回给定模块的 ModuleMetadata</span></span><br><span class="line"> <span class="comment">// Angular CLI 会在生成 .d.ts 文件并且模块导出变量或带有装饰器的类时为模块生成元数据</span></span><br><span class="line"> <span class="comment">// 模块元数据也可以通过在 tools/metadata 中使用 MetadataCollector 直接从 TypeScript 源生成</span></span><br><span class="line"> getMetadataFor(modulePath: <span class="built_in">string</span>): { [key: <span class="built_in">string</span>]: <span class="built_in">any</span> }[] | <span class="literal">undefined</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同时,AOT 收集器(<code>collector</code>)会记录 Angular 装饰器中的元数据,并把它们输出到<code>.metadata.json</code>文件(可以把<code>.metadata.json</code>文件看做一个包括全部装饰器的元数据的全景图)中,和每个<code>.d.ts</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 从 TypeScript 模块收集装饰器元数据</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> MetadataCollector {</span><br><span class="line"> <span class="comment">// 返回一个 JSON.stringify 友好形式</span></span><br><span class="line"> <span class="comment">// 描述源文件中导出的类的装饰器,该类预期与模块相对应</span></span><br><span class="line"> <span class="keyword">public</span> getMetadata(</span><br><span class="line"> sourceFile: ts.SourceFile,</span><br><span class="line"> strict: <span class="built_in">boolean</span> = <span class="literal">false</span>,</span><br><span class="line"> substituteExpression?: (</span><br><span class="line"> value: MetadataValue,</span><br><span class="line"> node: ts.Node</span><br><span class="line"> ) => MetadataValue</span><br><span class="line"> ): ModuleMetadata | <span class="literal">undefined</span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>收集器不会试图理解它收集并输出到<code>.metadata.json</code>中的元数据,它所能做的只是尽可能准确的表述这些元数据,并在检测到元数据中的语法违规时记录这些错误。解释这些<code>.metadata.json</code>是编译器在代码生成阶段要承担的工作。</p><p><strong>二、代码生成。</strong>在此阶段,编译器的<code>StaticReflector</code>会解释在 1 中收集的元数据,对元数据执行附加验证,如果检测到元数据违反了限制,则抛出错误。</p><p><code>StaticReflector</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> StaticReflector <span class="keyword">implements</span> CompileReflector {</span><br><span class="line"> <span class="comment">// 元数据相关的静态符号缓存</span></span><br><span class="line"> <span class="keyword">private</span> annotationCache = <span class="keyword">new</span> Map<StaticSymbol, <span class="built_in">any</span>[]>();</span><br><span class="line"> <span class="keyword">private</span> shallowAnnotationCache = <span class="keyword">new</span> Map<StaticSymbol, <span class="built_in">any</span>[]>();</span><br><span class="line"> <span class="keyword">private</span> propertyCache = <span class="keyword">new</span> Map<StaticSymbol, { [key: <span class="built_in">string</span>]: <span class="built_in">any</span>[] }>();</span><br><span class="line"> <span class="keyword">private</span> parameterCache = <span class="keyword">new</span> Map<StaticSymbol, <span class="built_in">any</span>[]>();</span><br><span class="line"> <span class="keyword">private</span> methodCache = <span class="keyword">new</span> Map<StaticSymbol, { [key: <span class="built_in">string</span>]: <span class="built_in">boolean</span> }>();</span><br><span class="line"> <span class="keyword">private</span> staticCache = <span class="keyword">new</span> Map<StaticSymbol, <span class="built_in">string</span>[]>();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 解释元数据</span></span><br><span class="line"> componentModuleUrl(typeOrFunc: StaticSymbol): <span class="built_in">string</span> {}</span><br><span class="line"> resolveExternalReference(</span><br><span class="line"> ref: o.ExternalReference,</span><br><span class="line"> containingFile?: <span class="built_in">string</span></span><br><span class="line"> ): StaticSymbol {}</span><br><span class="line"> findDeclaration(</span><br><span class="line"> moduleUrl: <span class="built_in">string</span>,</span><br><span class="line"> name: <span class="built_in">string</span>,</span><br><span class="line"> containingFile?: <span class="built_in">string</span></span><br><span class="line"> ): StaticSymbol {}</span><br><span class="line"> tryFindDeclaration(</span><br><span class="line"> moduleUrl: <span class="built_in">string</span>,</span><br><span class="line"> name: <span class="built_in">string</span>,</span><br><span class="line"> containingFile?: <span class="built_in">string</span></span><br><span class="line"> ): StaticSymbol {}</span><br><span class="line"> findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 验证元数据</span></span><br><span class="line"> <span class="keyword">public</span> tryAnnotations(<span class="keyword">type</span>: StaticSymbol): <span class="built_in">any</span>[] {}</span><br><span class="line"> <span class="keyword">public</span> annotations(<span class="keyword">type</span>: StaticSymbol): <span class="built_in">any</span>[] {}</span><br><span class="line"> <span class="keyword">public</span> shallowAnnotations(<span class="keyword">type</span>: StaticSymbol): <span class="built_in">any</span>[] {}</span><br><span class="line"> <span class="keyword">public</span> propMetadata(<span class="keyword">type</span>: StaticSymbol): { [key: <span class="built_in">string</span>]: <span class="built_in">any</span>[] } {}</span><br><span class="line"> <span class="keyword">public</span> parameters(<span class="keyword">type</span>: StaticSymbol): <span class="built_in">any</span>[] {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译器理解收集器支持的所有语法形式,但是它也可能拒绝那些虽然语法正确但语义违反了编译器规则的元数据。</p><p><strong>三、模板类型检查。</strong>在此可选阶段,Angular 模板编译器使用 TypeScript 编译器来验证模板中的绑定表达式。</p><p>Angular 编译器最有用的功能之一就是能够对模板中的表达式进行类型检查,在由于出错而导致运行时崩溃之前就捕获任何错误。在模板类型检查阶段,Angular 模板编译器会使用 TypeScript 编译器来验证模板中的绑定表达式。</p><p>当模板绑定表达式中检测到类型错误时,进行模板验证时就会生成错误。这和 TypeScript 编译器在处理<code>.ts</code>文件中的代码时报告错误很相似。</p><h3 id="AOT-的优势"><a href="#AOT-的优势" class="headerlink" title="AOT 的优势"></a>AOT 的优势</h3><p>显然,使用 AOT 编译有这些好处:</p><ol><li>更快的渲染:借助 AOT,浏览器可以下载应用的预编译版本。浏览器加载的是可执行代码,因此它可以立即渲染应用,而无需等待先编译好应用。</li><li>更少的异步请求:编译器会在应用 JavaScript 中内联外部 HTML 模板和 CSS 样式表,从而消除了对那些源文件的单独 ajax 请求。</li><li>较小的 Angular 框架下载大小:如果已编译应用程序,则无需下载 Angular 编译器。编译器大约是 Angular 本身的一半,因此省略编译器会大大减少应用程序的有效载荷。</li><li>尽早检测模板错误:AOT 编译器会在构建步骤中检测并报告模板绑定错误,然后用户才能看到它们。</li><li>更高的安全性:AOT 在将 HTML 模板和组件提供给客户端之前就将其编译为 JavaScript 文件。没有要读取的模板,没有潜藏风险的客户端 HTML 或 JavaScript eval,受到注入攻击的机会就更少了。</li></ol><p>在 AOT 模式下,生成的包不再包含 HTML 模板,而是直接包含已编译的模板。如果检查由构建生成的文件<code>main.bundle.js</code>,会发现包含已编译模板的代码部分。</p><p>在同一个应用程序中,AOT 编译生成以下包:</p><ul><li>main.bundle.js : 59k (27k 缩小)</li><li>vendor.bundle.js:2281k(610k 缩小)</li></ul><p>可以看到,<code>vendor.bundle.js</code>的大小大大减少,因为它不再包含编译器。这种编译的优点很明显:减少应用程序负载、更少的请求、快速渲染。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文介绍了 Angular 中的 JIT/AOT 编译过程和工作原理,看起来似乎这些都和 Ivy 编译器关系不大。实际上,要实现 JIT、AOT 编译,核心便是 Ivy 编译器。在 View Engine 中虽然也有 JIT/AOT 的两种模式,但不管是装饰器元数据的解析,还是模板编译过程中的类型错误检查,在 Ivy 编译器的设计里都有非常大的区别。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://angular.cn/guide/aot-compiler" target="_blank" rel="noopener">预先(AOT)编译器</a></li><li><a href="https://medium.com/@kadrimoujib/angular-jit-vs-aot-15e211d94966" target="_blank" rel="noopener">Angular JiT vs AoT</a></li></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要介绍 Angular 中的 AOT 和 JIT 相关设计。</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>前端这几年--12.技术开发的门槛高吗</title>
<link href="https://godbasin.github.io/2021/11/12/about-front-end-12/"/>
<id>https://godbasin.github.io/2021/11/12/about-front-end-12/</id>
<published>2021-11-12T13:35:32.000Z</published>
<updated>2021-11-12T13:36:38.252Z</updated>
<content type="html"><![CDATA[<p>最近对程序员这个职业产生了一些困惑,所以一个问题一个问题地记录下来叭~</p><a id="more"></a><h2 id="技术开发到底是门槛低还是高?"><a href="#技术开发到底是门槛低还是高?" class="headerlink" title="技术开发到底是门槛低还是高?"></a>技术开发到底是门槛低还是高?</h2><p>对于“程序员”这个职业,或许现在已经被大多数人认知,常常被认为是吃技术饭碗、工资高的一个工种。正所谓内行看门道外行看热闹,这两个标签都只能代表一部分开发。</p><p>有工资高的,自然就有工资低的。资本的本质就是商业化,通俗来说就是赚钱,因此只要存在可优化和压缩的可能性,都会被优化,而目前的技术开发大多数服务于资本。</p><h3 id="技术开发与研发"><a href="#技术开发与研发" class="headerlink" title="技术开发与研发"></a>技术开发与研发</h3><p>实际上,我们常常将“研发”和“开发”搞混。根据维基百科,研发并非旨在立即产生利润,而且通常具有更大的风险和不确定的投资回报。如今大多数互联网产品“研发”会对投入产出比要求更高,也会对盈利预期有所要求,讲究“快速试错”、“快速迭代”,对产品生命周期、准确来说是盈利周期,会有更高的要求,这个不行就撤了做下一个。</p><p><a href="https://en.wikipedia.org/wiki/Research_and_development" target="_blank" rel="noopener">维基百科</a>:</p><blockquote><p>新产品的设计和开发往往是公司生存的关键因素。在瞬息万变的全球工业格局中,公司必须不断修改其设计和产品范围。由于激烈的竞争和消费者不断变化的偏好,这也是必要的。如果没有研发计划,公司就必须依靠战略联盟、收购和网络来利用他人的创新。</p></blockquote><p>显然,如今很多大公司更倾向于后者,因为相比预期不确定的产品研发,这种方式可以更稳定地盈利。因此我们也常常会看到,愿意长期投入而不计成本地进行研发的团队很少,因为研发需要资金,更多时候都是通过融资、战略合作来解决资金问题。但这样依然会存在问题,投资方对盈利的预期,是否和产品本身能够匹配。</p><p>所以,很多时候我们提到“程序员”,其实大多数都属于开发而非研发。至于做开发是不是一个技术活,说实话,这玩意入门不难,但是做好不易。</p><h3 id="自学入门与培训班的红海"><a href="#自学入门与培训班的红海" class="headerlink" title="自学入门与培训班的红海"></a>自学入门与培训班的红海</h3><p>正因为入门不难,所以这些年我们也能看到无数培训班的出现。当然,培训效果显然不像宣传那么好,但对于一些对这个行业一概不知的人来说,有人引路和所谓培训是更快捷的方式。</p><p>培训班里做的事情,就是将一些网上的文章/博客/课程资源进行整合,然后按照课时整理成每节课/每天的计划,最重要的一点是,他们会针对面试的知识点做较多的培训,也会教学员简历该怎么写。(其实我没怎么了解过培训班,以上是我自己认为一个培训班应该会做的一些事情)。所以,对于知道如何获取资源的小白来说,自学入门也是可以做到的。</p><p>很多培训班都会有一些成功案例,多少人拿到了 offer,甚至如果有人进了大公司肯定都得上历史荣耀板了(如果有这么一个板的话)。这些成功案例的确能说明一些事情:这个培训班针对面试的知识体系准备/简历包装比较到位,但实际上开始工作之后,能拿到怎样的表现都只能靠自己了。</p><p>其实我觉得培训班里最需要教的一件事,就是要怎么通过搜索引擎,使用合适的关键字,找到有效的问题解决方法。因为在大多数开发的工作中遇到的问题,99% 都可以在网上找到办法解决,至于剩下的 1%,只需要重启 VsCode/App/浏览器/电脑,就可以解决。所以,对自学入门的开发来说,搜索也是直接影响工作能力和效率的一种能力。</p><h3 id="晋级-考核与技术能力的关系"><a href="#晋级-考核与技术能力的关系" class="headerlink" title="晋级/考核与技术能力的关系"></a>晋级/考核与技术能力的关系</h3><p>对于开发来说,职级则常常被认为是技术能力的标签。</p><p>不管是面试/找工作,还是开发与开发之间的交流,通常都会问到职级。什么阿里 P7/P8、腾讯 T10/T11,还有其他公司的(抱歉这块了解得不多),职级一般会与薪酬待遇挂钩,也会被动与开发的技术能力挂钩,所以晋级对开发来说是一件大事情。同样,考核也会和年终奖/待遇挂钩,因此也是开发中的大事情。拿到一个好的考核,顺利通过晋级答辩,可能就是互联网打工人比较开心的事情了。</p><p>或许有些人会认为,职级高的人肯定技术能力比较厉害,实际上也未必都是这样的。</p><p>我在工作中经历过两次晋级答辩,而唯一让我觉得自己能通过的原因,结论是只是运气好罢了。要怎么理解“运气好”这件事呢?大概就是你恰好在答辩前拿到了一个“容易答辩”的项目,然后做的结果还不错,答辩过程中恰好评委也认为有价值/做得不错,就通过了。</p><p>除此以外,老生常谈的 KPI 项目反而到处都是,很多人为了答辩而故意做一些项目(将原本简单的场景搞得很复杂、用高大上的术语包装等等),答辩通过后就甩手不管、让其他人维护,继续做下一个可以用来下次答辩的项目,这样的事情每天都在开发的世界里上演。还有所谓 PPT 工程,有些人没有做出成果甚至还没开始做的项目,只拿一个包装得很好的 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>因此,<strong>与其说大多数的开发工作中对技术能力的要求门槛不高,还不如说对于“本该就拥有技术能力”这样的开发群体,更多时候技术以外的其他能力更能直接影响他的工作表现</strong>。</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><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%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>Angular框架解读--Ivy编译器之心智模型</title>
<link href="https://godbasin.github.io/2021/11/06/angular-design-ivy-3-mental-model/"/>
<id>https://godbasin.github.io/2021/11/06/angular-design-ivy-3-mental-model/</id>
<published>2021-11-06T06:50:23.000Z</published>
<updated>2021-11-06T06:54:13.843Z</updated>
<content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中 Ivy 的心智模型。</p><a id="more"></a><p>上一篇<a href="https://godbasin.github.io/2021/10/31/angular-design-ivy-2-cli-compiler/">Angular 框架解读–Ivy 编译器之 CLI 编译器</a>中,我们介绍了 Angular 基于 TypeScript 的<code>tsc</code>编译能力之上,实现了 Angular 本身的一些特性能力,包括支持模板的类型检查、将文件和模块依赖关系生成到<code>d.ts</code>文件中,等等。</p><p>这部分的内容主要从 CLI 脚手架层面的编译出发,介绍在 Angular 中如何通过编译器处理代码的编译过程。本文我们主要关注 Angular 的模板编译过程,具体到装饰器的编译过程等。</p><h2 id="View-Engine"><a href="#View-Engine" class="headerlink" title="View Engine"></a>View Engine</h2><p>在 View Engine 中,编译器执行整个程序分析并生成模板和注入器定义,这些定义使用此全局知识来扁平化注入器作用域定义、将指令内联到组件中、预计算查询、预计算内容投影等。全局知识要求在编译模块时生成模块和组件工厂作为最后的全局步骤。如果任何传递信息发生变化,则需要重新生成所有工厂。</p><p>单独的组件和模块编译仅在模块定义级别和源代码中受支持。也就是说,npm 包必须包含生成工厂所需的元数据,它们本身不能包含生成的工厂。这是因为如果它们的任何依赖项发生变化,它们的工厂将无效,从而阻止它们在其依赖项中使用版本范围。</p><p>在源代码中,Angular 将 View Engine 的这种代码生成风格称为 Renderer2,当我们看到 render2 相关的代码,便是 View Engine 相关实现。相对的,当我们看到 render3 相关的代码,便是 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>标记模板。</li><li>将标记内容解析为 HTML AST。</li><li>将 HTML AST 转换为 Angular 模板 AST。</li><li>将 Angular 模板 AST 转换为模板函数。</li></ol><p>前面提到,View Engine 中编译器生成模板和注入器时依赖全局知识,因此工厂是在构建最终应用程序时生成的。</p><p>Ivy 模板编译可以从字符串生成模板函数,而无需附加信息。但是,该字符串的正确解释需要选择器范围。选择器作用域是在运行时构建的(参考<a href="https://godbasin.github.io/2021/09/19/angular-design-ivy-1-view-data-and-node-injector/">《Angular 框架解读–Ivy 编译器的视图数据和依赖解析》</a>),允许运行时使用仅从字符串构建的函数,只要给它一个在实例化期间使用的选择器作用域(例如<code>NgModule</code>)。</p><h3 id="Ivy-心智模型"><a href="#Ivy-心智模型" class="headerlink" title="Ivy 心智模型"></a>Ivy 心智模型</h3><p>在 Ivy 中,运行时的设计允许单独编译,通过在运行时执行之前由编译器预先计算的大部分内容。这允许更改组件的定义,而无需重新编译依赖于它们的模块和组件。</p><p><strong>Ivy 的心智模型是:装饰器就是编译器。</strong></p><p>也就是说,装饰器可以被认为是类转换器的参数,该类转换器通过基于装饰器参数生成定义来转换类:</p><ul><li><code>@Component</code>装饰器通过添加<code>ɵcmp</code>静态属性来转换类</li><li><code>@Directive</code>添加<code>ɵdir</code></li><li><code>@Pipe</code>添加<code>ɵpipe</code>等</li></ul><p>在大多数情况下,提供给装饰器的值足以生成定义。但是,在解释模板的情况下,编译器需要知道为模板范围内的每个组件、指令和管道定义的选择器。</p><p>前面我们说过,Ivy 编译模型是将 Angular 装饰器(<code>@Injectable</code>等)编译为类 (<code>ɵprov</code>) 上的静态属性。此操作必须在没有全局程序知识的情况下进行,并且在大多数情况下仅需要知道该单个装饰器。因此在 Ivy 中,每个将单个装饰器转换为静态字段的“编译器”都将充当“纯函数”。</p><p>对于这种情况,在 Angular 中可使用元数据来维护这些数据。给定有关特定类型和装饰器的输入元数据,它将生成一个对象,该对象描述要添加到该类型的字段,以及该字段的初始化值(采用 AST 格式)。</p><h3 id="元数据"><a href="#元数据" class="headerlink" title="元数据"></a>元数据</h3><p>前面我在<a href="https://godbasin.github.io/2021/03/27/angular-design-metadata/">《Angular 框架解读–元数据和装饰器》</a>一文中介绍了编译时从装饰器产生元数据的过程,而在<a href="https://godbasin.github.io/2021/10/31/angular-design-ivy-2-cli-compiler/">《Angular 框架解读–Ivy 编译器之 CLI 编译器》</a>一文中介绍了 Angular 通过修改了 TypeScript 编译过程,从而将 Angular 中模块和文件间的依赖关系保存在生成的<code>d.ts</code>。</p><p>Angular 会转换<code>.js</code>文件和<code>.d.ts</code>文件以反映 Angular 装饰器的内容,然后将其删除。除了在类型检查和引用反转期间,这种转换是逐个文件完成的,没有全局知识。</p><p>比如,这样一个<code>Component</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">import</span> { Component, Input } <span class="keyword">from</span> <span class="string">"@angular/core"</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span>({</span><br><span class="line"> selector: <span class="string">"greet"</span>,</span><br><span class="line"> template: <span class="string">"<div> Hello, {{name}}! </div>"</span>,</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> GreetComponent {</span><br><span class="line"> <span class="meta">@Input</span>() name: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>经<code>ngtsc</code>编译后,会包括该组件的<code>.js</code>文件:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> i0 = <span class="built_in">require</span>(<span class="string">"@angular/core"</span>);</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">GreetComponent</span> </span>{}</span><br><span class="line">GreetComponent.ɵcmp = i0.ɵɵdefineComponent({</span><br><span class="line"> type: GreetComponent,</span><br><span class="line"> tag: <span class="string">"greet"</span>,</span><br><span class="line"> factory: <span class="function"><span class="params">()</span> =></span> <span class="keyword">new</span> GreetComponent(),</span><br><span class="line"> template: <span class="function"><span class="keyword">function</span> (<span class="params">rf, ctx</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (rf & RenderFlags.Create) {</span><br><span class="line"> i0.ɵɵelementStart(<span class="number">0</span>, <span class="string">"div"</span>);</span><br><span class="line"> i0.ɵɵtext(<span class="number">1</span>);</span><br><span class="line"> i0.ɵɵelementEnd();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (rf & RenderFlags.Update) {</span><br><span class="line"> i0.ɵɵadvance(<span class="number">1</span>);</span><br><span class="line"> i0.ɵɵtextInterpolate1(<span class="string">"Hello "</span>, ctx.name, <span class="string">"!"</span>);</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>以及带有装饰器元数据信息的<code>.d.ts</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">import</span> * <span class="keyword">as</span> i0 <span class="keyword">from</span> <span class="string">"@angular/core"</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> GreetComponent {</span><br><span class="line"> <span class="keyword">static</span> ɵcmp: i0.NgComponentDef<GreetComponent, <span class="string">"greet"</span>, { input: <span class="string">"input"</span> }>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>引用反转和类型检查所需的信息包含在<code>.d.ts</code>中<code>ɵcmp</code>的类型声明中。</p><h3 id="NPM-上现有代码的兼容"><a href="#NPM-上现有代码的兼容" class="headerlink" title="NPM 上现有代码的兼容"></a>NPM 上现有代码的兼容</h3><p>如今存在于 NPM 上的 Angular 库以 Angular 包的格式分发,其中详细说明了所交付的工件,包括 ES2015 和 ESM(ES5 + ES2015 模块)风格的已编译<code>.js</code>文件、<code>.d.ts</code>文件和<code>.metadata.json</code>文件。其中,<code>.js</code>文件删除了 Angular 装饰器信息,而<code>.metadata.json</code>文件以替代格式保留装饰器元数据。</p><p>我们已经知道,在 Ivy 中,在工厂中生成的信息现在在 Angular 中作为定义生成,在 Angular 装饰类中作为静态字段生成。View Engine(Renderer2) 要求在构建最终应用程序时,还要生成所有库的所有工厂。在 Ivy 中,定义是在编译库时生成的。</p><p>Ivy 编译可以通过为它们生成工厂、并在运行时将静态属性回补到类中来适应 View Engine 目标库。比如:</p><ul><li>当应用程序包含 View Engine 目标库时,Ivy 定义需要回补到组件、指令、模块、管道和可注入类</li><li>可以在生成它的同一位置生成<code>NgModuleFactory</code>的实现,<code>NgModuleFactory</code>的这个实现将在通过调用函数创建第一个模块实例时,对 View Engine 样式类进行回补丁</li></ul><p>同样的,Ivy 编译后的产物与 View Engine 的不同之处在于声明包含在生成的输出中,并且应该包含在发布到 npm 的包中。</p><p>因此,编译后的产物仍然需要包含<code>.metadata.json</code>文件,它们会按如下所述进行转换:</p><ul><li>当编译器向类添加声明时,它也会转换<code>.metadata.json</code>文件以反映添加到类中的新静态字段</li><li>一旦将静态字段添加到元数据中,Ivy 编译器就不再需要装饰器中的信息</li></ul><h3 id="转换元数据"><a href="#转换元数据" class="headerlink" title="转换元数据"></a>转换元数据</h3><p>View Engine 中使用<code>.metadata.json</code>文件来存储直接从<code>.ts</code>文件推断的信息,并包含 TypeScript 生成的<code>.d.ts</code>文件中未包含的值信息。</p><p>Ivy 中,某个类的元数据被转换为 Ivy 编译器生成的转换后的<code>.js</code>文件的元数据。</p><p>例如,一个组件的<code>@Component</code>被编译器删除并替换为<code>ɵcmp</code>,<code>.metadata.json</code>文件也进行了类似的转换,但省略了分配值的内容(例如<code>“ɵcmp”:{}</code>)。 编译器不记录为组件声明的选择器,但需要生成<code>ngModuleScope</code>以便记录信息。构建所需的信息需要<code>ngModuleScope</code>从指令和管道传送到声明它们的模块。</p><p><code>@Component</code>组件的元数据通过以下方式转换:</p><ol><li>删除<code>@Component</code>指令。</li><li>添加<code>"ɵcmp": {}</code>静态字段。</li><li>添加<code>"ngSelector": <selector-value></code>静态字段。</li></ol><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="comment">// my.component.ts</span></span><br><span class="line"><span class="meta">@Component</span>({</span><br><span class="line"> selector: <span class="string">"my-comp"</span>,</span><br><span class="line"> template: <span class="string">`<h1>Hello, {{ name }}!</h1>`</span>,</span><br><span class="line">})</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> MyComponent {</span><br><span class="line"> <span class="meta">@Input</span>() name: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>会生成:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// my.component.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">MyComponent</span> </span>{</span><br><span class="line"> name: string;</span><br><span class="line"> <span class="keyword">static</span> ɵcmp = ɵɵdefineComponent({...});</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以及元数据信息:</p><figure class="highlight json"><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">{</span><br><span class="line"> <span class="attr">"__symbolic"</span>: <span class="string">"module"</span>,</span><br><span class="line"> <span class="attr">"version"</span>: <span class="number">4</span>,</span><br><span class="line"> <span class="attr">"metadata"</span>: {</span><br><span class="line"> <span class="attr">"MyComponent"</span>: {</span><br><span class="line"> <span class="attr">"__symbolic"</span>: <span class="string">"class"</span>,</span><br><span class="line"> <span class="attr">"statics"</span>: {</span><br><span class="line"> <span class="attr">"ɵcmp"</span>: {},</span><br><span class="line"> <span class="attr">"ngSelector"</span>: <span class="string">"my-comp"</span></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>@Directive</code>、<code>@Pipe</code>等其他装饰器也是相类似的处理,这里不多介绍。</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>Angular 的设计真的是太多太多啦,研究了好久还没研究到具体编译的地方,不过讲到目前这里,其实我们已经对 Ivy 编译器的整体情况有个大概的了解,包括基于 Ivy 编译模型下的组件、指令、管道等装饰器的编译过程和产物,以及它与 View Engine 更优的地方、兼容的处理。</p><p>而关于 Ivy 中的变更检测、AOT/JIT、Tree-shaking 等内容,会在后面继续研究分析~</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://github.com/angular/angular/blob/master/packages/compiler/design/separate_compilation.md" target="_blank" rel="noopener">DESIGN DOC (Ivy): Separate Compilation</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></ul>]]></content>
<summary type="html">
<p>作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中 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>
</feed>