-
Notifications
You must be signed in to change notification settings - Fork 299
Expand file tree
/
Copy pathatom.xml
More file actions
532 lines (292 loc) · 361 KB
/
atom.xml
File metadata and controls
532 lines (292 loc) · 361 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
<?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>2026-01-04T07:13:15.289Z</updated>
<id>https://godbasin.github.io/</id>
<author>
<name>被删</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>2026 程序员指南</title>
<link href="https://godbasin.github.io/2026/01/01/2026-programer/"/>
<id>https://godbasin.github.io/2026/01/01/2026-programer/</id>
<published>2026-01-01T07:00:27.000Z</published>
<updated>2026-01-04T07:13:15.289Z</updated>
<content type="html"><![CDATA[<p>2025 年大概是 AI 在各个场景的应用很让人吃惊的一年了。</p><span id="more"></span><h2 id="2025-年,程序员的技术壁垒在被打破"><a href="#2025-年,程序员的技术壁垒在被打破" class="headerlink" title="2025 年,程序员的技术壁垒在被打破"></a>2025 年,程序员的技术壁垒在被打破</h2><p>2025 年初,复杂类应用、垂直类应用的程序员,技术壁垒依然很高。一个工作经验不足的新人,对比一个在某个领域深耕多年的开发,缺失的经验积累可能无法在短时间内能替代其工作内容。</p><p>但这种技术壁垒,随着 AI 模型能力越来越强,正逐渐被打破。</p><p>大多数的程序员的日常开发工作,已经无法离开 AI 了。亲自写的代码比重,已经大幅度地降低。</p><p>2025 年底的今天,大多数的业务,只用给一个新人能力足够强的 AI,便能迅速胜任开发工作。</p><p>目前或许还有跨环境、跨链路、人工调试和验收的工作无法完全用 AI 闭环,但这些依赖“工作经验”的事情,可预见会被逐渐替代。</p><h2 id="2026-年,程序员的工作方式也将转变"><a href="#2026-年,程序员的工作方式也将转变" class="headerlink" title="2026 年,程序员的工作方式也将转变"></a>2026 年,程序员的工作方式也将转变</h2><p>从前通过堆屎山建立技术壁垒、依赖屎山堆工作经验来让自身不可替代的野路子,已经被打破。</p><p>从代码层面来说,AI 已经可替代所有人。以前我们常说“公司离了谁都能转”,这一事实在某种程度上还是需要付出或多或少的代价。但如今,对程序员来说,该代价已经大幅度降低。</p><p>一个好的 AI,可以让没有任何工作经验的程序员,写出和 10 年工作经验的程序员同样高质量的代码。</p><p>在这样的背景下,<strong>逻辑能力、问题拆解能力、协作能力、责任心/积极性,这些能力和品质在工作中拥有更高的占比。</strong></p><p><strong>程序员行业的专业性在降低,它逐渐跟其他工作一样,需要的会是更多逻辑清晰、积极主动、认真负责的普通人。</strong></p><p>从代码层面来看,的确如此。但程序员的成长值积累,代码本就只占领最底层部分。</p><p>在过去的工作经验中,我已经遇见过无数的程序员,即使代码写得很糟糕,但只要开始走管理,跳过了需要写代码的工作内容后,凭借管理能力、汇报能力、PUA 能力等,各种不管好的坏的能力,原地起飞。</p><h2 id="程序员的工作,写代码仅是门槛"><a href="#程序员的工作,写代码仅是门槛" class="headerlink" title="程序员的工作,写代码仅是门槛"></a>程序员的工作,写代码仅是门槛</h2><p>现在 AI 带我们跨越了该门槛,我们依然要面临剩下的命题:如何有效成长、如何高效工作、如何顺畅协作、如何有所成就,等等。</p><p><strong>工作态度为基础。</strong></p><p>老话常谈,“凡事有交代,件件有着落,事事有回音”。积极的态度、开放的姿态,主动承担工作内容和边界,遇到问题反思复盘并做出改变。</p><p><strong>工作能力为核心。</strong></p><p>解决问题的能力便是工作的全部,自己解决还是找人帮忙解决,都是解决。可以是自身的技术能力,也可以是沟通协作能力。但实际工作中,不需要与人协作就能完成的工作,少之又少。那么如何与人高效协作便是关键,换位思考不可缺。</p><p><strong>工作技巧为沉淀。</strong></p><p>10 年工作经验和 0 工作经验,除了业务熟悉度和合作伙伴关系之外,高效的工作技巧、冲突的处理技巧、有效的协作技巧,如今还有 AI 的使用技巧等,决定了我们的工作,到底会事半功倍,还是会事倍功半。</p><h2 id="2026年,依然值得分享的职场心得"><a href="#2026年,依然值得分享的职场心得" class="headerlink" title="2026年,依然值得分享的职场心得"></a>2026年,依然值得分享的职场心得</h2><p>程序员的工作,说到底和其他的工作并没有太大的区别了。</p><p>而所有工作面临的职场困境,依然是每个人要自己去探索、去磨合,最后逐渐成长的。</p><p>在我成长的过程中,收获了以下的职场心得和体会。它们至今依然会在我面临困惑和选择的时候,起到十分重要的作用。</p><p><strong>再懒不能懒思考。</strong></p><p>很多情况下,我们之所以会陷入“忙得没有喘息的时间”的困境,常常是因为懒得思考。我们要花时间去看清,看清当前付出代价所收获的,到底是不是自己想要的。</p><p><strong>请时刻保持清醒。</strong></p><p>在我们一生中,每时每刻都在做取舍,所以状态在线很重要。职场中也是,我们会遇到很多的困难和挫折,也可能会获得一时的成功和光环。胜不骄,败不馁,时刻知道自己在做什么,要做什么。</p><p>我们会在事情不如期的时候,产生困惑和矛盾。“大家都是这样的”、“这是不是我的问题”、“关键时期,以后不会了”这样的念头会不断地冒出来,能保持清醒,理清楚自己真正所想、然后坚持,才是最难的。<br>要明确自己的方向,不要随便妥协。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>最后想说的还是那些话:</p><blockquote><p>想多了全是问题,做多了全是答案。<br>想得再多都是焦虑,做得再少也算进步。</p></blockquote><p>对于明天,期待总应该比担忧更多一些。</p>]]></content>
<summary type="html">
<p>2025 年大概是 AI 在各个场景的应用很让人吃惊的一年了。</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/2025/12/21/how-to-manage-front-end-project/"/>
<id>https://godbasin.github.io/2025/12/21/how-to-manage-front-end-project/</id>
<published>2025-12-21T06:25:12.000Z</published>
<updated>2025-11-01T14:25:43.389Z</updated>
<content type="html"><![CDATA[<p>前面在<a href="https://godbasin.github.io/2023/01/12/design-and-manage-front-end-project/">《如何设计与管理一个前端项目》</a>一文中,提到过需要做风险把控、事后复盘、数据量化,但具体怎么执行呢?本文主要结合以前的实战经验进行说明。</p><span id="more"></span><p>前面提到,我们需要主动把控各个环节的情况,及时推动和解决出现的一些多方协作的问题,先从风险控制说起。</p><h1 id="1-风险控制"><a href="#1-风险控制" class="headerlink" title="1. 风险控制"></a>1. 风险控制</h1><p>举个例子,之前我接过一个需要带外包同学进行小程序开发的项目。在项目启动之前,我找到另外一个有过相同经验的开发咨询他们项目的一些情况,得到的反馈包括以下问题: </p><p>(1) 交付物质量问题: </p><ul><li>主流程无法正常体验 </li><li>代码中充斥着调试代码,交付时接口还连着测试环境 </li></ul><p>(2) 代码规范问题: </p><ul><li>格式缩进混乱 </li></ul><p>(3) 性能问题:</p><ul><li>只是用一个方法,随意引入第三方开源库 </li></ul><p>(4) 可维护性问题: </p><ul><li>多页面重复逻辑 </li><li>多页面写死appid等应该配置的数据 </li><li>接口调用及处理放在多个页面逻辑代码内 </li></ul><p>(5) 代码健壮性问题: </p><ul><li>需求逻辑边界未处理,后台数据异常直接报错 </li><li>样式兼容不足,交付时文字掉行 </li></ul><p>这些都是可预估的风险。为了避免同样的问题出现,我做了一些前期的准备工作。通过前置的这些准备工作,也达到了不错的使用效果:</p><table><thead><tr><th>解决方案</th><th>分析/其他方案对比/模块划分</th><th>使用效果</th></tr></thead><tbody><tr><td>开发协作 + 代码管理</td><td>小程序提供的TGit仓库<br>TGit与小程序关联,权限管理与开发者权限相关,可在公众平台配置<br>对比内网Git,可支持与离岸外包外网协作<br>使用Git可方便跟踪代码变更历史,同时Git在前端开发中普及度较高</td><td>开发 外包负责人 与外包协助顺畅,支持小团队并行开发<br>代码变更可追踪,方便管理<br>便于进行 code review,及时发现问题</td></tr><tr><td>基础环境</td><td>Gulp编译<br>支持依赖安装与打包<br>支持less与Babel编译<br>可进行文件变更监控、编译、打包</td><td>支持使用less编写样式,提升开发效率<br>支持使用ES6/ES7语法</td></tr><tr><td>mock环境</td><td>配置json作为mock数据<br>本地 node express 服务,简单路由映射到文件路径<br>通过切换 host 代理和 cgi 接口配置,切换 mock 数据、sandbox 和正式环境</td><td>后台提供完整接口文档和返回数据,前端通过 mock 完成功能流程开发,无需依赖后台<br>后台服务异常时,可切换 mock 环境,不阻塞前端开发<br>项目维护或交接时,可通过 mock 数据快速查看所有功能</td></tr><tr><td>代码规范</td><td>prettier<br>配置好基本规范(换行、行末分号、缩进等),配合git commit提交时自动格式化代码,抹平编译器配置差距。对比 Eslint 校验:<br>静默格式化,无需开发手动调整或格式化<br>格式化不影响 git 历史,不会导致大片代码变更、不好定位的问题</td><td>只需要 外包负责人 配置化规范,开发对代码格式化无感知<br>代码缩进、句尾分号等都保持一致</td></tr><tr><td>基础库</td><td>提供常用基础库<br>请求管理<br>日志插件<br>常用工具<br>常用组件</td><td>提供公共请求方法,统一处理登录态失效、静默续期、测速上报、数据缓存、全局接口session_key参数等,外包只需关心接口的业务逻辑<br>大大减少了搭建环境的时间,部分功能模块也得到了复用,对开发速度有很大帮助</td></tr><tr><td>文档管理</td><td>提供完善文档<br>项目文档<br>工具库 demo 与文档<br>接口文档</td><td>项目相关文档很细,降低了不少沟通成本</td></tr><tr><td>权限和联调环境</td><td>提供可使用的权限和联调环境<br>申请外包驻场开发<br>提供带权限和环境的测试机<br>联调接口部署 sandbox</td><td>驻场开发过程,外包与后台联调沟通效率提升不少<br>解决了外包无商户权限的问题<br>外包可离岸联调,同时设计、产品体验也无需配置HOST进行</td></tr></tbody></table><p>通过前期准备的这些方案和工具,提前控制好一些可预见的风险,整个开发过程比较顺利。但是如果我们的效果只有这些的话,很多时候是无法证明自己做了这么多事情的价值。那么,我们可以尝试用数据说话。</p><h1 id="用数据说话"><a href="#用数据说话" class="headerlink" title="用数据说话"></a>用数据说话</h1><p>这次带外包进行开发的项目共分成了两期,第一期开发的时候依然存在一些问题导致延期,包括: </p><p>(1) 开发完成延期3天: </p><ul><li>开发未结束上个项目(另外一个小城小程序),通过临时加入开发人员来赶上进度 </li><li>对可联调时间点评估不准确,启动联调时间晚了2天 </li><li>联调过程,后台接口依赖其他团队配合,其他团队启动时间较晚,最终联调比预估多花费1天 </li></ul><p>(2) 体验时长共2周,延期7天: </p><ul><li>设计对小程序质量要求十分高,故有不少的视觉调整,基本上每天一版细节调整(共 12 版) </li><li>产品体验时间较长,基本上一周一次产品体验的迭代(共两轮体验) </li><li>部分交互细节在需求没明确,产品体验时发现问题,其中的一些对开发逻辑调整较大 </li><li>开发预留自测时间不充分,产品体验发现问题较多 </li></ul><p>在二期开发的时候,通过一些对策来避免这些问题,例如: </p><ul><li>开发评估新的合作模式的项目预留多一些buffer </li><li>对涉及多方合作的项目,开发外包负责人需多预留些buffer </li><li>如果希望高度还原或者想做细致的交互,需要和外包商特别提一下 </li><li>体验过程中,对开发、体验问题及时反馈 </li><li>需求文档需完善交互细节,才给到外包开发 </li><li>开发需预留足够的自测时间,对正常路径、异常路径都完成充分自测 </li></ul><p>通过这些对策,二期开发的时候成功地避免了同样的问题。我们可以通过图表的方式更加清晰洗表达,如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/my-career5-6.jpg" alt="两期的开发时间结果"> </p><p>除了时间维度,我们还可以通过质量的维度来对比两期的开发情况,如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/my-career5-7.jpg" alt="两期的开发时间结果"> </p><p>通过这些数据的输出,我们可以进行项目的复盘。</p><h1 id="及时反馈与复盘"><a href="#及时反馈与复盘" class="headerlink" title="及时反馈与复盘"></a>及时反馈与复盘</h1><p>很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目,其实他们遗漏了一个很重要的环节:复盘。通过复盘这种方式,我们可以发现自身的一些问题并改进,还可以让团队其他人以及管理者知道我们做了些什么,这是很重要的。</p><p>依然回到这个项目,复盘内容除了上述的数据展示,还可以包括一些项目结束后的问题分析,例如在这次开发过程问题: </p><ul><li>外包开发效率上问题不大,项目延期主要原因: <ul><li>等待后台接口可联调时间比预期要长 </li><li>产品对接问题:提供资源、体验产品不够及时,需求规划需要更加详细 </li><li>需求变更,主要体现在设计和交互调整 </li></ul></li><li>沟通和交流上,外包态度比较积极,反馈及时。可以改进的地方: <ul><li>每日、每周工作进度及时做总结和汇总 </li><li>对产品设计和体验的合理性可具备有一定的思考,主动发现问题、提出问题、解决问题 </li><li>开发风格不一致,代码规范不一致,导致部分问题如下:过度抽象、使用自带工具库、逻辑管理较乱、变量类型混乱、重复代码过多 </li></ul></li></ul><p>问题发现了,就需要进行解决,与外包同学进行沟通反馈的内容同样可以作为复盘内容的一部分: </p><p>(1) 开发启动前,与外包进行需求评审,同时提供了基础代码,可参照上方“前期准备”所述。 </p><p>(2) 开发过程中,外包负责人发现一些代码上的问题,与外包进行沟通后,已调整和优化大部分代码。 </p><p>(3) 开发完成后,与外包收集建议反馈,以及与其他项目对比,主要整理如下: </p><ul><li>该项目过程的一些优点: <ul><li>初始化时提供的代码大大减少了搭建环境的时间,部分功能模块也得到了复用,对开发速度有很大帮助 </li><li>项目相关文档很细,降低了不少沟通成本 </li><li>项目开发过程中,外包成长很多,也知道了之前存在的一些问题 </li></ul></li><li>对该项目的一些建议: <ul><li>代码规范:外包希望外包负责人能提供一份前端代码规范</li><li>分工:外包负责业务相关页面开发,外包负责人 负责提供基础框架和工具库</li><li>小程序:小程序的特性还有一些限制应该考虑进去,比如小程序自定义modal盖不住输入框、title bar没有分割线、tab bar高度无法自定义等</li><li>视觉还原:外包工程师大部分都是编程方向的,对UI理解不深,如果希望高度还原或者想做细致的交互,需要和外包商特别提一下</li></ul></li></ul><p>这些内容,都可以通过邮件的方式发送给团队以及合作方,同时还可以作为自身的经验沉淀,后续更多项目中可以进行参考。这里举了一个特别详细的例子,可能会有些啰嗦,但大家可以仔细品读里面的一些问题的分析、处理过程和总结方式。如果使用得当,我们还可以通过这种方式来影响我们的团队和管理者,也是向上管理的一种方法。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>但其实不只是工作中,我们生活里也可以常常进行反思和总结,这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题,有些问题被我们忽视而过,有些问题我们选择了逃避,但其实我们还可以通过迎面应战、解决并反思的方式,在这样一次次战斗中快速地成长。</p>]]></content>
<summary type="html">
<p>前面在<a href="https://godbasin.github.io/2023/01/12/design-and-manage-front-end-project/">《如何设计与管理一个前端项目》</a>一文中,提到过需要做风险把控、事后复盘、数据量化,但具体怎么执行呢?本文主要结合以前的实战经验进行说明。</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/2025/11/01/front-end-performance-website-complexity-design/"/>
<id>https://godbasin.github.io/2025/11/01/front-end-performance-website-complexity-design/</id>
<published>2025-11-01T13:03:02.000Z</published>
<updated>2025-09-06T17:10:51.801Z</updated>
<content type="html"><![CDATA[<p>关于复杂度,相信所有开发都了解这个词。毕竟准备面试过程中,多少都会刷刷题,了解过算法复杂度这个词。</p><p>今天想聊的,是关于网页复杂度的一些想法。</p><h2 id="怎样算复杂?"><a href="#怎样算复杂?" class="headerlink" title="怎样算复杂?"></a>怎样算复杂?</h2><p>相信经常看我写的性能相关文章的小伙伴都知道,复杂的前端应用、大型前端应用这个词经常会出现在我的文章里。</p><p>是的,我经常说像在线文档、在线表格这样的应用是很复杂的大型应用,但是具体是复杂在哪呢?代码量大算复杂吗?模块很多算复杂吗?功能繁杂又该怎么定义呢?</p><p>在我们的工作里,也经常会提到一些网页之所以卡顿、加载慢,是因为它很复杂。但是,我们只能用“复杂”二字来概括所有的无法精细描述和量化的场景吗?</p><h2 id="关于网页复杂度的畅想"><a href="#关于网页复杂度的畅想" class="headerlink" title="关于网页复杂度的畅想"></a>关于网页复杂度的畅想</h2><p>关于复杂度,相信所有开发都了解这个词。在计算机领域,复杂度常常出现在程序算法中,主要包括:时间复杂度、空间复杂度。</p><p>我们衡量不同算法之间的优劣主要是通过时间和空间两个维度去考量:</p><ul><li>时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。</li><li>空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述</li></ul><p>如果将一篇网页的体验视作一组算法题目,那么其中需要考虑的同样包括了时间和空间两个维度。我们可以同样地用复杂度来定义其中的解法,同样包括时间和空间两个维度:</p><ul><li>时间复杂度:主要指用户等待的时间,包括打开等待时间、操作等待时间等,由程序执行耗时的影响因素组成</li><li>空间复杂度:主要指程序的内存占用,包括稳定态和最大值下的内存占用,由程序执行内存占用的影响因素组成</li></ul><h3 id="寻找复杂度影响因子"><a href="#寻找复杂度影响因子" class="headerlink" title="寻找复杂度影响因子"></a>寻找复杂度影响因子</h3><p>之前在<a href="https://godbasin.github.io/2025/10/07/front-end-performance-website-quality-score/">《前端性能优化–网页质量得分》</a>一文中,有介绍跟进一些用户体验的影响指标,来建设网页的质量得分数据。</p><p>基于该体系之上,我们可以根据大盘得分情况,捞取体验得分低的网页进行分析,获取其中的一些共同特征。通过可量化的方式,来搭建该特征与得分之间的大盘数据关系图,验证是否为网页质量得分的影响因子。</p><p>比如,在线文档的使用体验可能与文本内容数量、文档内容类型等情况有关系,在线表格则可能与单元格数量、单元格类型等情况有关系。我们可以根据实际的使用体验、用户反馈,再结合大盘数据来验证这些影响因子是否正确,再慢慢探索其中可量化的关系。</p><p>通过这样的方式,我们得到了网页质量得分(即用户访问和使用体验)的影响因子,它们同时也会是我们做复杂度定义的一些影响因素了。</p><h2 id="具体复杂度设计"><a href="#具体复杂度设计" class="headerlink" title="具体复杂度设计"></a>具体复杂度设计</h2><p>找到影响因子之后,我们可以慢慢探索影响因子与复杂度之间的关系,并且尝试使用可量化可计算的方式来对复杂度进行表达。</p><h3 id="网页时间复杂度"><a href="#网页时间复杂度" class="headerlink" title="网页时间复杂度"></a>网页时间复杂度</h3><p>我们知道,算法中时间复杂度是指执行这个算法所需要的计算工作量,其复杂度反映了程序执行时间「随输入规模增长而增长的量级」。</p><p>在网页的场景中,我们将时间复杂度定义为「随网页内容数量规模增长而增长的量级」。其中,根据浏览器渲染的原理和常见的性能优化经验,我们可猜测时间复杂度的一些影响因素:</p><ol><li>网页 DOM 元素数量:该因素可定义为常数项。在理想情况中,用户体验不应该受到网页元素数量的影响(因为大多数都会做首屏渲染和分片渲染),即仅有一屏内容和 10 屏、20 屏内容的网页体验相近,因此时间复杂度理想情况下应该为常数,但现实中显然不是这样。</li><li>网页打开性能影响因素:首屏内容数量、首屏不同类型的内容数量。</li><li>页面重排重绘次数。</li></ol><h3 id="网页空间复杂度"><a href="#网页空间复杂度" class="headerlink" title="网页空间复杂度"></a>网页空间复杂度</h3><p>空间复杂度主要指执行算法所需内存的大小,用于对程序运行过程中所需要的临时存储空间的度量。</p><p>在网页的场景中,与空间复杂度相关的主要有:</p><ul><li>JavaScript 包大小:代码解析过程中会占用内存</li><li>网页内容数量:内存占用与网页内容数量有直接的关系</li></ul><h3 id="复杂度的验证"><a href="#复杂度的验证" class="headerlink" title="复杂度的验证"></a>复杂度的验证</h3><p>假设我们认为网页 DOM 元素数量会同时影响网页的时间复杂度和空间复杂度,那么我们可以捞取大盘数据来验证当中的关系,并尝试使用函数关系来表达复杂度的定义。</p><p>在多次摸索和验证后,我们将能得到一个较完整的复杂度定义公式。我们可以通过复杂度的计算,来捞取大盘上复杂的网页内容,并且可验证复杂度和得分质量会呈负相关,即复杂度越高的网页,质量得分会越低。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>或许有些人会很困惑,我们得到了这样一个复杂度指标也好,质量得分数据也好,似乎也都没啥意义。</p><p>很多时候,我们都觉得一些指标或许不能代表什么,也经常会执着于它的准确精确与否。但除此之外,对于大盘用户的现状,往往也只能用“机器性能差”、“复杂”等字眼来描述,而可量化则是我们验证许多方案有效性的重要前提。</p>]]></content>
<summary type="html">
<p>关于复杂度,相信所有开发都了解这个词。毕竟准备面试过程中,多少都会刷刷题,了解过算法复杂度这个词。</p>
<p>今天想聊的,是关于网页复杂度的一些想法。</p>
<h2 id="怎样算复杂?"><a href="#怎样算复杂?" class="headerlink" ti
</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/2025/10/07/front-end-performance-website-quality-score/"/>
<id>https://godbasin.github.io/2025/10/07/front-end-performance-website-quality-score/</id>
<published>2025-10-07T14:47:22.000Z</published>
<updated>2025-09-06T15:06:28.477Z</updated>
<content type="html"><![CDATA[<p>提到网页得分,相信很多做过性能的前端同学,也都有用过 <a href="https://developer.chrome.com/docs/lighthouse/overview?hl=zh-cn">Lighthouse</a> 的性能评分。</p><p>而在更复杂的业务场景下,类似 Lighthouse 这样的工具只能提供打开相关的性能数据。对于重度使用的前端网页来说,或许我们还需要更详细的参考标准。</p><h2 id="Lighthouse-性能得分"><a href="#Lighthouse-性能得分" class="headerlink" title="Lighthouse 性能得分"></a>Lighthouse 性能得分</h2><p>其实我们之前在<a href="https://godbasin.github.io/2024/03/17/front-end-performance-metric/">《前端性能优化–数据指标体系》</a>一文中也有介绍常见的前端性能指标,并提到 Google 的 <a href="https://developers.google.com/speed/docs/insights/v5/about?hl=zh-cn">PageSpeed Insights (PSI)</a> 网页性能指标体系。</p><p>简单来说,PageSpeed Insights 可同时获取实验室性能数据和用户实测数据,而 Lighthouse 则可获取实验室性能数据以及网页整体优化建议(包括但不限于性能建议)。</p><p>而我们通过阅读 Lighthouse 官方文档,也能知道 Lighthouse 的评分标准。以 <a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring?hl=zh-cn#lighthouse_10">10 号灯塔</a>为例,其评分加权为:</p><table><thead><tr><th>评分点</th><th>权重</th></tr></thead><tbody><tr><td>First Contentful Paint(FCP,首次内容渲染)</td><td>10%</td></tr><tr><td>Speed Index(速度指数)</td><td>10%</td></tr><tr><td>Largest Contentful Paint(LCP,最大绘制内容)</td><td>25%</td></tr><tr><td>Total Blocking Time(TBT,总屏蔽时间)</td><td>30%</td></tr><tr><td>Cumulative Layout Shift(CLS,累计布局偏移)</td><td>25%</td></tr></tbody></table><p>Lighthouse 收集性能指标(大多数以毫秒为单位报告)后,会根据指标值在 Lighthouse 评分分布中的所处位置,将每个原始指标值转换为 0 到 100 之间的指标得分。评分分布是基于 HTTP 归档中真实网站性能数据的性能指标派生出来的对数正态分布。</p><p>相比 Lighthouse,PageSpeed Insights (PSI) 根据网页指标计划设置了阈值,并选择了所有指标的第 75 百分位,将用户体验质量分为三类:良好、需要改进或较差。</p><p>但对于复杂的应用页面,这些指标都无法足够衡量用户的整体体验。我们通常会在首屏加载完成的时机将一些打开指标数据进行上报,但这依然会有个问题:用户在后续使用过程中的性能和质量情况是否可以关注到?</p><p>因此我们可以借鉴 Lighthouse 和 PSI,自行根据业务的具体问题和使用场景,提出复杂网页的质量得分定义。</p><h2 id="复杂网页质量得分"><a href="#复杂网页质量得分" class="headerlink" title="复杂网页质量得分"></a>复杂网页质量得分</h2><p>复杂的前端应用不多见,在我的文章里,经常提到的无非是游戏和在线文档两种。和以往一样,我们同样以在线表格为例子,来定义复杂页面的质量得分。</p><p>或许我们在日常工作中,很多时候都是点对点地跟进性能问题,并没有再进一步地进行归档整理和体系化建设。但这样的模式会让我们一直处于被动,等到用户反馈问题、投诉卡顿等,其实已经比较滞后了。</p><p>而对于线上的大盘数据中,到底还有多少用户的使用体验其实并不好,但没能反馈到开发者手上呢?这样的用户占比是怎样的,又是什么原因导致他们的使用体验很差呢?</p><p>因此,我们基于以上的思考,来尝试定义网页质量得分:</p><ol><li>打开场景,主要为打开流畅度、响应速度等等。其实也是 PSI 里面的指标,包括 CLS、FID、LCP、INP、FCP、TTFB。</li><li>使用场景,主要包括卡顿情况、页面滚动流畅度、用户交互等待耗时等等。除了卡顿、FPS 等指标,剩余的则是跟业务强相关的指标定义了。</li></ol><p>我们可以这样设计网页的质量得分,那么该质量得分数据会以以下方式呈现:</p><ol><li>首次产生得分:首屏/首表加载相关性能数据获取完毕,根据各种指标(CLS、FID、LCP、INP、FCP、TTFB)计算得分,并跟随这些数据进行统一上报。</li><li>使用过程中分数变化:当用户在继续浏览和操作网页时,根据产生卡顿的次数/卡顿耗时、页面 FPS 流畅度、特定用户操作的等待耗时等等,以<strong>扣分</strong>的方式进行得分计算。当得分下降较严重时,将相关数据一并进行上报。</li></ol><h3 id="数据如何上报"><a href="#数据如何上报" class="headerlink" title="数据如何上报"></a>数据如何上报</h3><p>已知质量得分分成打开过程和持续使用过程,有两种方式进行得分的上报:</p><ol><li>统一时机上报。如果需要计算单次访问质量情况,更适合在某个统一的加载时机,将上述涉及的性能数据一起进行上报。这种方式更适合将这些数据进行聚类和二次分析,但问题在于用户在后续使用过程中,可能会突然关闭页面,导致数据未能及时上报。</li><li>多次数据分别上报。</li></ol><p>多次上报(不同上报时机)的数据,如果需要聚合在一起进行分析,则需要关联到同一篇网页内容、甚至是同一次访问会话中,这种情况下:</p><ul><li>相关指标每次上报时,都需要关联到内容 id(考虑是否存在数据隐私问题)</li><li>使用的数据平台需要支持数据二次聚合(先将多次访问数据以单篇内容为维度进行汇总,再以单篇内容作为基础颗粒度进行数据的二次计算,得到最终数据看板)</li></ul><h3 id="得分上报时机"><a href="#得分上报时机" class="headerlink" title="得分上报时机"></a>得分上报时机</h3><p>一般来说,我们希望关注用户的整体使用过程,则可以考虑两个阶段上报:</p><ol><li>在首屏数据加载完成时,根据打开指标计算得到一个得分,并进行上报。</li><li>在打开阶段得到一个初始分数后,会基于此得分进行后续的计算,在页面<code>beforeunload</code>前,将初始得分、扣除分数、运行时长、生命周期信息、最终得分等进行上报。</li></ol><p>但在实际项目中验证发现,<code>beforeunload</code>下的上报数据对比首表加载完成,转化率不到 10%,因此需要考虑使用其他上报时机。</p><p>因此,我们可以调整方向,使用其他方式来进行得分上报:</p><ol><li>达到一定扣分阈值时,进行上报。</li><li>按停留时间上报,则需要配置定时器进行定时上报(可能会出现上报量过大的场景)。</li></ol><p>不管是哪种方式,我们还需要考虑一个问题:假设每次扣分都进行上报,那么要如何将多次的上报关联到一次,并认为是同一个会话过程产生的,进行去重获取最后的得分?</p><p>答案也挺明显简单的:使用每次访问的唯一会话 id 进行去重过滤。</p><p>使用扣分机制的好处是,我们针对相同会话中的多次上报,只需要取最小得分,便可得到该过程中用户的具体扣分情况,以此来得到本次网页的访问体验得分。</p><h2 id="质量得分建设"><a href="#质量得分建设" class="headerlink" title="质量得分建设"></a>质量得分建设</h2><p>当我们尝试给一个页面定义质量得分时,很难一开始就能给到完整各种维度的指标定义和参考值。因此,为了跑通该指标体系,我们可以分阶段进行:</p><ol><li>定义初步指标评分体系,跑大盘数据观察数据。</li><li>根据用户体验新增或调整指标,逐渐贴近用户真实感受。</li><li>指标稳定后,分析待提升质量的网页特征,进行特征优化。</li></ol><h3 id="初步得分建设"><a href="#初步得分建设" class="headerlink" title="初步得分建设"></a>初步得分建设</h3><p>假设我们现在仅参考几个指标,还是以在线文档编辑的场景为例,以以下三个核心指标作为初步指标:</p><table><thead><tr><th>指标名称</th><th>Good</th><th>Needs Improvement</th><th>Poor</th><th>得分占比</th></tr></thead><tbody><tr><td>首屏可见耗时(ms)</td><td>(0, 1800]</td><td>(1800, 2500]</td><td>> 2500</td><td>30%</td></tr><tr><td>首屏可交互耗时(ms)</td><td>(0, 2950]</td><td>(2950, 4000]</td><td>> 4000</td><td>40%</td></tr><tr><td>首屏可编辑耗时(ms)</td><td>(0, 5000]</td><td>(5000, 8000]</td><td>> 8000</td><td>30%</td></tr><tr><td>得分</td><td>(100, 80]</td><td>(80, 60]</td><td>(60, 0]</td></tr></tbody></table><blockquote><p>以上只是初步最简单的定义,具体实践时可以调整或新增,比如全文档可编辑耗时、卡顿情况等等都可以统计进去。</p></blockquote><p>举个例子,假设一篇文档的加载耗时分别为:</p><table><thead><tr><th>指标名称</th><th>指标数值</th><th>计算得分</th><th>得分</th></tr></thead><tbody><tr><td>首屏可见耗时</td><td>2000 ms</td><td>= 80 - (2000 - 1800) / (2500 - 1800) * (80 - 60)</td><td>74.29</td></tr><tr><td>首屏可交互耗时</td><td>3000 ms</td><td>= 80 - (3000 - 2950) / (4000 - 2950) * (80 - 60)</td><td>79.05</td></tr><tr><td>首屏可编辑耗时</td><td>6000 ms</td><td>= 80 - (6000 - 5000) / (8000- 5000) * (80 - 60)</td><td>73.33</td></tr><tr><td>总得分</td><td></td><td>= 74.29 _ 30% + 79.05 _ 40% + 73.33 * 30%</td><td>75.91</td></tr></tbody></table><p>那么,这篇文档的得分是 75.91 分。</p><h3 id="大盘数据建设"><a href="#大盘数据建设" class="headerlink" title="大盘数据建设"></a>大盘数据建设</h3><p>现在,我们有了每篇文档的得分,那么针对每篇文档得分大盘数据就有了参考意义。我们可以针对每篇文档的得分,捞取出服务质量不好的文档并进行分析。</p><p>举个例子,我们可以建立一个大盘表格服务质量占比看板:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/website-quality-score-1.jpg" alt=""></p><p>这样就可以观察大盘上文档质量的整体情况。</p><h4 id="定制化分析文档质量"><a href="#定制化分析文档质量" class="headerlink" title="定制化分析文档质量"></a>定制化分析文档质量</h4><p>我们可以根据单篇文档服务质量的大盘情况,捞取质量得分偏低的文档进行进一步分析。我们可以捞取得分在【30,50】区间的文档数据,来分析得分偏低的文档是符合预期还是某些特性场景(比如超大文档)。</p><p>我们还可以调整数据统计方向,针对性考察某些特性的分布情况。假设现在针对在线表格的场景下,我们怀疑每篇表格服务质量与表格的单元格数量有关系,那么我们可以建立一个视图:</p><ul><li>以表格质量得分为 y 轴</li><li>以表格单元格数量为 x 轴</li></ul><p>假设我们跑出来的数据呈现如下,那么我们可以认为每篇表格服务质量与单元格数量呈明显的负相关:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/website-quality-score-2.jpg" alt=""></p><p>除了表格数量以外,我们还可以根据疑似有问题的特性(比如函数数量等)进行数据统计,来进行数据分析。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>很多时候,项目小的时候我们吐槽没啥事做,项目大的时候我们又吐槽没啥办法,但其实能做的可做的还有很多。</p><p>每一个解决的问题,它都可以成为后续很多问题的助推力,去完善一个大的解决方案。多思考,多尝试,会发现还有很多东西可以学习和研究呢~</p>]]></content>
<summary type="html">
<p>提到网页得分,相信很多做过性能的前端同学,也都有用过 <a href="https://developer.chrome.com/docs/lighthouse/overview?hl=zh-cn">Lighthouse</a> 的性能评分。</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>前端基础补齐--有关 JavaScript 单线程</title>
<link href="https://godbasin.github.io/2025/09/06/front-end-basic-js-eventloop/"/>
<id>https://godbasin.github.io/2025/09/06/front-end-basic-js-eventloop/</id>
<published>2025-09-06T13:01:34.000Z</published>
<updated>2025-09-06T13:00:55.527Z</updated>
<content type="html"><![CDATA[<p>上一篇<a href="https://godbasin.github.io/2025/08/09/front-end-basic-js-engine/">《前端基础补齐–有关 JavaScript 代码执行》</a>我给大家介绍了 JavaScript 代码的运行过程。如果说运行过程中的语法分析阶段、编译阶段和执行阶段属于微观层面的运行逻辑,那么本文将了解宏观角度下的 JavaScript 运行过程,包括 JavaScript 的单线程设计、事件循环的并发模型设计。</p><p>要怎么理解 JavaScript 是单线程这个概念呢?大概需要从浏览器来说起。</p><p>JavaScript 最初被设计为浏览器脚本语言,主要用途包括对页面的操作、与浏览器的交互、与用户的交互、页面逻辑处理等。如果将 JavaScript 设计为多线程,则当有多个线程同时对同一个 DOM 节点进行操作,这时候线程间的同步问题会变得很复杂。</p><p>因此,为了避免复杂性,JavaScript 被设计为单线程。</p><p>这样一个单线程的 JavaScript,意味着任务需要一个接一个地处理。如果有一个任务是等待用户输入,那在用户进行操作前,所有其他任务都处于等待状态,页面会进入假死状态,用户体验会很糟糕。</p><p>那么,要怎样才能高效地进行页面的交互和渲染处理呢?在 JavaScript 中,通过将执行任务拆分为同步任务与异步任务,来解决单线程中任务阻塞的问题。</p><h2 id="同步任务与异步任务"><a href="#同步任务与异步任务" class="headerlink" title="同步任务与异步任务"></a>同步任务与异步任务</h2><p>我们围绕着任务执行是否阻塞 JavaScript 主线程,将 JavaScript 中的任务分为同步任务和异步任务:</p><ul><li>同步任务:在主线程上排队执行的任务,前一个任务完整地执行完成后,后一个任务才会被执行</li><li>异步任务:不会阻塞主线程,在其任务执行完成之后,会再根据一定的规则去执行相关的回调</li></ul><p>我们来分别看一下。</p><h3 id="同步任务与函数调用栈"><a href="#同步任务与函数调用栈" class="headerlink" title="同步任务与函数调用栈"></a>同步任务与函数调用栈</h3><p>在 JavaScript 中,同步任务基本上可以认为是执行 JavaScript 代码。</p><p>上一讲内容中,我们提到 JavaScript 在执行过程中每进入一个不同的运行环境时,都会创建一个相应的执行上下文。那么,当我们执行一段 JavaScript 代码时,通常会创建多个执行上下文。</p><p>JavaScript 解释器会以栈的方式管理这些执行上下文、以及函数之间的调用关系,形成函数调用栈(call stack)。调用栈可理解为一个存储函数调用的栈结构,遵循 FILO(先进后出)的原则。</p><p>我们来看一下 JavaScript 中代码执行的过程:</p><ol><li>首先进入全局环境,全局执行上下文被创建并添加进栈中。</li><li>每调用一个函数,该函数执行上下文会被添加进调用栈,并开始执行。</li><li>如果正在调用栈中执行的 A 函数还调用了 B 函数,那么 B 函数也将会被添加进调用栈。</li><li>一旦 B 函数被调用,便会立即执行。</li><li>当前函数执行完毕后,JavaScript 解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。</li></ol><p>由此可见,JavaScript 代码执行过程中,函数调用栈栈底永远是全局执行上下文,栈顶则永远是当前执行上下文。</p><p>在不考虑全局执行上下文时,我们可以理解为刚开始的时候调用栈是空的,每当有函数被调用,相应的执行上下文都会被添加到调用栈中。执行完函数中相关代码后,该执行上下文又会自动被调用栈移除,最后调用栈又回到了空的状态(同样不考虑全局执行上下文)。</p><p>由于栈的容量是有限制的,因此当我们没有合理调用函数的时候,可能会导致爆栈异常,此时控制台便会抛出错误:</p><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAA1BMVEXk8PCgkFYHAAAAH0lEQVRo3u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAvg0hAAABfxmcpwAAAABJRU5ErkJggg==" alt="图片"></p><p>这样的一个函数调用栈结构,可以理解为 JavaScript 中同步任务的执行环境,同步任务也可以理解为 JavaScript 代码片段的执行。</p><p>同步任务的执行会阻塞主线程,也就是说,一个函数执行的时候不会被抢占,只有在它执行完毕之后,才会去执行任何其他的代码。这意味着如果我们一个任务执行的时机过长,浏览器就无法处理与用户的交互,例如点击或滚动。</p><p>因此,我们还需要用到异步任务。</p><h3 id="异步任务与回调队列"><a href="#异步任务与回调队列" class="headerlink" title="异步任务与回调队列"></a>异步任务与回调队列</h3><p>异步任务包括一些需要等待响应的任务,包括用户交互、HTTP 请求、定时器等。</p><p>我们知道,I/O 类型的任务会有较长的等待时间,对于这类无法立刻得到结果的事件,可以使用异步任务的方式。这个过程中 JavaScript 线程就不用处于等待状态,CPU 也可以处理其他任务。</p><p>异步任务需要提供回调函数,当异步任务有了运行结果之后,该任务则会被添加到回调队列中,主线程在适当的时候会从回调队列中取出相应的回调函数并执行。</p><p>这里提到的回调队列又是什么呢?</p><p>实际上,JavaScript 在运行的时候,除了函数调用栈之外,还包含了一个待处理的回调队列。在回调队列中的都是已经有了运行结果的异步任务,每一个异步任务都会关联着一个回调函数。</p><p>回调队列则遵循 FIFO(先进先出)的原则,JavaScript 执行代码过程中,会进行以下的处理:</p><ul><li>运行时,会从最先进入队列的任务开始处理队列中的任务</li><li>被处理的任务会被移出队列,该任务的运行结果会作为输入参数,并调用与之关联的函数,此时会产生一个函数调用栈</li><li>函数会一直处理到调用栈再次为空,然后 Event Loop 将会处理队列中的下一个任务</li></ul><p>这里我们提到了 Event Loop,它主要是用来管理单线程的 JavaScript 中同步任务和异步任务的执行问题。</p><h2 id="单线程的-JavaScript-是如何管理任务的"><a href="#单线程的-JavaScript-是如何管理任务的" class="headerlink" title="单线程的 JavaScript 是如何管理任务的"></a>单线程的 JavaScript 是如何管理任务的</h2><p>我们知道,单线程的设计会存在阻塞问题,为此 JavaScript 中任务被分为同步和异步任务。那么,同步任务和异步任务之间是按照什么顺序来执行的呢?</p><p>JavaScript 有一个基于事件循环的并发模型,称为事件循环(Event Loop),它的设计解决了同步任务和异步任务的管理问题。</p><p>根据 JavaScript 运行环境的不同,Event Loop 也会被分成浏览器的 Event Loop 和 Node.js 中的 Event Loop。</p><h3 id="浏览器的-Event-Loop"><a href="#浏览器的-Event-Loop" class="headerlink" title="浏览器的 Event Loop"></a>浏览器的 Event Loop</h3><p>在浏览器里,每当一个被监听的事件发生时,事件监听器绑定的相关任务就会被添加进回调队列。通过事件产生的任务是异步任务,常见的事件任务包括:</p><ul><li>用户交互事件产生的事件任务,比如 DOM 操作</li><li>计时器产生的事件任务,比如<code>setTimeout</code></li><li>异步请求产生的事件任务,比如 HTTP 请求</li></ul><p>JavaScript 的运行过程,可以借用 Philip Roberts 演讲《Help, I’m stuck in an event-loop》中经典的一张图来描述:</p><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAA1BMVEXk8PCgkFYHAAAAH0lEQVRo3u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAvg0hAAABfxmcpwAAAABJRU5ErkJggg==" alt="图片"></p><p>如图,主线程运行的时候,会产生堆(heap)和栈(stack),其中堆为内存、栈为函数调用栈。我们能看到,Event Loop 负责执行代码、收集和处理事件以及执行队列中的子任务,具体包括:</p><ol><li>JavaScript 有一个主线程和调用栈,所有的任务最终都会被放到调用栈等待主线程执行。</li><li>同步任务会被放在调用栈中,按照顺序等待主线程依次执行。</li><li>主线程之外存在一个回调队列,回调队列中的异步任务最终会在主线程中以调用栈的方式运行。</li><li>同步任务都在主线程上执行,栈中代码在执行的时候会调用浏览器的 API,此时会产生一些异步任务。</li><li>异步任务会在有了结果(比如被监听的事件发生时)后,将异步任务以及关联的回调函数放入回调队列中。</li><li>调用栈中任务执行完毕后,此时主线程处于空闲状态,会从回调队列中获取任务进行处理。</li></ol><p>上述过程会不断重复,这就是 JavaScript 的运行机制,称为事件循环机制(Event Loop)。</p><p>Event Loop 的设计会带来一些问题,比如<code>setTimeout</code>、<code>setInterval</code>的时间精确性。这两个方法会设置一个计时器,当计时器计时完成,需要执行回调函数,此时才把回调函数放入回调队列中。</p><p>如果当回调函数放入队列时,假设队列中还有大量的回调函数在等待执行,此时就会造成任务执行时间不精确。</p><p>要优化这个问题,可以使用系统时钟来补偿计时器不准确性,从而提升精确度。举个例子,如果你的计时器会在回调时触发二次计时的,可以在每次回调任务结束的时候,根据最初的系统时间和该任务的执行时间进行差值比较,来修正后续的计时器时间。</p><h3 id="Node-js-中的-Event-Loop"><a href="#Node-js-中的-Event-Loop" class="headerlink" title="Node.js 中的 Event Loop"></a>Node.js 中的 Event Loop</h3><p>除了浏览器,Node.js 中同样存在 Event Loop。由于 JavaScript 是单线程的,Event Loop 的设计使 Node.js 可以通过将操作转移到系统内核中,来执行非阻塞 I/O 操作。</p><p>Node.js 中的事件循环执行过程为:</p><ol><li>当 Node.js 启动时将初始化事件循环,处理提供的输入脚本。</li><li>提供的输入脚本可以进行异步 API 调用,然后开始处理事件循环。</li><li>在事件循环的每次运行之间,Node.js 会检查它是否正在等待任何异步 I/O 或计时器,如果没有,则将其干净地关闭。</li></ol><p>与浏览器不一样,Node.js 中事件循环分成不同的阶段:</p><figure class="highlight plaintext"><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><br><span class="line">┌─>│ timers │</span><br><span class="line">│ └─────────────┬─────────────┘</span><br><span class="line">│ ┌─────────────┴─────────────┐</span><br><span class="line">│ │ pending callbacks │</span><br><span class="line">│ └─────────────┬─────────────┘</span><br><span class="line">│ ┌─────────────┴─────────────┐</span><br><span class="line">│ │ idle, prepare │</span><br><span class="line">│ └─────────────┬─────────────┘ ┌───────────────┐</span><br><span class="line">│ ┌─────────────┴─────────────┐ │ incoming: │</span><br><span class="line">│ │ poll │<─────┤ |</span><br><span class="line">│ └─────────────┬─────────────┘ │ data, etc. │</span><br><span class="line">│ ┌─────────────┴─────────────┐ └───────────────┘</span><br><span class="line">│ │ check │</span><br><span class="line">│ └─────────────┬─────────────┘</span><br><span class="line">│ ┌─────────────┴─────────────┐</span><br><span class="line">└──┤ close callbacks │</span><br><span class="line"> └───────────────────────────┘</span><br><span class="line"></span><br></pre></td></tr></table></figure><table><thead><tr><th style="text-align:left">Event Loop 阶段</th><th style="text-align:left">描述</th></tr></thead><tbody><tr><td style="text-align:left">timers</td><td style="text-align:left">此阶段由 setTimeout()和安排的回调 setInterval()执行</td></tr><tr><td style="text-align:left">pending callbacks</td><td style="text-align:left">执行推迟到下一个循环迭代的 I/O 回调</td></tr><tr><td style="text-align:left">idle/prepare</td><td style="text-align:left">仅在 Node.js 内部使用</td></tr><tr><td style="text-align:left">poll</td><td style="text-align:left">检索新的 I/O 事件,执行与 I/O 相关的回调,节点将在此处阻塞</td></tr><tr><td style="text-align:left">check</td><td style="text-align:left">setImmediate()在这里调用回调</td></tr><tr><td style="text-align:left">close callbacks</td><td style="text-align:left">一些关闭回调,例如 socket.on(‘close’, …)</td></tr></tbody></table><p>由于事件循环阶段划分不一致,Node.js 和浏览器在对宏任务和微任务的处理上也不一样。</p><h3 id="宏任务和微任务"><a href="#宏任务和微任务" class="headerlink" title="宏任务和微任务"></a>宏任务和微任务</h3><p>事件循环中的异步回调队列有两种:宏任务(MacroTask)和微任务(MicroTask)队列。</p><p>什么是宏任务和微任务呢?</p><ul><li>宏任务:包括 script 全部代码、<code>setTimeout</code>、<code>setInterval</code>、<code>setImmediate</code>(Node.js)、<code>requestAnimationFrame</code>(浏览器)、I/O 操作、UI 渲染(浏览器),这些代码执行便是宏任务</li><li>微任务:包括<code>process.nextTick</code>(Node.js)、<code>Promise</code>、<code>MutationObserver</code>,这些代码执行便是微任务</li></ul><p>为什么要将异步任务分为宏任务和微任务呢?这是为了避免回调队列中等待执行的异步任务(宏任务)过多,导致某些异步任务(微任务)的等待时间过长。在每个宏任务执行完成之后,会先将微任务队列中的任务执行完毕,再执行下一个宏任务。</p><p>因此,前面我们所说的回调队列可以理解为宏任务队列,同时还有另外一个任务队列为微任务队列。</p><p>在浏览器的异步回调队列中,宏任务和微任务的执行过程如下:</p><ol><li>宏任务队列一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务。</li><li>微任务队列中所有的任务都会被依次取出来执行,直到微任务队列为空。</li><li>在执行完所有的微任务之后,执行下一个宏任务之前,浏览器会执行 UI 渲染操作、更新界面。</li></ol><p>我们能看到,在浏览器中每个宏任务执行完成后,会执行微任务队列中的任务。而在 Node.js 中,事件循环分为 6 个阶段,微任务会在事件循环的各个阶段之间执行。也就是说,每当一个阶段执行完毕,就会去执行微任务队列的任务。</p><p>宏任务和微任务的执行顺序,常常会被用作面试题,比如下面一道题目:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"script start"</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"setTimeout"</span>);</span><br><span class="line">}, <span class="number">1000</span>);</span><br><span class="line"></span><br><span class="line"><span class="title class_">Promise</span>.<span class="title function_">resolve</span>()</span><br><span class="line"> .<span class="title function_">then</span>(<span class="keyword">function</span> (<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"promise1"</span>);</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">then</span>(<span class="keyword">function</span> (<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"promise2"</span>);</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">errorFunc</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">reject</span>(<span class="string">"error!!!"</span>);</span><br><span class="line"> } <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"error caught"</span>); <span class="comment">// 微1-3</span></span><br><span class="line"> }</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"errorFunc"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Promise</span>.<span class="title function_">resolve</span>(<span class="string">"errorFunc success"</span>);</span><br><span class="line">}</span><br><span class="line"><span class="title function_">errorFunc</span>().<span class="title function_">then</span>(<span class="function">(<span class="params">res</span>) =></span> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"errorFunc then res"</span>));</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"script end"</span>);</span><br></pre></td></tr></table></figure><p>可以去浏览器控制台贴一下代码看看结果。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>本文介绍了 JavaScript 的单线程设计,它的设计初衷是为了让用户获得更好的交互体验。同时,为了避免单线程的任务执行过程中发生阻塞,事件循环(Event Loop)机制便出现了。</p><p>在浏览器和 Node.js 中,都存在单线程的 Event Loop 设计,它们之间的不一致主要表现为 Event Loop 阶段划分、以及宏任务和微任务的处理时机会有些区别。</p><p>或许你会感到疑惑,除了应对面试以外,掌握 JavaScript 的事件循环、宏任务和微任务相关机制,对我们有什么用处呢?</p><p>要知道,浏览器中在执行 JavaScript 代码的时候不会进行页面渲染,如果一项任务花费的时间太长,浏览器将无法执行其他任务(例如处理用户事件)。因此,当存在大量复杂的计算、或导致了死循环的编程错误时,甚至会使页面终止。</p><p>我们可以更合理地利用这些机制来拆分任务,比如考虑将多次触发的数据变更通过微任务收集起来,再一起进行 UI 的更新和渲染,便可以降低浏览器渲染的频率,提升浏览器的性能,给到用户更好的体验。</p>]]></content>
<summary type="html">
<p>上一篇<a href="https://godbasin.github.io/2025/08/09/front-end-basic-js-engine/">《前端基础补齐–有关 JavaScript 代码执行》</a>我给大家介绍了 JavaScript 代码的运行过程。如
</summary>
<category term="js什锦" scheme="https://godbasin.github.io/categories/js%E4%BB%80%E9%94%A6/"/>
<category term="分享" scheme="https://godbasin.github.io/tags/%E5%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>前端基础补齐--有关 JavaScript 代码执行</title>
<link href="https://godbasin.github.io/2025/08/09/front-end-basic-js-engine/"/>
<id>https://godbasin.github.io/2025/08/09/front-end-basic-js-engine/</id>
<published>2025-08-09T02:25:21.000Z</published>
<updated>2025-07-10T23:56:01.677Z</updated>
<content type="html"><![CDATA[<p>上一篇<a href="https://godbasin.github.io/2025/07/06/front-end-basic-js-extend/">《前端基础补齐–有关 JavaScript 继承》</a>我们介绍了 JavaScript 的原型和继承,JavaScript 作为基于对象的编程语言,原型和继承是它的一大特点,理解 JavaScript 的继承方式有助于我们对代码进行更好的设计和优化。</p><p>除此之外,JavaScript 在执行过程中也与其他语言有所不一样,如果不理解 JavaScript 的词法环境、执行上下文等内容,很容易便会在开发过程中埋下“莫名奇妙”的 bug,比如<code>this</code>指向和预期不一致、某个变量不知道为什么被改了等等。</p><p>我们在谈论 JavaScript 的时候,常常会提到执行环境、词法环境、作用域、执行上下文、闭包等内容,要搞清楚它们之间的区别,首先我们需要理解 JavaScript 引擎在执行代码的过程。</p><h1 id="代码运行的各个阶段"><a href="#代码运行的各个阶段" class="headerlink" title="代码运行的各个阶段"></a>代码运行的各个阶段</h1><p>相信大家都知道,JavaScript 是弱类型语言,运行时才能确定变量类型,即使是如今流行的 Typescript,也只是增加了编译时(编译成 JavaScript)的类型检测。</p><p>对于编译器相信大家都有所了解,代码编译过程中会包括语法解析、语义分析、生成 AST 等过程。同样的,JavaScript 引擎在执行 JavaScript 代码的时候,会从上到下进行分词(Tokenizing)、词法分析(Lexing),并在代码解析完成后生成 AST(抽象语法树),最终根据 AST 生成 CPU 可以执行的机器码并执行。</p><p>以 V8 引擎为例,我们能看到 JavaScript 运行过程主要分成三个阶段:</p><p>语法分析阶段:<br>(1) 该阶段会对代码进行语法分析,检查是否有语法错误(SyntaxError)。如果发现语法错误,会在控制台抛出异常,并终止执行。<br>编译阶段。<br>(2) 该阶段会进行执行上下文(Execution Context)的创建,包括创建变量对象、建立作用域链、确定 this 的指向等。每进入一个不同的运行环境时,V8 引擎都会创建一个新的执行上下文。</p><p>执行阶段:<br>(3) 将步骤 (2) 中创建的执行上下文压入执行栈,并成为正在运行的执行上下文。<br>(4) 执行代码。<br>(5) 在代码执行结束后,将其弹出执行栈。</p><p>在了解完 JavaScript 执行的各个阶段之后,下面我们来进一步了解整个代码执行的过程,包括从中产生的执行上下文、词法环境、变量环境等内容。</p><blockquote><p>其中,关于执行栈的内容我们会在下一讲详细讲解,目前我们只需要知道 JavaScript 在运行过程中会产生一个调用栈,调用栈遵循 LIFO(先进后出,后进先出)原则。<br>语法分析阶段比较简单,本文我们会重点介绍编译阶段,其核心便是执行上下文的创建。</p></blockquote><h1 id="执行上下文的创建"><a href="#执行上下文的创建" class="headerlink" title="执行上下文的创建"></a>执行上下文的创建</h1><p>执行上下文的创建离不开 JavaScript 的运行环境。我们知道,JavaScript 运行环境包括全局环境、函数环境和<code>eval</code>,其中:</p><ol><li>第一次载入 JavaScript 代码时,首先会创建一个全局环境。全局环境位于最外层,直到应用程序退出后(例如关闭浏览器和网页)才会被销毁。</li><li>每个函数都有自己的运行环境。当函数被调用时,则会进入该函数的运行环境,当该环境中的代码被全部执行完毕后,该环节会被销毁。不同的函数运行环境不一样,即使是同一个函数,在被多次调用时也会创建多个不同的函数环境。</li></ol><p>要知道,在不同的运行环境中,变量和函数可访问的其他数据范围不同,各种的行为也有所区别。</p><p>每进入一个不同的运行环境时,JavaScript 都会创建一个新的执行上下文,该过程包括:建立作用域链(Scope Chain)、创建变量对象(Variable Object,简称 VO)以及确定 this 的指向。</p><p>由于建立作用域链过程中会涉及变量对象的概念,因此我们先来看看变量对象的创建。</p><h2 id="创建变量对象"><a href="#创建变量对象" class="headerlink" title="创建变量对象"></a>创建变量对象</h2><p>什么是变量对象呢?每个执行上下文都会有一个关联的变量对象,该对象上会保存在这个上下文中定义的所有变量和函数。</p><p>在浏览器中,全局环境的变量对象是<code>window</code>对象,因此所有的全局变量和函数都是作为<code>window</code>对象的属性和方法创建的。相对应的,在 Node 中全局环境的变量对象则是<code>global</code>对象。</p><p>创建变量对象过程将会创建<code>arguments</code>对象(仅函数环境下),同时会检查当前上下文的函数声明和变量声明:</p><ul><li>对于变量声明:此时会给变量分配内存,并将其初始化为<code>undefined</code>(该过程只进行定义声明,执行阶段才执行赋值语句)</li><li>对于函数声明:此时会在内存里创建函数对象,并且直接初始化为该函数对象</li></ul><p>上述过程便是我们常说的变量提升和函数提升,其中函数声明提升会优先于变量声明提升。变量提升容易带来变量在预期外被覆盖掉的问题,同时还可能导致本应该被销毁的变量没有被销毁等情况,因此 ES6 中引入了<code>let</code>和<code>const</code>关键字,从而使 JavaScript 也拥有了块级作用域。</p><p>或许你会感到疑惑,JavaScript 是怎么支持块级作用域的呢?这就涉及到了作用域的概念。</p><h3 id="什么是作用域"><a href="#什么是作用域" class="headerlink" title="什么是作用域"></a>什么是作用域</h3><p>在各类编程语言中,作用域分为静态作用域和动态作用域。JavaScript 采用的是词法作用域(Lexical Scoping),也就是静态作用域。词法作用域中的变量,在编译过程中会产生一个确定的作用域。</p><p>这里面涉及到很多概念,包括词法作用域、执行上下文、词法环境等等,它们之间是什么关系呢?</p><p>要知道,我们常说的作用域即当前的执行上下文,在 ES5 后我们使用词法环境(Lexical Environment)替代作用域来描述该执行上下文。因此,词法环境可理解为我们常说的作用域,同样也指当前的执行上下文(注意,是当前的执行上下文)。</p><p>在 JavaScript 中,词法环境分为词法环境(Lexical Environment)和变量环境(Variable Environment),其中:</p><ul><li>变量环境用来记录<code>var</code>/<code>function</code>等变量声明</li><li>词法环境是用来记录<code>let</code>/<code>const</code>/<code>class</code>等变量声明</li></ul><p>回到前面的问题,JavaScript 是怎么支持块级作用域的呢?便是通过这两个词法环境。使用两个词法环境(而不是一个)是为了实现块级作用域的同时,不影响<code>var</code>变量声明和函数声明。</p><p>我们继续来看执行上下文的创建过程,变量对象的创建过程会进行函数提升和变量提升,下面我们来看看作用域链的建立。</p><h2 id="建立作用域链"><a href="#建立作用域链" class="headerlink" title="建立作用域链"></a>建立作用域链</h2><p>什么是作用域链呢?顾名思义,作用域链就是将各个作用域通过某种方式连接在一起,具体是怎样的方式呢?下面我会把作用域的建立过程进行详细的介绍。</p><p>前面说过,作用域也就是词法环境,词法环境由两个成员组成:</p><ul><li>环境记录(Environment Record):用于记录自身词法环境中的变量对象</li><li>外部词法环境引用(Outer Lexical Environment):记录外层词法环境的引用</li></ul><p>其中,当某个变量无法在自身词法环境记录时,可以根据外部词法环境引用向外层寻找,这便是作用域链的变量查询。在最外层的全局词法环境中,外部词法环境引用为<code>null</code>。</p><p>那么,作用域链是通过怎样的方式建立起来的呢?这个外部词法环境引用又是怎样指向外层呢?</p><h3 id="通过外部词法环境引用来创建作用域"><a href="#通过外部词法环境引用来创建作用域" class="headerlink" title="通过外部词法环境引用来创建作用域"></a>通过外部词法环境引用来创建作用域</h3><p>为了方便描述,我们将 JavaScript 代码执行过程分为定义期和执行期,前面提到的编译阶段则属于定义期。</p><p>来看一个例子,我们定义了全局函数<code>foo</code>,并在该函数中定义了函数<code>bar</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">foo</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">dir</span>(bar);</span><br><span class="line"> <span class="keyword">var</span> a = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">function</span> <span class="title function_">bar</span>(<span class="params"></span>) {</span><br><span class="line"> a = <span class="number">2</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">dir</span>(foo);</span><br><span class="line"><span class="title function_">foo</span>();</span><br></pre></td></tr></table></figure><p>前面我们说到,JavaScript 使用的是静态作用域,因此函数的作用域在定义期已经决定了。在上面的例子中,全局函数<code>foo</code>创建了一个<code>foo</code>的<code>[[scope]]</code>属性,包含了全局<code>[[scope]]</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">foo[[scope]] = [globalContext];</span><br></pre></td></tr></table></figure><p>而当我们执行<code>foo()</code>时,也会分别进入<code>foo</code>函数的定义期和执行期。同样,在<code>foo</code>函数的定义期时,函数<code>bar</code>的<code>[[scope]]</code>将会包含全局<code>[[scope]]</code>和<code>foo</code>的<code>[[scope]]</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bar[[scope]] = [fooContext, globalContext];</span><br></pre></td></tr></table></figure><p>运行上述代码,我们可以在控制台看到符合预期的输出:<br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAA1BMVEXk8PCgkFYHAAAAH0lEQVRo3u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAvg0hAAABfxmcpwAAAABJRU5ErkJggg==" alt="图片"><br>也就是说,<strong>当代码进入执行阶段后,会通过外部词法环境引用来创建变量对象的一个作用域链,从而保证对执行环境有权访问的变量和函数的有序访问</strong>。</p><p>在这个过程中,前面我们在编译阶段创建的变量对象(VO)中变量属性会进行赋值,变量对象会转为活动对象(Active Object,简称 AO),此时活动对象才可被访问,这是 VO -> AO 的过程,我们来详细进行分析。</p><h3 id="VO-gt-AO-过程"><a href="#VO-gt-AO-过程" class="headerlink" title="VO -> AO 过程"></a>VO -> AO 过程</h3><p>为了更好地理解这个过程,我们来看个例子:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">foo</span>(<span class="params">a</span>) {</span><br><span class="line"> <span class="keyword">var</span> b = <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">function</span> <span class="title function_">c</span>(<span class="params"></span>) {}</span><br><span class="line"> <span class="keyword">var</span> d = <span class="keyword">function</span>(<span class="params"></span>) {};</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="title function_">foo</span>(<span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>在执行<code>foo(1)</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></pre></td><td class="code"><pre><span class="line"><span class="variable constant_">AO</span> = {</span><br><span class="line"> <span class="attr">arguments</span>: {</span><br><span class="line"> <span class="number">0</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">length</span>: <span class="number">1</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">a</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">b</span>: <span class="literal">undefined</span>,</span><br><span class="line"> <span class="attr">c</span>: reference to <span class="keyword">function</span> <span class="title function_">c</span>(<span class="params"></span>){},</span><br><span class="line"> <span class="attr">d</span>: <span class="literal">undefined</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><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="variable constant_">AO</span> = {</span><br><span class="line"> <span class="attr">arguments</span>: {</span><br><span class="line"> <span class="number">0</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">length</span>: <span class="number">1</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">a</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">b</span>: <span class="number">2</span>,</span><br><span class="line"> <span class="attr">c</span>: reference to <span class="keyword">function</span> <span class="title function_">c</span>(<span class="params"></span>){},</span><br><span class="line"> <span class="attr">d</span>: reference to <span class="title class_">FunctionExpression</span> <span class="string">"d"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其实,变量对象和活动对象本质上为同一个对象:</p><ul><li>在定义期(编译阶段):该对象值仍为<code>undefined</code>,且处于不可访问的状态</li><li>进入执行期(执行阶段):VO 被激活,其中变量属性会进行赋值</li></ul><p>实际上在执行的时候,除了 VO 被激活,活动对象还会添加函数执行时传入的参数和<code>arguments</code>这个特殊对象,因此 AO 和 VO 的关系可以理解为:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable constant_">AO</span> = <span class="variable constant_">VO</span> + <span class="keyword">function</span> parameters + <span class="variable language_">arguments</span></span><br></pre></td></tr></table></figure><p>现在,我们知道作用域链是在进入代码的执行阶段时,通过外部词法环境引用来创建的。但前面我们又说过,在创建执行上下文的时候会创建作用域链,而创建执行上下文又是在编译阶段,这是否有些矛盾呢?<br>其实并不会,比如上面例子中<code>bar</code>函数的编译阶段,其实是在<code>foo</code>函数的执行阶段中。JavaScript 代码虽然可以将其分为语法分析阶段、编译阶段和执行阶段,但实际上在 JavaScript 引擎中是通过调用栈的方式来运行的,因此并不存在“整个 JavaScript 运行过程只会在某个阶段中”这一说法。</p><p>相信大家现在已经掌握了作用域链的建立过程,那么作用域链的用途想必大家也已经了解,比如在函数执行过程中变量的解析:</p><ul><li>从当前词法环境开始,沿着作用域链逐级向外层寻找环境记录,直到找到同名变量为止</li><li>找到后不再继续遍历,找不到就报错</li></ul><p>一般来说,当函数执行结束之后,执行期上下文将被销毁(作用域链和激活对象均被销毁)。但在某些场景下,我们还需要使用到其中的一些变量对象,此时会使用到闭包。</p><h3 id="闭包的产生"><a href="#闭包的产生" class="headerlink" title="闭包的产生"></a>闭包的产生</h3><p>我们已经知道,通过作用域链,我们可以在函数内部可以直接读取外部以及全局变量。但在 JavaScript 中,只有函数内部的子函数才能读取局部变量。我们看下面的例子:</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="keyword">function</span> <span class="title function_">foo</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">var</span> a = <span class="number">1</span>;</span><br><span class="line">}</span><br><span class="line"><span class="title function_">foo</span>();</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a); <span class="comment">// undefined</span></span><br></pre></td></tr></table></figure><p>在全局环境下无法访问函数<code>foo</code>内的变量,这是因为全局函数的作用域链里,不含有函数<code>foo</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">foo</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">var</span> a = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">function</span> <span class="title function_">bar</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">return</span> a;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> bar;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> b = <span class="title function_">foo</span>();</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">b</span>()); <span class="comment">// 1</span></span><br></pre></td></tr></table></figure><p>前面我们说到,当函数执行结束之后,执行期上下文将被销毁,其中包括作用域链和激活对象。那么,在这个例子中,当<code>b()</code>执行时,<code>foo</code>函数上下文包括作用域都已经被销毁了,为什么<code>foo</code>作用域下的<code>a</code>依然可以被访问到呢?</p><p>这是因为<code>bar</code>函数引用了<code>foo</code>函数变量对象中的值,此时即使创建<code>bar</code>函数的<code>foo</code>函数执行上下文被销毁了,但它的变量对象依然会保留在 JavaScript 内存中,<code>bar</code>函数依然可以通过<code>bar</code>函数的作用域链找到它,并进行访问。这便是我们常说的闭包,即使创建它的上下文已经销毁,它仍然被保留在内存中。</p><p>闭包则使得我们可以从外部读取局部变量,在大多数项目中都会被使用到,常见的用途包括:</p><ul><li>用于从外部读取其他函数内部变量的函数</li><li>可以使用闭包来模拟私有方法</li><li>让这些变量的值始终保持在内存中</li></ul><p>需要注意的是,我们在使用闭包的时候,需要及时清理不再使用到的变量,否则可能导致内存泄露问题。</p><p>下面我们继续来看,执行上下文的创建过程中还会做的一件事:确定<code>this</code>的指向。</p><h2 id="确定-this-的指向"><a href="#确定-this-的指向" class="headerlink" title="确定 this 的指向"></a>确定 this 的指向</h2><p>在 JavaScript 中,<code>this</code>指向执行当前代码的对象的所有者,可简单理解为<code>this</code>指向最后调用当前代码的那个对象。</p><p>相信大家都很熟悉<code>this</code>,因此这里我就进行结论性的简单总结。根据 JavaScript 中函数的调用方式不同,<code>this</code>的指向分为以下情况:</p><ul><li>在全局环境中,<code>this</code>指向全局对象(在浏览器中为 <code>window</code>)</li><li>在函数内部,<code>this</code>的值取决于函数被调用的方式<ul><li>函数作为对象的方法被调用,<code>this</code>指向调用这个方法的对象</li><li>函数用作构造函数时(使用<code>new</code>关键字),它的<code>this</code>被绑定到正在构造的新对象</li><li>在类的构造函数中,<code>this</code>是一个常规对象,类中所有非静态的方法都会被添加到<code>this</code>的原型中</li></ul></li><li>在箭头函数中,<code>this</code>执行它被创建时的环境</li><li>使用<code>apply</code>、<code>call</code>、<code>bind</code>等方式调用:根据 API 不同,可切换函数执行的上下文环境,即<code>this</code>绑定的对象</li></ul><p>由于<code>this</code>指向的场景较复杂,在 ES6 箭头函数还没出现之前,为了能正确获取某个运行环境下<code>this</code>对象,我们常常会使用<code>var that = this;</code>、<code>var self = this;</code>这样的代码将变量分配给<code>this</code>,便于使用。如今这种做法不再被提倡,通过正确使用箭头函数,我们可以更好地管理作用域。</p><p>到这里,围绕 JavaScript 的编译阶段和执行阶段中执行上下文创建相关的内容已经介绍完毕。</p><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>本文主要介绍了 JavaScript 代码的执行过程,该过程分为语法分析阶段、编译阶段、执行阶段三个阶段,主要围绕着编译阶段和执行阶段进行描述,但 JavaScript 代码运行过程远比这复杂。</p><p>实际上,关于 JavaScript 代码执行这块,越研究越深,还是有很多东西可以学习的。</p>]]></content>
<summary type="html">
<p>上一篇<a href="https://godbasin.github.io/2025/07/06/front-end-basic-js-extend/">《前端基础补齐–有关 JavaScript 继承》</a>我们介绍了 JavaScript 的原型和继承,JavaSc
</summary>
<category term="js什锦" scheme="https://godbasin.github.io/categories/js%E4%BB%80%E9%94%A6/"/>
<category term="分享" scheme="https://godbasin.github.io/tags/%E5%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>前端基础补齐--有关 JavaScript 继承</title>
<link href="https://godbasin.github.io/2025/07/06/front-end-basic-js-extend/"/>
<id>https://godbasin.github.io/2025/07/06/front-end-basic-js-extend/</id>
<published>2025-07-06T13:14:11.000Z</published>
<updated>2025-07-06T13:15:22.154Z</updated>
<content type="html"><![CDATA[<p>我们都知道,前端页面中HTML用于描述页面结构,CSS用于装饰页面样式,这两者结合得到一个暂时还没办法和用户交互的静态页面。为了使得页面能接收用户的输入,然后进行相应的反馈,我们需要用到JavaScript。</p><p>如今前端项目规模越来越大,架构也越来越复杂,甚至在一些项目中我们都接触不到页面布局、样式调整等工作。但不管如何,基本上都不会离开 JavaScript 开发,因此我们需要掌握 JavaScript 的一些特点,所谓知己知彼,百战不殆。</p><p>对于Java、C++、C#这些编程语言来说,它们都是基于类来实现继承的。JavaScript最初设计是为了让网页可交互,因此使用了更简单的继承方式:基于原型继承。在JavaScript中,每个对象拥有一个原型对象,并可以从中继承方法和属性。</p><p>因此,在JavaScript中并没有<code>public</code>、<code>private</code>、<code>protect</code>等关键字,我们如今经常使用的<code>class</code>也只是ES6的语法糖,JavaScript本质上依然是基于原型来实现继承的。</p><blockquote><p>翻出来旧文新发</p></blockquote><h2 id="原型对象"><a href="#原型对象" class="headerlink" title="原型对象"></a>原型对象</h2><p>当谈到继承时,JavaScript只有一种结构:对象。</p><p>几乎所有JavaScript中的对象都是<code>Object</code>的实例,包括函数、数组、对象等。在JavaScript中,对象由一组或多组的属性和值组成:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">key1</span>: value1,</span><br><span class="line"> <span class="attr">key2</span>: value2,</span><br><span class="line"> <span class="attr">key3</span>: value3,</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>JavaScript中的对象之所以用途广泛,是因为它的值既可以是原始类型(<code>numver</code>、<code>string</code>、<code>boolean</code>、<code>null</code>、<code>undefined</code>、<code>bigint</code>和<code>symbol</code>),还可以是对象和函数。其中,函数也是一种特殊的对象,它同样拥有属性和值,所有的函数会有一个特别的属性<code>prototype</code>。除此之外,在V8引擎中函数还有<code>name</code>(函数名)和<code>code</code>(函数代码)两个隐藏属性,因此可被调用。</p><p>在一个对象中,属性的值同样可以为另外一个对象,因此我们可以通过这样的方式来实现继承:使用一个代表原型的属性,属性的值为被继承的对象,此时可以通过层层查找来得到原型链上的对象和属性。在JavaScript中,该属性便是<code>__proto__</code>,被继承的对象即原型对象<code>prototype</code>。通过<code>prototype</code>属性,一个对象可以访问其他对象的属性和方法。</p><p>作为一种特殊的对象,我们来看一下函数的原型对象有什么:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Person</span>(<span class="params">name</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">name</span> = name;</span><br><span class="line">}</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title class_">Person</span>.<span class="property"><span class="keyword">prototype</span></span>)</span><br></pre></td></tr></table></figure><p>打印结果为:</p><p><img src="data:image/7WBcWcz8lvQPOiKueityQXCep2N9szXeWRZch94EGQg;base64,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" alt="图片"></p><p>我们可以看到,<code>Person</code>函数的原型对象(<code>prototype</code>)有两个属性:<code>constructor</code>和<code>__proto__</code>。我们已经知道,<code>__proto__</code>属性值指向原型对象,在这里是<code>Objcet</code>。此时,我们可以获得这样的关系:</p><p><img src="data:image/XF3-DoPyDRF0J2GHjh800CpUuQFMjUgMTHCbPVx-TkY;base64,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/WkOAqCR3mS1GskYEZHhU5Sx6fOkb8T8iEbtfXjahSjJ57I9Bzy/y5ydpdKUyZv5CQqtxVqDpn04W8t2lnTmCRcaWwBbUVTLssYktzzTAVA29XCdMhBMj7qoROSYUpSxaSaAbmmLzto88HmYeB5Ymd2G1tqpLrjP7jLnxCu16O9bQOe7FJoXL7QGRJo/BbqPrm9tXXgwn/954M/X4dKfA8+lo1/cNvO/CDRKl6CzXmokn0VEbBj77ASjdqD9ahb6/DT9t1WKhZ90xmiSIRshJmtyCBCEZTebM32bM4yLqZfjOOs/GEyCgXocZ51PA+C14zjr5dGsqVrcjCQ9dX0oR/C4ngejp5I1VA622CpZ3ctYNX01S7Bf/h4Ay6t54Akau1dUna8AtfmNbjbnC+5u076afuNfzsZNXM4etfj/MgHwdCHuMFvPM/979cNITy0itydFGbum31oNAK9SX0/euzz73mrgAsFC/fJer0Au1yt+z5zmBRov2JmFefq90AeC6rWJh7ORE5erZRfccvXadDIi9MANPgfAzPVq2QP8SvW6/Q40ReS2ZbtH5qPvuO6XLSovW1SyhsrBFlslq2yZYoEz6g2QY05n2BGiGjMREbFHUUZEROxRlBEREXsUZURExB49/ZfhUuMfsUtn2FGjexkREbFHUUZEROxRlBEREXsUZURExB5FGRERsWfIbcyU90FERAp0LyMiIvYoyoiIiD2KMiIiYo+ijIiI2DO8XsxERET66V5GRETsUZQRERF7FGVERMQeRRkREbFHUUZEROxRlBEREXsUZURExB5FGRERsUdRRkRE7FGUERERexRlRETEHkUZERGxR1FGRETsUZQRERF7FGVERMQeRRkREbFHUUZEROxRlBEREXsUZURExB5FGRERsUdRRkRE7FGUERERexRlRETEHkUZERGxR1FGRETsUZQRERF7FGVERMQeRRkREbFHUUZEROxRlBEREXsUZURExB5FGRERsUdRRkRE7FGUERERexRlRETEHkUZERGxR1FGRETsUZQRERF7FGVERMQeRRkREbFHUUZEROxRlBEREXsUZURExB5FGRERsUdRRkRE7FGUERERexRlRETEHkUZERGxR1FGRETsUZQRERF7FGVERMQeRRkREbFHUUZEROxRlBEREXsUZURExB5FGRERsUdRRkRE7FGUERERexRlRETEHkUZERGxR1FGRETsUZQRERF7FGVERMQeRRkREbFHUUZEROxRlBEREXsUZURExB5FGRERsUdRRkRE7FGUERERexRlRETEHkUZERGxR1FGRETsUZQRERF7FGVERMQeRRkREbFHUUZEROxRlBEREXsUZURExJ7/A9BlHnKnw5DTAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTAxLTAzVDA4OjExOjQwKzAwOjAwSTrM8QAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wMS0wM1QwODoxMTo0MCswMDowMDhndE0AAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDEtMDNUMDg6MTE6NDArMDA6MDBvclWSAAAAAElFTkSuQmCC" alt="图片"></p><p>默认情况下,所有原型对象(<code>prototype</code>)自动获得一个<code>constructor</code>属性,指向与之关联的构造函数。我们也能看到,<code>Person</code>函数的原型对象的<code>constructor</code>属性值便是指向<code>Person</code>函数自身。当我们创建对象时,JavaScript就会创建该构造函数的实例。</p><p>我们可以使用工厂模式、构造函数模式、原型模式等各种模式创建一个对象,具体来说可以使用以下方法:</p><ul><li>使用语法结构创建对象:即定义一个数组、函数、对象等,如<code>var o = {a: 1};</code>、<code>function f(){}</code></li><li>使用构造器<code>new XXX()</code>创建对象:构造器其实就是一个普通的函数,当我们使用<code>new</code>操作符的方式使用这个函数时,它就被我们称为构造函数</li><li>使用<code>Object.create()</code>创建对象:使用<code>Object.create(null)</code>可以创建出来的没有原型的对象</li><li>使用ES6<code>class</code>关键字创建对象:<code>class</code>是ES6语法糖,JavaScript依然是基于原型的</li></ul><p>不管是哪种方式,都可以理解为通过将<code>__proto__</code>属性赋值为原型对象(<code>prototype</code>)来实现继承。其中,最常见的便是使用构造函数来创建对象,在这个过程中创建的实例通过将<code>__proto__</code>指向构造函数的原型对象(<code>prototype</code>),来继承该原型对象的所有属性和方法。</p><p>也就是说,当我们运行以下代码时:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> lily = <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">'Lily'</span>)</span><br></pre></td></tr></table></figure><p>实际上JavaScript引擎执行了以下代码:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> lily = {} </span><br><span class="line">lily.<span class="property">__proto__</span> = <span class="title class_">Person</span>.<span class="property"><span class="keyword">prototype</span></span></span><br><span class="line"><span class="title class_">Person</span>.<span class="title function_">call</span>(lily, <span class="string">'Lily'</span>)</span><br></pre></td></tr></table></figure><p>我们来打印一下<code>lily</code>实例:</p><p><img src="data:image/XF3-DoPyDRF0J2GHjh800CpUuQFMjUgMTHCbPVx-TkY;base64,iVBORw0KGgoAAAANSUhEUgAAAPwAAABwCAMAAAANB+32AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAGAUExURf///9vb2z09PSIiInBwcPb29sjIyImJiaqqqlJSUu7u7n19fWJiYpWVlVFRUeXl5fDw8Nvn+7zR+LPL9yEhIW9vb+CFg9lraPjk5NFLSOinpfba2ss2MvDGxfrt7Tw8PPL2/vv8/8/e+nx8fP329tBKR9zc3GFhYdVcWqenp3Jycq+vr5+fn7+/v9DQ0Jubm7a2tlZWVuDg4GNjY62trXFxccQaFtx4duvx/f3+/9rm+3p6eoODg5GRkfj4+IeHh+adm+jv/cfY+ff5/uHr/MrKytPT056enu3t7VBQUPPQz+69u7i4uMHBwdGk1caOyqJGqYgTkfbs9sCBxOLF5LJmuMyZ0Pr2++zZ7bl0vpYwnqtXsaFFqPr0+sya0PPo9Lm5ueTk5IyMjNGj1OfQ6bhxvdmz2+3c78eQy8CDxePI5vz6/caPytWs2NSp1/Xp9dKn1tjb8So9tA0iqn6J0vb2/GJwyMPI6nt7e1Niw+zu+I2Njd2736Kq3uDC40FSvP5vrkMAAAABYktHRACIBR1IAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAB3RJTUUH6QEDCAspfR92qwAAAAFvck5UAc+id5oAAAlvSURBVHja7ZyNf9PGGcdPJD4pKnEpkZ3GjuOOpIR6YDumc7umwAoEWAuBYBjpiwmrazsv7baOttva0v7re57n7qSTIzu2cbAV7veJZJ1ebH3vee509+ROjBkZGRl1kXVqajpxaC+fmrIjT7cd7aA94zKWmJ46ZQUnJN6wWIzEI+AZcyLhHXGuo8Ez5p7Wea3T7riJjgdegYUPhuG7ZNurlz01BcYBx0TXdmZnMIVeO+WQv+OHgsckP4QgvoAuwZNtRzvoFxiCR59ITDuH82Js7HRzdEu2zRyAg7uXhdJKWsyd4QrenXZpCcPDVUiF52LaL8/SuFyDx5V0DJVFY5U0ATHBfeId42KTwbmtbpMQyLZ6xeXDAhHBw1l+7kTAM8fBPya2xq8u8LCG2roTvrOg6vBYbtD7e8Jbp9+UjjER8OS1wu1xCeCZ9Qa5Mi4SwTolCzyfcgI+/ALb9nOml9vjifI0mx95Z69A0mLuDD2YJTym0L/R0bn0d4Dg4kPCO7jXVlUlfpDpRWmWB+WV+H3yCwX0hFR4oxJXNXnPZzifiXoOxl5U2dGTwYlsE5DEcy5ujZyjRW4v0LqWZ1s2eWPWvDU6OTrzGmvceW9kZGRk1ENvnZ0jnX1r3HcyBvY5X5ND73ipxEAXpOffXsgEyewiJdIZ/ZzsYi6zsMQSqbzfEhZ2f+cP58D2x4CRSHmeN2h/y1oeDJ2tzGey72YDWgG/Mp/TT8otLKXTlLX+/cwF8HMjAj4vhdvuqsXc5IBhhoFjMksXsrn5lU5a2KsnM2l5PPj6OU0jgn+vQPojblurLlifQwZ4HvymcxE8AbLC9jx0Pu5hylq+FPYO6sMVS+W1tTJjlctra5crxVLp8vtrJcZg55/OIEH+cDdO0i4tVMnC2Q9W1M5QAQi6iAj9IfvzRyOET6wj+8fkuhwKr5NKYB7gYgMreJ2KRyVdPIFDCdc93U1h17wMqMUSq0xXWOXK1eIav3aNl1gJsgMX5iQP998FLfvLEroBmhpTaO6QD6AtAviPPrl+4+YI4dkGwt+iTUcUebA02jiREh7HydBYJDBLKA98yydSAqt87Sorl4m0XIK/M8uVchndYA0OREvQ4oZe30FCZIafuUn1Y+Tv526cGyU8my0Ubuu5rDJb2FThoyugE2CITi/mlBFXrxXBxsWrVyqwCXlQBPOXisWu4BqtLPuqBsilVz7QCz/9rAZ/88b1kcLfKazfEbSrloCXsabAucHdrb9aDBbMGc0VZUaAoXHBPCitFWGriLYvUnnHyyOKvF/fgZdnPyVfz+SwMHwWeuRpwT6C//CduU+ujxCe3b0XzmUr73ngzaqse5TCMgEM9EBY1VioQqosn6EFKrj3r1TI7LCCjKBaMLq+W6hWqapbma+in6er1QztDjm9HhKj5/xNxm4ez3N+CCWWRxuOoxZA9NdPXgsv5AUvr6WFkNPT8yegn7i2vTNwk7C7oASE2K183oR5jYyMjIwmQs7Mm9P6Ey+9sEQPrkWtxWKd4s7MyfrnrBAHeGjjZy8safAdAQp3hvf453WMxacTtg4vFYY/bfHYwG/ef7D14CFjta2trUeYevy3rU1KPn6CXb+IqKWEzy5WRe+E4DMUssoM+Ovjhgfk2iZtbn/+ZPOLRw9qmzW2CXtw6QXPVDyC4HHV6RITLyAVC1j+wcPN2sMvtzdrTx5DaqvW5ZJIeAzOZGJmeAW//dU22/7Sh/98u8cl0fDZC2+HgjNxkIR/BOW+5lue1bDAs0HcHur+dLrfH50USXh09PuPfXjy+0fd4BcpOpODNVR5FKvBJ32umhvi90+Icgsxq+5GqNg950aodDV2Jd7IyMjIyMgIVX+6M+5bGJk2ngn9/XWEPy8G6Hx9uM9Sb3zTbLYYa7dazWabMVjv1nFNnzt7zeb+4a9z4vWvtrsEH2H4+m7jAM3cBvJ246AF+bDfOJCW3/l2B05oxx2efQ3s9yL2E2WLwCF18B2kdp7WJXwbsoK19gf7pQnUnfXCbNT+1wKebaxHhpeREmkFPEO3x+Wggc6Obo9Lp+Lm9oxF1/T13WZzD/Ak/EGj2aStNlV4sG4eLvIxhI/WSXqmGfie2m+S/rFHH7v/7AO+JS75l/hojZvAyMjIyKgvnX+VP/b9v5//MOSlR050GWaA8Ozsnf5OlM2+aPXXQvjxp/8ECZzt0P+MLfvo8a98wPlfCF/477NXBf/9/372t8WY/n7nLTl9gIVmCPQLXyj8Ipv6MlwhGvH1xq+Uwhb+bh2b/dimOfjum72m6PUTsLikI96hgiGd/+V9ofk8jeTGMT42OQC3bZrvhAP/HTEbwAFP5kmaBiAmCXDb8Q/mLbgifxEHRDtiqsAwLyiZLfjTb3b2qN/SRopvdzCyQV082YqT/dxG4wB6eQpeXtIR75DBkDD8jz89B71QSeThSVdN6nCAiotpT3gQZ3ysum4yb1FRlm+m8ehNPXRwOcG9S6nUJTElRh4fCn5dRDb2BeY+mVvjg8zQ4NsKFhd5SUeXv0sJ0b2eTIwW9+R8J3HjbhKNSA4MroHT/3ASkBzvj74P9R5NAML5YLDbcdANZN0xFPxtaZ5oeOzpBv3cl4HXvV69NEzecFBVIz7B4gw3gMJJQMIfKEtsjqdiZWFzOGRzrZob/D1ks+u31KYMVaDbwxLAU3SHohwBPDh3ay+IboTjHQo+XOZDdb2qnWRNFkzy8i2e5P57rAS8nPmDV9oehy2Otuf+bL4h5oNsaLeHNd0+9fX2dgLjqs5biyo8AY97f8Mwl7wkHO+Ihg95vbpTmtnL1WMKZ3ni9E4xr9MvzgJeTYOECu7iskVmR7ewRRU54S9k+mHY9k1/DzGenNwxvy+e//7z0Bf30X7BZ8AJ1ZEvl9MqDSMjIyMjo5COP5ITvBrAyk/Eu2AD9R3JkRrkvzo29bR7w494hvaA8P1GcgaHd4JXk3TXmOGDSE6YcpgxKtwLmmKEBStr+SKFZUQXRoVuZLCG3lsytla5FsnpgB9mjIoOTxaHPomVt5XrYyvVES8jsfI8yKJxSYvkdMC/7DANHx7fSiSCFTYXDuD5L+YZO/ztrsM0Xgred/swvIzZTAb8+q3oA0ONUdHdXr0dnEKVok+Kbm+LTdUFHSLgPDptdOspDzVGJQSPHo5RuLxfpSG8qvhkkBo/Jy8McQzDNMZq5XHDT3K0ZfAxKh3DWuo9yfOeFxfDGxnFVP8HVf/69m1C3poAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjUtMDEtMDNUMDg6MTE6NDErMDA6MDDvTcdFAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI1LTAxLTAzVDA4OjExOjQxKzAwOjAwnhB/+QAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNS0wMS0wM1QwODoxMTo0MSswMDowMMkFXiYAAAAASUVORK5CYII=" alt="图片"></p><p>可以看到,<code>lily</code>作为<code>Person</code>的实例对象,它的<code>__proto__</code>指向了<code>Person</code>的原型对象,即<code>Person.prototype</code>:</p><p><img src="data:image/XF3-DoPyDRF0J2GHjh800CpUuQFMjUgMTHCbPVx-TkY;base64,iVBORw0KGgoAAAANSUhEUgAABDAAAAG0CAAAAADh5oijAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAACYktHRAD/h4/MvwAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAAd0SU1FB+kBAwgLKX0fdqsAAAABb3JOVAHPoneaAAAs9klEQVR42u3d/28j52Hn8fcc+qvrnaV/MnzGHkc/nOE7yJdwF4Gr9XkDeEdZF4GDuiE3DXIubHhD5XBF6pyUygmMovbaI9yl16JeaQ0XZxSXkIod2CgsZbkLZA2TDeI17UpoDf9Qj2IYTX8iQzv5A577YWb4RV92H4kUhxQ/rx92qdFw+MyQ/OiZZ57nGccgImLn36VdABEZHwoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARawoMEbGmwBARa7/T5/OdtHdgO5N2AUSOMNUwRMSaAkNErPV7SsJInQSM3AmSyNGiGsatbTqzuy0uOLW0SyYyZAqMW/vF7ouvpl0ukaEbwCnJkff6rktrrcN6vck4sRqhM9mbm4i3w/rdmOwaxpLjsH7ScaaWmkDNcWqbJ50loFxwHOfk01vArFOh4jizQPPyScdxZi83oeachtOOs+Y4JwFoOs5JZp1ZlqYc52Q52n551nGOz22lvZsiA2P60/8WBmm/pQmgGB2GXMOYKqy5EJiGHx8cd8OY+LFvzIZHe+Vq/LBahNAYY9Zgxfh48epFY4wpdrayz72YAGl/VvR2HOjdmPjAIGiYjRwExlQh7y4GVbMI+dA0VsBtGGPAN8YYk4OgYcJi9HMAVWNMFVaMMaYIDeODWzJmzYVqFEehqbp443xMD8U47eI4lfXw93DiAyNvjDEheMZUiTIg+in69YppB0YprjcYHzbagWE8csYY45I3xo/TI1rVizazAmtj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" alt="图片"></p><p>很多初学者容易搞混构造函数和<code>constructor</code>属性、原型对象(<code>prototype</code>)和<code>__proto__</code>、实例对象之间的关系,现在我们可以直观地看到:</p><ol><li><p>每个原型对象(<code>Person.prototype</code>)都拥有<code>constructor</code>属性,指向该原型对象的构造函数(<code>Person</code>)。</p></li><li><p>使用构造函数(<code>new Person()</code>)可以创建对象,创建的对象称为实例对象(<code>lily</code>)。</p></li><li><p>实例对象通过将<code>__proto__</code>属性指向原型对象(<code>Person.prototype</code>),实现了该原型对象的继承。</p></li></ol><p>我们能看到,实例(<code>lily</code>)与构造函数原型(<code>Person.prototype</code>)之间有直接的关系,但与构造函数(<code>Person</code>)之间没有。</p><p>关于<code>__proto__</code>和<code>prototype</code>,很多时候我们容易搞混:</p><ul><li><p>每个对象都有<code>__proto__</code>属性来标识自己所继承的原型对象,但只有函数才有<code>prototype</code>属性</p></li><li><p>通过<code>prototype</code>和<code>__proto__</code>,JavaScript可以在两个对象之间创建一个关联,使得一个对象可以访问另一个对象的属性和函数,从而实现了继承</p></li></ul><h2 id="原型链"><a href="#原型链" class="headerlink" title="原型链"></a>原型链</h2><p>原型链是JavaScript中主要的继承方式。我们已经知道,一个对象可通过<code>__proto__</code>访问原型对象上的属性和方法,而该原型同样也可通过<code>__proto__</code>访问它的原型对象,这样我们就在实例和原型之间构造了一条原型链。图中红色的线则为原型链:</p><p><img src="data:image/XF3-DoPyDRF0J2GHjh800CpUuQFMjUgMTHCbPVx-TkY;base64,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" alt="图片"></p><p>JavaScript中的所有对象都来自<code>Object</code>,因此默认情况下,任何函数的原型属性<code>__proto__</code>都是<code>window.Object.prototype</code>。<code>prototype</code>原型对象同样会具有一个自己的原型,层层向上直到一个对象的原型为<code>null</code>。</p><p>关于原型链,我们需要知道:</p><ul><li><p>当试图访问一个对象的属性时,会优先在该对象上搜寻。如果找不到,还会依次层层向上搜索该对象的原型对象、该对象的原型对象的原型对象等(套娃告警)</p></li><li><p>根据定义,<code>null</code>没有原型,并作为这个原型链中的最后一个环节</p></li><li><p>在<code>__proto__</code>的整个原型链被查看之后,浏览器才会认为该属性不存在,并给出属性值为<code>undefined</code>的结论</p></li></ul><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 任何函数的原型属性 __proto__ 都是 Object.prototype</span></span><br><span class="line"><span class="comment">// Object.getPrototypeOf() 方法返回指定对象的原型</span></span><br><span class="line"><span class="comment">// 我们能看到,null 作为原型链中最后一个环节</span></span><br><span class="line"><span class="title class_">Object</span>.<span class="title function_">getPrototypeOf</span>(<span class="title class_">Object</span>.<span class="property"><span class="keyword">prototype</span></span>) === <span class="literal">null</span>; <span class="comment">// true</span></span><br></pre></td></tr></table></figure><p>我们来看个具体的例子:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 让我们假设我们有一个对象 o, 其有自己的属性 a 和 b:</span></span><br><span class="line"><span class="keyword">var</span> o = {<span class="attr">a</span>: <span class="number">1</span>, <span class="attr">b</span>: <span class="number">2</span>};</span><br><span class="line"><span class="comment">// o 的原型 o.__proto__有属性 b 和 c:</span></span><br><span class="line">o.<span class="property">__proto__</span> = {<span class="attr">b</span>: <span class="number">3</span>, <span class="attr">c</span>: <span class="number">4</span>};</span><br><span class="line"><span class="comment">// 最后, o.__proto__.__proto__ 是 null.</span></span><br><span class="line"><span class="comment">// 这就是原型链的末尾,即 null,</span></span><br><span class="line"><span class="comment">// 根据定义,null 没有__proto__.</span></span><br><span class="line"><span class="comment">// 综上,整个原型链如下:</span></span><br><span class="line">{<span class="attr">a</span>:<span class="number">1</span>, <span class="attr">b</span>:<span class="number">2</span>} ---> {<span class="attr">b</span>:<span class="number">3</span>, <span class="attr">c</span>:<span class="number">4</span>} ---> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 当我们在获取属性值的时候,就会触发原型链的查找:</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(o.<span class="property">a</span>); <span class="comment">// o.a => 1</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(o.<span class="property">b</span>); <span class="comment">// o.b => 2</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(o.<span class="property">c</span>); <span class="comment">// o.c => o.__proto__.c => 4</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(o.<span class="property">d</span>); <span class="comment">// o.c => o.__proto__.d => o.__proto__.__proto__ == null => undefined</span></span><br></pre></td></tr></table></figure><p>原型链带来了继承的遍历,我们不需要在创建对象的时候给该对象重新赋值/添加方法,便可以通过原型链去访问原型对象上的属性和方法。比如,我们调用<code>lily.valueOf()</code>时,JavaScript引擎会进行以下操作:</p><ol><li>先检查<code>lily</code>对象是否具有可用的<code>valueOf()</code>方法。</li><li>如果没有,则检查<code>lily</code>的原型对象(<code>Person.prototype</code>)是否具有可用的<code>valueof()</code>方法。</li><li>如果也没有,则检查<code>Person()</code>构造函数的<code>prototype</code>属性所指向的对象的原型对象(即<code>Object.prototype</code>)是否具有可用的<code>valueOf()</code>方法,于是该方法被调用。</li></ol><p>我们能看到,通过原型链进行属性的查找会层层遍历每个原型对象,因此也可能会带来性能问题:</p><ul><li>当试图访问不存在的属性时,会遍历整个原型链</li><li>在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要</li></ul><p>因此,我们在设计对象的时候,需要注意代码中原型链的长度。当原型链过长时,可以选择进行分解,来避免可能带来的性能问题。</p><p>说到属性查找,前面我们提到JavaScript中对象是由一组或多组的属性和值组成。要保存这些属性和值,一般我们都会想到使用字典,使用字符串作为键名,键值可以是任意对象,通过键名就可以读写键值。</p><p>但我们都知道字典是非线性结构的,会导致读取效率会大大降低。因此,V8引擎采用了一套更为复杂和高效的存储策略,其中便涉及我们常说的快属性和慢属性,大家有兴趣也可以再深入进行研究。</p><h2 id="实现JavaScript继承"><a href="#实现JavaScript继承" class="headerlink" title="实现JavaScript继承"></a>实现JavaScript继承</h2><p>通过原型链可以实现JavaScript继承,实际上JavaScript中实现继承的方式还包括经典继承(盗用构造函数)、组合继承、原型式继承、寄生式继承等等。</p><p>其中,原型链继承方式中引用类型的属性被所有实例共享,无法做到实例私有;经典继承方式可以实现实例属性私有,但要求类型只能通过构造函数来定义;组合继承融合原型链继承和构造函数的优点,是JavaScript中最常用的继承模式,它长这样:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Parent</span>(<span class="params">name</span>) {</span><br><span class="line"> <span class="comment">// 私有属性,不共享</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">name</span> = name;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 需要复用、共享的方法定义在父类原型上 </span></span><br><span class="line"><span class="title class_">Parent</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">speak</span> = <span class="keyword">function</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'hello'</span>);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Child</span>(<span class="params">name</span>) {</span><br><span class="line"> <span class="title class_">Parent</span>.<span class="title function_">call</span>(<span class="variable language_">this</span>, name);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 将子类的 __proto__ 指向父类原型</span></span><br><span class="line"><span class="title class_">Child</span>.<span class="property">__proto__</span> = <span class="title class_">Parent</span>.<span class="property"><span class="keyword">prototype</span></span></span><br></pre></td></tr></table></figure><p>虽然如今我们更倾向于使用ES6中的<code>class</code>,但实际上ES6/ES7中的新特性本质上主要是用来提升开发效率的语法糖。我们可以使用<code>@babel/plugin-transform-classes</code>对<code>class</code>相关代码进行编译,便可以看到编译后的代码本质上也是通过组合继承类似的方式实现继承。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>关于JavaScript的原型和继承,常常会在我们面试题中出现。随着ES6/ES7等新语法糖的出现,我们在日常开发中可能更倾向于使用<code>class</code>等语法来编写代码,原型继承等概念逐渐变淡。</p><p>但不管语法糖怎么先进,JavaScript的设计在本质上依然没有变化。如果不了解这些内容,可能在我们遇到一些超出自己认知范围的内容时,很容易束手无策。</p>]]></content>
<summary type="html">
<p>我们都知道,前端页面中HTML用于描述页面结构,CSS用于装饰页面样式,这两者结合得到一个暂时还没办法和用户交互的静态页面。为了使得页面能接收用户的输入,然后进行相应的反馈,我们需要用到JavaScript。</p>
<p>如今前端项目规模越来越大,架构也越来越复杂,甚至在
</summary>
<category term="js什锦" scheme="https://godbasin.github.io/categories/js%E4%BB%80%E9%94%A6/"/>
<category term="分享" scheme="https://godbasin.github.io/tags/%E5%88%86%E4%BA%AB/"/>
</entry>
<entry>
<title>复杂渲染引擎架构与设计--10.降级渲染</title>
<link href="https://godbasin.github.io/2025/06/15/render-engine-downgrate-render/"/>
<id>https://godbasin.github.io/2025/06/15/render-engine-downgrate-render/</id>
<published>2025-06-15T10:22:21.000Z</published>
<updated>2025-06-15T10:22:46.448Z</updated>
<content type="html"><![CDATA[<p>前面我们在<a href="https://godbasin.github.io/2023/10/11/render-engine-diff-render/">《复杂渲染引擎架构与设计–6.增量计算》</a>一文中介绍了滚动过程中的增量渲染方案,通过减少渲染计算量或绘制量的方式,来提升页面滚动的流畅度。</p><p>除此之外,当滚动距离较远时,增量渲染并不能达到预期的优化效果,此时我看还需要考虑降级渲染。</p><h2 id="页面内的元素优先级划分"><a href="#页面内的元素优先级划分" class="headerlink" title="页面内的元素优先级划分"></a>页面内的元素优先级划分</h2><p>当用户打开一个大的页面时,除了使用搜索获取关键信息,还可能会快速滚动来翻阅整体内容,然后找到关注的信息进行详细查阅。增量渲染的方案,核心是将上一帧的内容尽可能复用到下一帧里,因此在快速滚动的场景下(比如拖动滚动条滚动),页面中可能并没有多少可复用的内容。</p><p>在这样的场景下,如果需要渲染的内容实在很多,我们可以对页面内容进行优先级划分。</p><p>该如何进行优先级划分呢?这个可能需要结合业务的具体情况进行分析,比如:</p><ul><li>分段分组的内容:标题、副标题等</li><li>方便用户定位位置的内容:文本框、图片、背景色等</li></ul><p>像图片这种渲染可能会比较耗时,那么可以用占位符等方式来进行骨架渲染,让用户能快速定位到对应位置之后,再进行详细内容的渲染。</p><p>具体到在线表格的场景下,首先行列位置十分重要,同时方便用户定位位置的还有单元格背景色、边框线、图片等等内容。我们可以这样拆分优先级:</p><ul><li>行列头、行列序号、选区</li><li>单元格背景色、边框线</li><li>图片(占位符)</li><li>文本内容</li><li>其他格式内容/图标(格式错误角标、下拉按钮、icon 内容等)</li><li>真实图片信息</li><li>其他</li></ul><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>页面帧率可以使用<code>requestAnimationFrame</code>来进行计算,当然前提还需要是页面进行了滚动操作,否则的话只是单纯当前页面绘制慢则进行降级,用户会感觉页面内容突然减少,体验较差。</p><p>我们可以在页面开始滚动时,监听 rAF 变化来计算 FPS,当 FPS 明显下降到不流畅的时候,则进入降级渲染,并根据帧率来调整降级渲染的级数。比如(简单举例):</p><ul><li><code>0 < FPS < 10</code>: 最高级别的降级渲染,只渲染边框线和单元格背景色</li><li><code>10 < FPS < 20</code>: 中级别的降级渲染,除了边框线和单元格背景色以外,还渲染单元格富文本内容</li><li><code>20 < FPS < 30</code>: 低级别的降级渲染,除了边框线和单元格背景色、单元格以外,还渲染图片</li><li><code>30 < FPS < 40</code>: 最低级别的降级渲染,渲染仅附加内容(如角标、协作者光标、icon 等)以外的内容</li></ul><h3 id="滚动距离"><a href="#滚动距离" class="headerlink" title="滚动距离"></a>滚动距离</h3><p>在很多情况下,其实我们并不能很好地使用<code>requestAnimationFrame</code>来计算 FPS 帧率,因为 FPS 的计算需要一个累计过程,才能得到平均 1s 内的平均 FPS,同时频繁使用 rAF 本身也可能会影响到页面渲染性能。</p><p>所以,我们可以从别的角度来控制降级渲染的情况,比如使用用户页面滚动的快慢来控制优先级。</p><p>我们依然可以使用渲染本身,在两次渲染之间获取用户的滚动距离,根据滚动距离判断滚动速度,并以此来调整降级渲染的策略。比如,当页面进入滚动状态后,两次绘制之间的滚动距离:</p><ul><li>超过 20 屏内容:最高级别的降级渲染</li><li>10 屏 < 滚动距离 < 20 屏:中级别的降级渲染</li><li>5 屏 < 滚动距离 < 10 屏:低级别的降级渲染</li><li>2 屏 < 滚动距离 < 5 屏:最低级别的降级渲染</li></ul><p>当然,这里的渲染,除了 rAF 本身之外,还可以是 API 的调用如渲染层的 render 接口,或者是 canvas 的绘制。</p><h3 id="渲染插件的降级"><a href="#渲染插件的降级" class="headerlink" title="渲染插件的降级"></a>渲染插件的降级</h3><p>前面在<a href="https://godbasin.github.io/2023/06/15/render-engine-plugin-design/">《复杂渲染引擎架构与设计–2.插件的实现》</a>一文中,我们介绍了除核心渲染内容以外,其他附加格式内容的渲染方式:使用渲染插件。</p><p>由于插件设计的存在,我们通过很简单的方式,就能实现降级渲染的能力。因为一些附加格式的渲染,都是使用渲染插件实现的,比如 icon 绘制、下拉菜单、角标、图片等等,我们可以十分轻易地将渲染插件进行降级优先级的归档。</p><p>当我们判断需要进行降级渲染时,可以直接通过优先级策略,来控制是否跳过某些插件的收集过程,便可以直接达到降级渲染的效果。</p><h2 id="降级渲染的启动和停止"><a href="#降级渲染的启动和停止" class="headerlink" title="降级渲染的启动和停止"></a>降级渲染的启动和停止</h2><p>前面提到,进入降级渲染的前提条件是用户进行滚动,在滚动过程中通过不同的方式判断用户滚动“是否流畅”,如果检测到滚动不流畅的话,则根据卡顿程度进入到不同级别的降级渲染。</p><p>降级渲染,说白了就是减少页面渲染的内容,从而减轻每次渲染的耗时,提升用户的使用流畅度。</p><p>但当用户停止滚动、或是滚动较慢时,这时候可以认为用户需要聚焦阅读页面中的信息,因此这种时候我们还需要退出降级渲染模式,将页面内容进行完整渲染。</p><p>通过这样的方式,我们保证了用户滚动流畅度的同时,还确保了页面内容不会丢失。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文介绍了当页面内容过多,用户在滚动时因为绘制较慢导致不流畅时,使用降级渲染的方式来保证用户的流畅度。</p><p>当然,其实说到底这个策略也会损耗了一些用户体验,但如果有更好的优化方式,我们也不需要使用到降级策略。很多时候,技术的决策便是在各种不同优劣的技术方案中,选出一种性价比更好、投入产出比更好的方案而已。</p>]]></content>
<summary type="html">
<p>前面我们在<a href="https://godbasin.github.io/2023/10/11/render-engine-diff-render/">《复杂渲染引擎架构与设计–6.增量计算》</a>一文中介绍了滚动过程中的增量渲染方案,通过减少渲染计算量或绘制量的
</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>复杂渲染引擎架构与设计--9.预热计算</title>
<link href="https://godbasin.github.io/2025/05/09/render-engine-pre-calculate/"/>
<id>https://godbasin.github.io/2025/05/09/render-engine-pre-calculate/</id>
<published>2025-05-09T12:22:21.000Z</published>
<updated>2025-04-12T07:36:29.762Z</updated>
<content type="html"><![CDATA[<p>前面我们在<a href="https://godbasin.github.io/2023/09/16/render-engine-calculate-split/">《复杂渲染引擎架构与设计–5.分片计算》</a>一文中介绍了分片计算,即将需要计算的内容进行拆分,拆分成约每 50 ms 一个的任务。</p><p>在这个方案中,我们维护了一个待计算区域,将页面中所有未完成的计算任务放在里面,等待异步每个计算任务进行计算。</p><h2 id="全量计算的性能瓶颈"><a href="#全量计算的性能瓶颈" class="headerlink" title="全量计算的性能瓶颈"></a>全量计算的性能瓶颈</h2><p><a href="https://godbasin.github.io/2023/09/16/render-engine-calculate-split/">《复杂渲染引擎架构与设计–5.分片计算》</a>中的方案,已经基本解决大多数场景下的性能问题。但面对超大页面的渲染计算来说,可能依然存在以下瓶颈:</p><ul><li>区域合并和碰撞检测,在待计算任务过多时容易产生性能问题。尤其在列数多的场景下,按行计算的每次任务计算范围都十分仅限</li><li>计算任务过多,当一个页面中的内容十分多的时候(比如一万多列、几十万行的表格),我们还需要考虑是否所有内容都需要异步计算完毕,是否存在资源的浪费</li></ul><p>因此,我们可以考虑更合适的异步方案,该方案需要考虑:</p><ol><li>尽量减少计算内容范围,减少资源浪费。</li><li>尽量提前计算好可能需要的资源,减少用户等待时间。</li></ol><p>这两点要求,看起来有点相互矛盾,毕竟一个要减少计算,一个却要增加计算。但实际上从用户的角度出发,我们的确可以做的更好。</p><h2 id="基于可视范围的预热计算"><a href="#基于可视范围的预热计算" class="headerlink" title="基于可视范围的预热计算"></a>基于可视范围的预热计算</h2><p>我们可以这样调整渲染引擎的计算设计:</p><ol><li>不再计算整个页面所有的数据,而是基于当前停留的界面可视范围来进行计算。</li></ol><ul><li>优先计算当前可视范围的渲染数据</li><li>以可视范围为基础倍数,异步计算横向 3 倍、纵向 15 倍的渲染层数据,放置到异步计算</li></ul><ol start="2"><li>每个倍数范围的计算,作为单独的一个计算任务,一个计算任务计算后,会通过任务调度进行下一个。任务优先顺序为:当前视图范围 -> 视图范围下方 -> 视图范围上方 -> 视图范围右侧 -> 视图范围左侧。</li><li>用户进行滚动操作时,会在滚动停止后,再基于停止后的界面,重复 1 步骤,已计算完成的数据会进行缓存,跳过已计算的内容不重复计算。</li></ol><p>举个例子,当界面停留不滚动时,当前可视范围有 10 列 50 行,则会计算 10 _ 3 = 30 列、 50 _ 15 = 750 行的数据。</p><p>当然,横向 3 倍、纵向 15 倍这个数字也是可以进行调整的,可以埋点记录用户行为,然后观察用户习惯后进行调整。</p><p>该方案的优点在于:</p><ul><li>每个计算任务单元格数量不会十分大,可有效避免按行计算在极端条件下可能计算量很大的场景</li><li>不需要计算整表完整的数据,可有效减少计算量</li><li>可提前预热可视区域附近的区域(横向 3 倍,纵向 15 倍),在用户滚动时可快速渲染</li><li>无需维护待计算区域任务,无需进行碰撞检测和区域合并,简化了计算性能</li></ul><p>至于为什么任务优先顺序会是:下、上、右、左,可参考另外一篇文章<a href="https://godbasin.github.io/2025/04/10/front-end-performance-preload-order/">《前端性能优化–预加载顺序设计》</a>。</p><h2 id="结合-50ms-计算任务拆分"><a href="#结合-50ms-计算任务拆分" class="headerlink" title="结合 50ms 计算任务拆分"></a>结合 50ms 计算任务拆分</h2><p>基于可视区域的计算方案,在大多数场景下都不会有性能问题,但还有一种场景:单个可视区域的计算量十分大,比如用户缩放到 10% 的时候,可能单个计算任务就会存在卡顿。</p><p>因此,我们可以结合之前提到的 50ms 任务拆分(参考<a href="https://godbasin.github.io/2024/04/03/front-end-performance-long-task/">《让你的长任务在 50 毫秒内结束》</a>一文),在异步计算的时候,当当前任务计算已超出 50ms 范围,则结束任务,释放出主线程给用户交互。</p><p>为此,我们可能需要:</p><ul><li>支持脏区标记,标记哪些已计算的数据不再为最新</li><li>支持是否计算完成标记,供异步任务计算时判断是否跳过</li></ul><p>基于此方案,我们需要调整异步任务管理:</p><ol><li>使用新的异步任务(滚动停止时,构建当前可视区域的横向 3 倍、纵向 15 倍异步任务)。</li><li>同步计算可视范围内数据,异步计算可视范围附近区域(考虑使用 requestAnimationFrame 的方式进行异步)。</li><li>当可视范围内单元格数量很多时(缩放倍率较小场景下),按照 50ms 进行二次拆分计算。</li></ol><h2 id="新的数据变更计算"><a href="#新的数据变更计算" class="headerlink" title="新的数据变更计算"></a>新的数据变更计算</h2><p>除了加载现有的数据,当后续数据发生变更的时候,还需要将一些已经计算完毕的数据进行重算,在这样的情况下,我们还需要将一些已经完成计算的数据进行标记,并进行重算。</p><p>在脏区标记完后,我们依然进行可视区域的预热计算,建立可视区域外的各个计算区域任务。当任务执行时,发现区域内存在脏数据,则将这些数据进行重算。计算完成后,则更新数据缓存,并重置脏数据标记位。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>当我们在使用各种技术方案尝试优化的时候,不妨也从用户的角度考虑下,用户最可能的行为是怎样的。大多数用户并不需要完整的网页内容,他们很多时候只会翻阅自己关心的内容,看完之后便会关掉。</p><p>我们还可以通过埋点去分析用户行为,通过确切的数据去调整我们的具体优化方案。</p>]]></content>
<summary type="html">
<p>前面我们在<a href="https://godbasin.github.io/2023/09/16/render-engine-calculate-split/">《复杂渲染引擎架构与设计–5.分片计算》</a>一文中介绍了分片计算,即将需要计算的内容进行拆分,拆分成约
</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/2025/04/10/front-end-performance-preload-order/"/>
<id>https://godbasin.github.io/2025/04/10/front-end-performance-preload-order/</id>
<published>2025-04-10T14:29:12.000Z</published>
<updated>2025-03-12T14:29:43.844Z</updated>
<content type="html"><![CDATA[<p>随着互联网的迅速发展,现在我们前端应用能做的事情也越来越多了。随之而来的便是复杂和大数据内容的网页渲染,因此很多时候我们为了尽快给用户看到网页内容,会将页面加载拆分成首屏和其他内容。</p><p>当首屏内容渲染完后,我们会进行后续内容的预加载处理。</p><h1 id="页面预加载"><a href="#页面预加载" class="headerlink" title="页面预加载"></a>页面预加载</h1><p>对于重文本、列表、表格等网页内容来说,当内容过多、过长时都会考虑进行分页。</p><p>一般来说,由于浏览器页面都是根据屏幕宽度进行加载和换行,因此加载过程基本都是从页面最上方开始,然后往下滚动和加载。很多时候,性能优化都会简单地考虑两个方面:</p><ol><li>内容分页和预拉取。</li><li>长列表的预加载和回收。</li></ol><p>对于长列表、长文本的页面来说,不管是屏幕外资源回收,还是屏幕外数据预拉取、预加载,都会比较简单,因为只有垂直方向。</p><p>这里我们即将讨论的预加载顺序设计,场景则是更加大一些的窗口画布场景。比如在线文档、在线表格、画板绘制等较大的画布,且具备各个方向滚动滑动的场景,我们的屏幕仅能展示其中的一部分,如下图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/preload-order-1.jpg" alt=""></p><h2 id="用户浏览行为参考"><a href="#用户浏览行为参考" class="headerlink" title="用户浏览行为参考"></a>用户浏览行为参考</h2><p>显然,对于画布类的页面来说,数据量可能会无限大,因此数据的分片/分页/分块几乎是必备的。至于如何加载和计算这些数据,之前我在<a href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/">《复杂渲染引擎架构与设计》</a>系列中也有介绍过,包括分片渲染、增量渲染、离屏渲染等等,这里不再赘述。</p><p>当我们在优先加载和渲染完屏幕内范围的数据之后,还需考虑后续其他数据该如何处理加载顺序。而屏幕外的各个方向都有未加载数据,我们可以结合用户行为来调整加载的优先顺序。</p><p>关于用户在网页阅读时的行为模式和视觉关注的研究,有不少相关的文献和数据。参考一些用户的研究报告,我们会发现用户的阅读习惯包括但不仅限于以下方式:</p><ol><li>F 形模式:许多用户在阅读网页时会遵循 F 形模式,即先水平扫描页面的顶部,然后向下移动,进行第二次水平扫描,最后在左侧进行垂直扫描。这种模式表明用户在页面的上部和左侧区域更容易获得注意。</li><li>Z 形模式:在某些情况下,用户可能会遵循 Z 形模式,尤其是在页面布局较为简单时。用户从左上角开始,水平移动到右上角,然后斜向下到左下角,最后水平移动到右下角。这种模式适用于较短的内容或广告。</li><li>中心聚焦:用户的注意力往往集中在页面的中心区域,尤其是当内容以图像、视频或重要信息块的形式呈现时。重要信息通常会放置在页面的中心,以吸引用户的注意。</li><li>边缘注意:页面边缘的内容(如侧边栏、广告或导航菜单)通常会获得较少的关注,但在某些情况下,用户会浏览这些区域以寻找额外的信息或链接。</li><li>滚动行为:用户在阅读长页面时,通常会向下滚动。研究表明,用户在滚动时的注意力会逐渐减少,尤其是在页面的底部。因此,重要信息应尽量放在用户最初可见的区域(即“首屏”)。</li><li>视觉层次:页面设计中的视觉层次(如标题、字体大小、颜色对比等)会影响用户的阅读方向和注意力分布。较大的标题和醒目的颜色通常会吸引更多的注意。</li></ol><p>这些研究很多是采用“使用眼动追踪技术”来分析用户的视觉关注,如下图则是针对一名中国人的研究结果:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/preload-order-2.png" alt=""></p><p>从这些研究结果中,我们可以获得一些可借鉴参考的点,包括:</p><ol><li>当人们以 F 形扫描时,他们会因为文本在列中的流动方式而错过大量内容。这种方式其实不利于网页内容的展示,解决方案则是可以使用用户的其它行为习惯来打断,包括:</li></ol><ul><li>将最重要的要点包含在页面的前两段中</li><li>使用标题和副标题,确保它们看起来比普通文本更重要、更显眼,以便用户能够快速区分它们</li><li>用包含最多信息的单词开始标题和副标题</li><li>在视觉上对少量相关内容进行分组</li><li>用粗体标出重要的单词和短语</li><li>使用项目符号和数字来标注列表或流程中的项目</li></ul><ol start="2"><li>在页面的预加载过程中,我们可以参考用户的行为习惯,作为数据拉取顺序的参考。</li></ol><h2 id="屏幕外数据预加载顺序设计"><a href="#屏幕外数据预加载顺序设计" class="headerlink" title="屏幕外数据预加载顺序设计"></a>屏幕外数据预加载顺序设计</h2><p>考虑到在大画布页面布局下,用户的垂直滚动行为会比水平滚动行为更常见,并且向下比向上、向右比向左的行为可能性更高,我们可以设计以下的数据加载顺序:</p><ol><li>当前屏幕范围。</li><li>屏幕范围下方。</li><li>屏幕范围上方。</li><li>屏幕范围右侧。</li><li>屏幕范围左侧。</li></ol><p>简单粗暴的如图:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/preload-order-3.jpg" alt=""></p><p>这种方式加载,其实有点像棋盘走棋的距离,比如:</p><ol><li>走一步的距离,按照优先顺序为:下、上、右、左。</li><li>走两步的距离,按照优先顺序为:下下、上上、下右、上右、下左、上左。</li><li>以此类推。</li></ol><p>当然,如果页面结构有其他布局变化的话,这种优先顺序显然是不满足的,比如表格考虑冻结、筛选等场景,长文页面布局考虑分栏、图片、标题、副标题等,各种场景下的优先顺序会有影响。</p><p>最好的方式,便是可以在页面中埋个点,验证一下用户在打开首屏后,接下来的行为顺序。同时收集页面中的各种要素,来大致得到用户的行为习惯,并以此来调整页面加载和渲染的优先顺序,给到用户更好的体验。</p><h2 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h2><ul><li><a href="https://www.nngroup.com/articles/how-people-read-online/">《How People Read Online: New and Old Findings》</a></li><li><a href="https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/">《F-Shaped Pattern of Reading on the Web: Misunderstood, But Still Relevant (Even on Mobile)》</a></li></ul><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>本文介绍了用户的一些常见阅读习惯,其实除了说在做数据预加载、预处理的时候可以用到,在更多的场景我们都能使用。比如,调整页面的布局,使得用户可以符合预期地注意到某些内容。</p><p>比如,写文章的时候,也可以使用一些小技巧(分段、标题和副标题、加粗、分组等),来减少阅读用户的流失率。</p>]]></content>
<summary type="html">
<p>随着互联网的迅速发展,现在我们前端应用能做的事情也越来越多了。随之而来的便是复杂和大数据内容的网页渲染,因此很多时候我们为了尽快给用户看到网页内容,会将页面加载拆分成首屏和其他内容。</p>
<p>当首屏内容渲染完后,我们会进行后续内容的预加载处理。</p>
<h1 id=
</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>前端性能优化--FPS页面流畅度</title>
<link href="https://godbasin.github.io/2025/03/09/front-end-performance-fps-monitor/"/>
<id>https://godbasin.github.io/2025/03/09/front-end-performance-fps-monitor/</id>
<published>2025-03-09T13:32:31.000Z</published>
<updated>2025-02-12T12:32:57.659Z</updated>
<content type="html"><![CDATA[<p>之前分享过不少关于前端卡顿检测的内容,实际上在前端应用里,FPS 也是我们常用的一个页面流畅度的指标。</p><p>除了前面介绍的卡顿检测之外,我们还可以使用 FPS 来辅助定义用户体验。</p><h2 id="前端页面-FPS"><a href="#前端页面-FPS" class="headerlink" title="前端页面 FPS"></a>前端页面 FPS</h2><p>我们都知道,帧率(英语:frame rate)是用于测量显示帧数的度量。</p><p>FPS 则是帧率的测量单位,为“每秒显示帧数”(frame per second,FPS)或“赫兹”,一般来说FPS用于描述影片、电子绘图或游戏每秒播放多少帧。</p><p>前面<a href="https://godbasin.github.io/2024/08/05/front-end-performance-task-schedule/">《前端性能优化–任务管理和调度》</a>一文介绍过,浏览器的“一帧”流程为:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/life-of-a-frame.jpg" alt=""></p><ol><li>用户事件。</li><li>一个宏任务。</li><li>队列中全部微任务。</li><li><code>requestAnimationFrame</code>。</li><li>浏览器重排/重绘。</li><li><code>requestIdleCallback</code>。</li></ol><p>因此,在前端开发中,我们常常会使用<code>requestAnimationFrame</code>来计算前端页面的 FPS。</p><h3 id="每秒帧数计算-FPS"><a href="#每秒帧数计算-FPS" class="headerlink" title="每秒帧数计算 FPS"></a>每秒帧数计算 FPS</h3><p>既然知道浏览器中每“一帧”里都会执行<code>requestAnimationFrame</code>,那么 FPS 的计算也很简单:计算一秒内<code>requestAnimationFrame</code>的执行次数 n,则 FPS 为 n。</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">let</span> frameCount = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">let</span> lastTime = performance.<span class="title function_">now</span>();</span><br><span class="line"> </span><br><span class="line"><span class="keyword">function</span> <span class="title function_">loop</span> () {</span><br><span class="line"> <span class="keyword">let</span> now = performance.<span class="title function_">now</span>();</span><br><span class="line"> frameCount++;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 超过 1s,重置计算下一秒</span></span><br><span class="line"> <span class="keyword">if</span> (now > lastTime + <span class="number">1000</span>) {</span><br><span class="line"> <span class="keyword">let</span> fps = <span class="title class_">Math</span>.<span class="title function_">round</span>((frameCount * <span class="number">1000</span>) / (now - lastTime));</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">${now}</span> 1S 内 FPS:`</span>, fps);</span><br><span class="line"> frameCount = <span class="number">0</span>;</span><br><span class="line"> lastTime = now;</span><br><span class="line"> };</span><br><span class="line"> </span><br><span class="line"> <span class="variable language_">window</span>.<span class="title function_">requestAnimationFrame</span>(loop);</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"><span class="title function_">loop</span>();</span><br></pre></td></tr></table></figure><p>通过这种方式,我们可以获取每秒平均的 FPS,但对于 1s 内的一些卡顿可能检测的不算灵敏,这种情况我们可以考虑用另外一种计算方式。</p><h3 id="rAF-间隔计算-FPS"><a href="#rAF-间隔计算-FPS" class="headerlink" title="rAF 间隔计算 FPS"></a>rAF 间隔计算 FPS</h3><p>除了计算每秒的渲染次数,我们还有另外一种计算方式:假设两次<code>requestAnimationFrame</code>间的执行间隔为 t 毫秒,那么 FPS 可以为 1000 / t。</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><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> lastTime = performance.<span class="title function_">now</span>();</span><br><span class="line"> </span><br><span class="line"><span class="keyword">function</span> <span class="title function_">loop</span> () {</span><br><span class="line"> <span class="keyword">let</span> now = performance.<span class="title function_">now</span>();</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">let</span> fps = <span class="title class_">Math</span>.<span class="title function_">round</span>(<span class="number">1000</span> / (now - lastTime));</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">${now}</span> FPS:`</span>, fps);</span><br><span class="line"> </span><br><span class="line"> <span class="variable language_">window</span>.<span class="title function_">requestAnimationFrame</span>(loop);</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"><span class="title function_">loop</span>();</span><br></pre></td></tr></table></figure><p>一般来说,由于<code>requestAnimationFrame</code>的执行次数实在太多了,如果逻辑太多则反而会导致性能问题。</p><p>因此,很多时候我们并不会在整个页面打开过程中都启动 FPS 检测,会在某些场景下才启动 FPS 监测。</p><h2 id="FPS-监测模块"><a href="#FPS-监测模块" class="headerlink" title="FPS 监测模块"></a>FPS 监测模块</h2><p>由于用户并不是时时刻刻都在操作页面,不进行操作时,页面的流畅度便有些无从谈起。</p><p>所以,我们可以局部监测用户行为和页面行为,在需要的时候启动 FPS 监控:</p><ul><li>页面滚动</li><li>用户编辑输入</li><li>页面渲染过程</li></ul><p>为此,我们需要提供一个 FPS 监测模块,包括启动和结束统计某阶段 FPS 的能力:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Fps</span> {</span><br><span class="line"> <span class="keyword">private</span> running = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">private</span> last = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> timer = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">record</span>: <span class="built_in">number</span>[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 启动 fps 监听</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> start = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">running</span>) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">warn</span>(<span class="string">'已经启动 fps 监听'</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">running</span> = <span class="literal">true</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">last</span> = performance.<span class="title function_">now</span>();</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">timer</span> = <span class="title function_">requestAnimationFrame</span>(<span class="variable language_">this</span>.<span class="property">run</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"> * 结束 fps 监听</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> end = <span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">window</span>.<span class="title function_">cancelAnimationFrame</span>(<span class="variable language_">this</span>.<span class="property">timer</span>);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">running</span> = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">record</span>.<span class="property">length</span> > <span class="number">0</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">calculateFPS</span>();</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">record</span>.<span class="property">length</span> = <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> run = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> now = performance.<span class="title function_">now</span>();</span><br><span class="line"> <span class="keyword">const</span> diff = now - <span class="variable language_">this</span>.<span class="property">last</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">record</span>.<span class="title function_">push</span>(diff);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">last</span> = now;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">timer</span> = <span class="title function_">requestAnimationFrame</span>(<span class="variable language_">this</span>.<span class="property">run</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"> * 结算该阶段 FPS</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> calculateFPS = <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> record = <span class="variable language_">this</span>.<span class="property">record</span>;</span><br><span class="line"> <span class="keyword">const</span> avageFPS = record.<span class="title function_">reduce</span>(<span class="function">(<span class="params">a, b</span>) =></span> (a + b), <span class="number">0</span>) / record.<span class="property">length</span>;</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`该阶段平均 FPS:<span class="subst">${avageFPS}</span>`</span>);</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>前面也说过,我们希望更准确获取到整个监测过程中的页面流畅度,而不只是一个简单的平均值。因此,我们可以统计该阶段中 FPS 整体情况:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">getIntervalFPS</span>(<span class="params">recore: <span class="built_in">number</span>[]</span>) {</span><br><span class="line"> <span class="keyword">const</span> internalMap = {</span><br><span class="line"> <span class="string">'0-10'</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">'11-20'</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">'21-30'</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">'31-40'</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">'41-50'</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">'51-60'</span>: <span class="number">0</span>,</span><br><span class="line"> <span class="string">'>60'</span>: <span class="number">0</span>,</span><br><span class="line"> };</span><br><span class="line"> <span class="keyword">let</span> maxFps = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> recore.<span class="title function_">forEach</span>(<span class="function">(<span class="params">interval</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> value = <span class="title class_">Math</span>.<span class="title function_">round</span>(<span class="number">1000</span> / interval);</span><br><span class="line"> maxFps = <span class="title class_">Math</span>.<span class="title function_">max</span>(value, maxFps);</span><br><span class="line"> <span class="keyword">switch</span> (<span class="literal">true</span>) {</span><br><span class="line"> <span class="keyword">case</span> value <= <span class="number">10</span>:</span><br><span class="line"> internalMap[<span class="string">'0-10'</span>] += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> value <= <span class="number">20</span>:</span><br><span class="line"> internalMap[<span class="string">'11-20'</span>] += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> value <= <span class="number">30</span>:</span><br><span class="line"> internalMap[<span class="string">'21-30'</span>] += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> value <= <span class="number">40</span>:</span><br><span class="line"> internalMap[<span class="string">'31-40'</span>] += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> value <= <span class="number">50</span>:</span><br><span class="line"> internalMap[<span class="string">'41-50'</span>] += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> value <= <span class="number">60</span>:</span><br><span class="line"> internalMap[<span class="string">'51-60'</span>] += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="attr">default</span>:</span><br><span class="line"> internalMap[<span class="string">'>60'</span>] += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> internalMap,</span><br><span class="line"> maxFps,</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过这样的方式,我们可以获得某个监控阶段期间,是否存在交互不流畅的情况,而不仅是一个总的平均值。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>FPS 检测和卡顿检测其实原理都很相似,卡顿可能会监测整个页面生命周期,而 FPS 则用来监控页面滚动、用户交互、页面渲染等流程。</p><p>两者相辅相成,可以从不同的维度共同搭建前端页面的流畅度。</p>]]></content>
<summary type="html">
<p>之前分享过不少关于前端卡顿检测的内容,实际上在前端应用里,FPS 也是我们常用的一个页面流畅度的指标。</p>
<p>除了前面介绍的卡顿检测之外,我们还可以使用 FPS 来辅助定义用户体验。</p>
<h2 id="前端页面-FPS"><a href="#前端页面-FPS"
</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>前端性能优化--JavaScript 引擎性能</title>
<link href="https://godbasin.github.io/2025/02/09/front-end-performance-fast-properties/"/>
<id>https://godbasin.github.io/2025/02/09/front-end-performance-fast-properties/</id>
<published>2025-02-09T02:57:12.000Z</published>
<updated>2025-02-07T10:50:40.650Z</updated>
<content type="html"><![CDATA[<p># </p><p>或许你在做性能优化的时候有听说过——尽量避免使用<code>delete</code>,但是为什么呢?</p><p>这涉及到 v8 引擎的几个概念:快属性(fast properties)、隐藏类(hidden Classes)、内联缓存(IC)等。讲述 v8 引擎的文章很多,因此本文会简单进行介绍,然后以此说明我们在开发过程中的注意事项。</p><h2 id="JavaScript-引擎的优化"><a href="#JavaScript-引擎的优化" class="headerlink" title="JavaScript 引擎的优化"></a>JavaScript 引擎的优化</h2><p>我们都知道,JavaScript 是动态语言,JavaScript 引擎在运行 JavaScript 代码时,会解析源代码并将其转换为抽象语法树 (AST)。基于该 AST,解释器可以开始执行其工作并生成字节码。</p><p>解释器可以快速生成字节码,但字节码的执行速度相对较慢。为了提高运行速度,字节码可以连同性能分析数据一起发送给优化编译器。优化编译器根据所掌握的性能分析数据做出某些假设,然后生成高度优化的机器码。</p><p>优化器需要更长的时间来生成代码,但它提供了更好的运行时性能。</p><p>V8 开始在解释器中运行字节码。在某个时刻,引擎确定代码是热门代码,并启动优化器前端,负责集成分析数据并构建代码的基本机器表示。然后,将其发送到不同线程上的优化器以进行进一步改进,如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/pipeline-detail-javascriptcore.svg" alt=""></p><p>当生成的代码准备就绪时,引擎开始运行此优化编译器代码而不是字节码。在 JavaScriptCore 中,所有优化编译器都与主 JavaScript 执行完全并发运行。</p><p>主线程会触发另一个线程上的编译作业,然后编译器使用复杂的锁定方案从主线程访问分析数据。</p><h3 id="热函数优化"><a href="#热函数优化" class="headerlink" title="热函数优化"></a>热函数优化</h3><p>现在我们知道 JavaScript 引擎生成优化的机器代码需要很长时间,除此之外,优化的机器代码还需要更多的内存。</p><p>一般来说,字节码往往比机器码(尤其是优化的机器码)紧凑得多。另一方面,字节码需要解释器才能运行,而优化的代码可以由处理器直接执行。因此,为了使用解释器等快速生成代码和使用优化编译器生成快速代码之间进行权衡,JavaScript 引擎具有不同的优化层级:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/interpreter-optimizing-compiler-jsc.svg" alt=""></p><p>由于添加更多优化层级会产生额外的复杂性和开销,同时优化生成代码的内存使用量也会增大。因此,JavaScript 引擎只会尝试优化热函数。</p><p><strong>那么,我们在写代码的时候,就可以考虑抽离出高频执行的通用代码,让其成为热函数得到优化。</strong>比如碰撞检测、</p><h3 id="JavaScript-引擎的去优化"><a href="#JavaScript-引擎的去优化" class="headerlink" title="JavaScript 引擎的去优化"></a>JavaScript 引擎的去优化</h3><p>现在我们知道,JavaScript 引擎在执行过程中会优化热函数,热函数即在代码执行过程中会高频执行的函数代码。</p><p>JavaScript 引擎在运行时,会结合运行代码时收集的分析数据,基于推测得到的数据类型,编译器可以生成高度优化的代码。但由于变量类型可能会在代码运行过程中发生变化,因此如果某一时刻某个假设被证明是错误的,优化编译器就会取消优化并返回到解释器。</p><p>我们可以看到 JavaScript 引擎运行 JavaScript 代码的过程如下图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/js-engine-pipeline.svg" alt=""></p><p>取消优化的过程我们称之为去优化。显然,去优化会使得代码执行效率大大降低,因此我们要尽量避免该过程。</p><p>那么,什么情况下会发生去优化呢?</p><h2 id="JavaScript-引擎的形状"><a href="#JavaScript-引擎的形状" class="headerlink" title="JavaScript 引擎的形状"></a>JavaScript 引擎的形状</h2><p>前面提到,JavaScript 引擎会基于运行代码时收集的分析数据,推测代码运行的大致类型然后进行优化。对于 JavaScript 引擎来说,相类似的对象模型,可以理解为是相类似的形状,这便是形状的概念。</p><p>所有 JavaScript 引擎都使用形状作为一种优化,当然,形状的说法还有很多种:隐藏类(Hidden Class)、<code>Maps</code>、类型、结构。</p><h3 id="JavaScript-中的隐藏类"><a href="#JavaScript-中的隐藏类" class="headerlink" title="JavaScript 中的隐藏类"></a>JavaScript 中的隐藏类</h3><p>我们先看看一个基本 JavaScript 对象在内存中包括了什么:</p><ul><li>HiddenClasses: 隐藏类保存了对象的形状信息,以及从属性名称到属性索引的映射</li><li>Elements: 数组索引属性被保存在独立的元素存储中</li><li>Properties: 命名属性被保存在属性存储中</li></ul><p>每个 JavaScript 对象都有相应的隐藏类来记录该对象的形状信息。隐藏类保存了与对象相关的元信息,包括对象上的属性数和对象原型的引用。在基于 JavaScript 中,通常不可能预先知道类。因此,在这种情况下,V8 的隐藏类是动态创建的,并随着对象的变化而动态更新。</p><p>关于隐藏类的内容已有许多文章介绍过(文末提供了参考链接),这里不再赘述,我们需要知道的是:</p><ul><li>具有相同结构的对象(相同顺序相同属性)具有相同的隐藏类</li><li>默认情况下,每添加新的命名属性都会导致一个新的隐藏类被创建</li><li>添加数组索引属性不会创建新的隐藏类</li></ul><h2 id="JavaScript-内联缓存"><a href="#JavaScript-内联缓存" class="headerlink" title="JavaScript 内联缓存"></a>JavaScript 内联缓存</h2><p>JavaScript 引擎不是将原型链接存储在实例本身上,而是将其存储在形状上。无论有多少个对象,只要它们具有相同的形状,我们只需存储一次形状和属性信息。隐藏类充当了对象形状的标识符,因此是 V8 优化编译器和内联缓存非常重要的组成部分。</p><p>内联缓存是让 JavaScript 快速运行的关键因素,JavaScript 引擎使用内联缓存来记住在何处查找对象属性的信息,以减少昂贵的查找次数。比如优化编译器可以直接内联属性访问,如果它可以通过隐藏类来确保对象结构是兼容的。</p><p>为了加快原型的后续加载速度,V8 设置了内联缓存,包括了四个字段:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/ic-validitycell.svg" alt=""></p><p>在首次运行此代码期间预热内联缓存时,V8 会记住在原型中找到该属性的偏移量、找到该属性的原型、实例的形状,以及从实例形状链接到的直接原型的当前<code>ValidityCell</code>的链接。</p><p>下次内联缓存命中时,引擎必须检查实例的形状和<code>ValidityCell</code>。如果它仍然有效,引擎可以直接访问<code>Offset</code>,<code>Prototype</code>跳过额外的查找。</p><p>当原型改变时,会分配一个新的形状,而之前的<code>ValidityCell</code>会失效,所以内联缓存在下次执行时会失效,导致性能下降:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/validitycell-invalid.svg" alt=""></p><p>因此,我们在开发代码中要尽量避免改变<code>Object.prototype</code>,因为它会使引擎在此之前为原型加载设置的任何内联缓存失效。</p><p>这就回到我们文章最开始提到的:尽量避免使用<code>delete</code>,因为删除属性会修改<code>Object.prototype</code>,因此所有内联缓存都会再次失效,引擎必须再次从头开始。</p><h3 id="JavaScript-的快属性"><a href="#JavaScript-的快属性" class="headerlink" title="JavaScript 的快属性"></a>JavaScript 的快属性</h3><p>在前端开发中曾经流行着一句话: JavaScript 里一切皆对象。而我们在写的 JavaScript 代码中,到处都是对对象属性的实现、修改和使用。</p><p>在 JavaScript 引擎中有三种不同的命名属性类型:对象内属性、快属性和慢属性(字典)。</p><ul><li>对象内属性(in-object properties): 对象属性直接保存在对象本身上,它们在 V8 可用的属性中是最快的,因为它们不需要间接层就可以访问</li><li>快属性: 将保存在线性属性存储中的属性定义为“快”,只需通过属性存储中的索引即可访问快属性。要从属性名称获取属性存储中的实际位置,可查看隐藏类上的描述符数组</li><li>慢属性: 带慢属性的对象内部会有独立的词典作为属性存储,属性元信息保存在内部独立的属性字典中,不再通过隐藏类共享元信息。</li></ul><p>如果从对象中添加和删除大量属性,则可能会产生大量时间和内存开销来维护描述符数组和隐藏类,此时则会 JavaScript 引擎会将其降级为慢属性。简单来说:慢属性允许高效的属性删除和添加,但访问速度比其它两种类型慢。</p><p>现在基于这些知识,我们找到了一个实用的 JavaScript 编码技巧,可以帮助提高性能:</p><ul><li>始终以相同的方式初始化你的对象,这样它们最终就不会有不同的形状</li><li>不要乱动原型</li><li>不要弄乱数组元素的属性,以便可以有效地存储和操作它们</li></ul><p>也就是说,我们尽量在最开始就初始化好对象的所有属性,并且在代码运行过程中尽量避免新增、修改和删除属性。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// bad</span></span><br><span class="line"><span class="keyword">const</span> <span class="built_in">object</span> = {};</span><br><span class="line"><span class="built_in">object</span>.<span class="property">x</span> = <span class="number">5</span>;</span><br><span class="line"><span class="built_in">object</span>.<span class="property">y</span> = <span class="number">6</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// good</span></span><br><span class="line"><span class="keyword">const</span> <span class="built_in">object</span> = {</span><br><span class="line"> <span class="attr">x</span>: <span class="number">5</span>,</span><br><span class="line"> <span class="attr">y</span>: <span class="number">6</span>,</span><br><span class="line">};</span><br></pre></td></tr></table></figure><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://mathiasbynens.be/notes/shapes-ics">JavaScript engine fundamentals: Shapes and Inline Caches</a></li><li><a href="https://v8.js.cn/blog/fast-properties/">V8 中的快属性</a></li><li><a href="https://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html">Explaining JavaScript VMs in JavaScript - Inline Caches</a></li><li><a href="https://v8.dev/blog/slack-tracking">Slack tracking in V8</a></li><li><a href="https://v8.dev/docs/hidden-classes">Maps (Hidden Classes) in V8</a></li><li><a href="https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html">What’s up with monomorphism?</a></li></ul><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>有时候我们以为用不到的东西就<code>delete</code>,但或许直接删除带来的麻烦会更多,或许我们在最开始,就应该认真对待这些事情、做好规划的呢~</p>]]></content>
<summary type="html">
<p># </p>
<p>或许你在做性能优化的时候有听说过——尽量避免使用<code>delete</code>,但是为什么呢?</p>
<p>这涉及到 v8 引擎的几个概念:快属性(fast properties)、隐藏类(hidden Classes)、内联缓存(IC)等。讲
</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/2025/01/02/front-end-performance-binary-attribute/"/>
<id>https://godbasin.github.io/2025/01/02/front-end-performance-binary-attribute/</id>
<published>2025-01-02T13:34:23.000Z</published>
<updated>2025-01-02T13:34:16.651Z</updated>
<content type="html"><![CDATA[<p>今天也是来介绍一种性能优化的具体方式,使用二进制存储特定数据,来降低内存占用、后台存储和传输成本。</p><h2 id="二进制数据设计"><a href="#二进制数据设计" class="headerlink" title="二进制数据设计"></a>二进制数据设计</h2><p>当我们需要描述某种数据的许多状态时,可以考虑使用二进制的方式优化。</p><p>简单来说,就是使用二进制数字<code>1</code>和<code>0</code>来表示单个状态,然后使用二进制数字来表示多种状态的组合,比如<code>10001001</code>可以表示 8 种状态。同时还可以将二进制转换为十进制来减少存储成本,比如<code>10001001</code>可转换成<code>137</code>。</p><h3 id="上报数据转换"><a href="#上报数据转换" class="headerlink" title="上报数据转换"></a>上报数据转换</h3><p>常用的场景可以考虑数据上报,比如当用户在编辑文档/打开文档的时候,需要收集一些数据来观测性能情况比如:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">IDocumentInfo</span> {</span><br><span class="line"> <span class="attr">isHugeDocument</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isReadonly</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasCharts</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasFormatting</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasImages</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasVideos</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasRadios</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasPivotTable</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasTableStyle</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasFreezePanels</span>: <span class="built_in">boolean</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们需要根据文档的具体情况,结合大盘的文档性能数据来判断加载速度是否与某些文档特性相关,假设一次上报数据为:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> reportData {</span><br><span class="line"> <span class="attr">timecost</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="attr">docInfo</span>: <span class="title class_">IDocumentInfo</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当希望收集的数据多了之后,我们每次都会携带十分大的数据内容。这时候可以考虑使用二进制的方式来进行上报,比如:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> docInfo = {</span><br><span class="line"> <span class="attr">isHugeDocument</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">isReadonly</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasCharts</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasFormatting</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasImages</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasVideos</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasRadios</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasPivotTable</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasTableStyle</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasFreezePanels</span>: <span class="literal">true</span>,</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>可以表示为 10 个二进制位,即:<code>1110100011</code>,那么转为十进制则是<code>931</code>,我们上报为<code>931</code>即可。</p><p>通过这样的方式,原本一个 JSON 字符串:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">"{\"isHugeDocument\":true,\"isReadonly\":true,\"hasCharts\":true,\"hasFormatting\":false,\"hasImages\":true,\"hasVideos\":false,\"hasRadios\":false,\"hasPivotTable\":false,\"hasTableStyle\":true,\"hasFreezePanels\":true}"</span></span><br></pre></td></tr></table></figure><p>只需要使用<code>931</code>来表示,可以极大地节省传输和存储成本。</p><h3 id="单元格数据状态"><a href="#单元格数据状态" class="headerlink" title="单元格数据状态"></a>单元格数据状态</h3><p>除了上报数据以外,多状态数据同样适应。使用表格为例,一个单元格的样式可能包括:加粗、下划线、删除线、斜体、字号、字体颜色、字体样式、背景色等。那么,我们需要这样来描述一个单元格数据:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">ICell</span> {</span><br><span class="line"> <span class="attr">isBold</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isItalic</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isUnderline</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isStrikeThrough</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">font</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">fontSize</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果我们对一个单元格进行样式编辑,假设只设置了加粗、下划线,按理说最简单的数据变更应该只有<code>isBold</code>和<code>isUnderline</code>两个新数据,那么这次变更我们可以认为是<code>10100000</code>,转换成十进制则是<code>160</code>。</p><p>如果说我们还需要再细致些,直接将单元格的最终状态进行存储,同样可以使用进制的方式进行,比如<code>boolean</code>类型的可以直接使用二进制表示最终状态,假设前面四个布尔值的加粗、下划线、删除线、斜体可以压缩为<code>1010</code>来表示一个加粗、无下划线、有删除线、无斜体样式的单元格。</p><p>那么,我们在存储单元格数据的时候,一些小的成本节约遇上百万千万单元格数据时,则可能会产生想象不到的优化效果。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>今天介绍的只是一个内存优化的思路,但依然还是一句话,性能优化往往是时间换空间、或是空间换时间,本文的例子中显然是时间换空间,毕竟我们需要对数据进行转换,这个过程需要消耗时间是不可避免的。</p><p>除了二进制转换成十进制以外,这样的思路可以拓展到许多地方,比如 16 进制/ 32 进制,甚至简单的字符串拼接等。本质上都是使用约定的方式来存储数据内容,比如 pb、json 便都是一种约定的数据结构。</p><p>还是那句,没有适用于所有方案的最优解,但总有更适合某个场景的解决方案。</p>]]></content>
<summary type="html">
<p>今天也是来介绍一种性能优化的具体方式,使用二进制存储特定数据,来降低内存占用、后台存储和传输成本。</p>
<h2 id="二进制数据设计"><a href="#二进制数据设计" class="headerlink" title="二进制数据设计"></a>二进制数据设计<
</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/2024/12/27/front-end-performance-code-detail/"/>
<id>https://godbasin.github.io/2024/12/27/front-end-performance-code-detail/</id>
<published>2024-12-27T15:25:12.000Z</published>
<updated>2024-12-27T15:25:46.536Z</updated>
<content type="html"><![CDATA[<p>大多数情况下,前端很少遇到性能瓶颈。但如果在大型前端项目、数据量百万千万的场景下,有时候一些毫不起眼的代码习惯也可能会带来性能问题。</p><p>今天来简单介绍几种,大家在写代码的时候也可以注意。</p><h2 id="代码细节与性能"><a href="#代码细节与性能" class="headerlink" title="代码细节与性能"></a>代码细节与性能</h2><h3 id="减少函数拆解"><a href="#减少函数拆解" class="headerlink" title="减少函数拆解"></a>减少函数拆解</h3><p>很多时候,为了提高代码复用率以及提升代码可读性,我们习惯地将一些相同逻辑的代码进行抽离,比如下述的代码:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 检查两个范围是否有相交</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">checkTwoDimensionCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> startA: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> endA: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> startB: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> endB: <span class="built_in">number</span></span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> !(startA > endB || endA < startB);</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="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">checkTwoColRangesCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> colRangeA: [<span class="built_in">number</span>, <span class="built_in">number</span>],</span></span><br><span class="line"><span class="params"> colRangeB: [<span class="built_in">number</span>, <span class="built_in">number</span>]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">const</span> [startColA, endColA] = colRangeA;</span><br><span class="line"> <span class="keyword">const</span> [startColB, endColB] = colRangeB;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">checkTwoDimensionCross</span>(startColA, endColA, startColB, endColB);</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="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">checkTwoRowRangeCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> areaA: { rowStart: <span class="built_in">number</span>; rowEnd: <span class="built_in">number</span> },</span></span><br><span class="line"><span class="params"> areaB: { rowStart: <span class="built_in">number</span>; rowEnd: <span class="built_in">number</span> }</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">checkTwoDimensionCross</span>(</span><br><span class="line"> areaA.<span class="property">rowStart</span>,</span><br><span class="line"> areaA.<span class="property">rowEnd</span>,</span><br><span class="line"> areaB.<span class="property">rowStart</span>,</span><br><span class="line"> areaB.<span class="property">rowEnd</span></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在该代码中,由于行范围和列范围的类型不一致,但为了逻辑判断一致性和方便管理,我们抽离了<code>checkTwoDimensionCross</code>方法,用于判断两个一维的范围是否相交。</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><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="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">export</span> <span class="keyword">function</span> <span class="title function_">checkTwoRowRangeCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> areaA: IRowRange,</span></span><br><span class="line"><span class="params"> areaB: IRowRange</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> !(areaA.<span class="property">rowStart</span> > areaB.<span class="property">rowEnd</span> || areaA.<span class="property">rowEnd</span> < areaB.<span class="property">rowStart</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"> * 检查两个列范围是否有相交</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">checkTwoColRangesCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> colRangeA: IColRange,</span></span><br><span class="line"><span class="params"> colRangeB: IColRange</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">const</span> [startColA, endColA] = colRangeA;</span><br><span class="line"> <span class="keyword">const</span> [startColB, endColB] = colRangeB;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> !(startColA > endColB || endColA < startColB);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="if-else-或许性能更优"><a href="#if-else-或许性能更优" class="headerlink" title="if else 或许性能更优"></a>if else 或许性能更优</h3><p>有时候我们为了偷懒,喜欢使用语法糖来缩减代码的编写,比如说判断两个字符串数组是否内容一致:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 判断两个字符串数组是否内容一致</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isStringArrayTheSame</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> stringArrayA.<span class="title function_">sort</span>().<span class="title function_">join</span>(<span class="string">","</span>) === stringArrayB.<span class="title function_">sort</span>().<span class="title function_">join</span>(<span class="string">","</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但同样的,假设这个方法被调用十万百万次,性能问题可能就会变得是否明显,不管是<code>sort</code>还是数组拼接成字符串都会有一定开销。这种情况下我们可以这么写:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="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">function</span> <span class="title function_">isStringArrayTheSame</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="comment">// 数量不一致,肯定不同</span></span><br><span class="line"> <span class="keyword">if</span> (stringArrayA.<span class="property">length</span> !== stringArrayB.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 相同数量时,A 的每一个都应该存在 B 中,才完全一致</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> <span class="keyword">type</span> <span class="keyword">of</span> stringArrayA) {</span><br><span class="line"> <span class="keyword">if</span> (!stringArrayB.<span class="title function_">includes</span>(<span class="keyword">type</span>)) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面这种偷懒写法也是:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// bad</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">mergeStringArray</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">string</span>[] {</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Array</span>.<span class="title function_">from</span>(<span class="keyword">new</span> <span class="title class_">Set</span>(stringArrayA.<span class="title function_">concat</span>(stringArrayB)));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// good</span></span><br><span class="line"><span class="comment">// 使用场景为单数组内的字符串不会重复</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">mergeStringArray</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">string</span>[] {</span><br><span class="line"> <span class="keyword">const</span> newStringArray = [].<span class="title function_">concat</span>(stringArrayA);</span><br><span class="line"> stringArrayB.<span class="title function_">forEach</span>(<span class="function">(<span class="params"><span class="keyword">type</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!newStringArray.<span class="title function_">includes</span>(<span class="keyword">type</span>)) newStringArray.<span class="title function_">push</span>(<span class="keyword">type</span>);</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> newStringArray;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="低性能消耗代码判断提前"><a href="#低性能消耗代码判断提前" class="headerlink" title="低性能消耗代码判断提前"></a>低性能消耗代码判断提前</h3><p><code>if...else</code>写法也有很多注意事项,最简单的莫过于尽量使执行代码提前<code>return</code>。假设我们现在有这样的代码:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params">arrayA: <span class="built_in">string</span>[], arrayB: <span class="built_in">string</span>[]</span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="title function_">costTimeFunction</span>(arrayA, arrayB) || <span class="title function_">noCostTimeFunction</span>(arrayA, arrayB)) {</span><br><span class="line"> <span class="title function_">testCodeA</span>();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="title function_">testCodeB</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样写看起来没什么问题,但假设已知<code>costTimeFunction</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params">arrayA: <span class="built_in">string</span>[], arrayB: <span class="built_in">string</span>[]</span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="title function_">noCostTimeFunction</span>(arrayA, arrayB) || <span class="title function_">costTimeFunction</span>(arrayA, arrayB)) {</span><br><span class="line"> <span class="title function_">testCodeA</span>();</span><br><span class="line"> <span class="comment">// 提前 retrun 可以简化代码复杂度</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="title function_">testCodeB</span>();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>虽然这些都是很细节的事情,有时候写代码甚至注意不到,但如果养成了思考代码性能的习惯,就可以写出更高效执行的代码。</p><p>实际上,除了简单的代码习惯以外,更多时候我们的性能问题也往往出现在不合理的代码执行流程里,这种就跟项目关系紧密,不在这里介绍啦。</p>]]></content>
<summary type="html">
<p>大多数情况下,前端很少遇到性能瓶颈。但如果在大型前端项目、数据量百万千万的场景下,有时候一些毫不起眼的代码习惯也可能会带来性能问题。</p>
<p>今天来简单介绍几种,大家在写代码的时候也可以注意。</p>
<h2 id="代码细节与性能"><a href="#代码细节与性
</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/2024/11/06/front-end-performance-flyweight-pattern/"/>
<id>https://godbasin.github.io/2024/11/06/front-end-performance-flyweight-pattern/</id>
<published>2024-11-06T12:51:10.000Z</published>
<updated>2024-11-06T12:51:54.122Z</updated>
<content type="html"><![CDATA[<p>之前讲到性能优化,大多数介绍的都是耗时上的一些优化,比如页面打开更快、用户交互响应更快等。不过,在最开始的<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>一文中有说过,前端性能优化可以从两个角度来衡量:时间和空间,今天介绍的享元模式则用于空间下内存占用的优化。</p><h2 id="享元模式"><a href="#享元模式" class="headerlink" title="享元模式"></a>享元模式</h2><p>享元是一种设计模式,通过共享对象的方式来减少创建对象的数量,从而降低程序运行过程中占用的内存,提升页面性能。</p><p>一般来说,假如我们的页面中存在大量相类似的内容时,这些内容在代码中被设计为对象的方式,则我们可以通过享元的方式,将一样的对象进行共享,从而减少页面中的总对象数,降低内存占用。</p><p>本文就以最近比较熟练的表格为例子来介绍吧。</p><h3 id="享元对象设计"><a href="#享元对象设计" class="headerlink" title="享元对象设计"></a>享元对象设计</h3><p>假设我们现在有 1W 个单元格的表格,每个单元格内都有不一样的文字信息,但是单元格格式基本上都是一样的,这里包括字体色、背景色、对齐方式等等格式。</p><p>我们可以将这样一个格式<code>CellStyle</code>作为享元对象,它的属性可能包括:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="keyword">enum</span> <span class="title class_">HorizontalAlign</span> {</span><br><span class="line"> left = <span class="string">"left"</span>,</span><br><span class="line"> center = <span class="string">"center"</span>,</span><br><span class="line"> right = <span class="string">"right"</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">enum</span> <span class="title class_">VerticalAlign</span> {</span><br><span class="line"> top = <span class="string">"top"</span>,</span><br><span class="line"> middle = <span class="string">"middle"</span>,</span><br><span class="line"> bottom = <span class="string">"bottom"</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ICellStyleProps</span> {</span><br><span class="line"> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>;</span><br><span class="line"> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么一个<code>CellStyle</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CellStyle</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">constructor</span>(<span class="params">{</span></span><br><span class="line"><span class="params"> textColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> backgroundColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> horizontalAlign: HorizontalAlign,</span></span><br><span class="line"><span class="params"> verticalAlign: VerticalAlign,</span></span><br><span class="line"><span class="params"> }</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">textColor</span> = textColor || <span class="string">"#000"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">backgroundColor</span> = backgroundColor || <span class="string">"#fff"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span> = horizontalAlign || <span class="title class_">HorizontalAlign</span>.<span class="property">left</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">verticalAlign</span> = verticalAlign || <span class="title class_">VerticalAlign</span>.<span class="property">middle</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">textColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">textColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">backgroundColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">backgroundColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">horizontalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">verticalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">verticalAlign</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>一个单元格可能是这样的:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Cell</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">row</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">column</span>: <span class="built_in">number</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">cellStyle</span>: <span class="title class_">CellStyle</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">text</span>: <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> row: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> column: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> text: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> cellStyle?: CellStyle</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">row</span> = row;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">column</span> = column;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">text</span> = text;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">cellStyle</span> = cellStyle || <span class="keyword">new</span> <span class="title class_">CellSyle</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由于单元格跟行列信息<code>row/column</code>挂钩,因此是无法完全享元的,那么 1W 个单元格的表格里可能有 1W 个<code>Cell</code>对象。同样的,每个<code>Cell</code>对象都有一个<code>CellStyle</code>对象,因此该表格同样会有 1W 个<code>CellStyle</code>对象。</p><p>但是<code>CellStyle</code>对象仅跟单元格的格式相关,我们可以考虑将<code>CellStyle</code>对象进行享元。</p><h3 id="享元工厂"><a href="#享元工厂" class="headerlink" title="享元工厂"></a>享元工厂</h3><p>我们可以给<code>CellStyle</code>定义一个享元的<code>key</code>,当然这个<code>key</code>可以代表完全相同格式的<code>CellStyle</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CellStyle</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="attr">pools</span>: {</span><br><span class="line"> [<span class="attr">key</span>: <span class="built_in">string</span>]: <span class="title class_">CellStyle</span>;</span><br><span class="line"> } = {};</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="title function_">generateKey</span>(<span class="params"></span></span><br><span class="line"><span class="params"> textColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> backgroundColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> horizontalAlign: HorizontalAlign,</span></span><br><span class="line"><span class="params"> verticalAlign: VerticalAlign</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<span class="subst">${textColor}</span>-<span class="subst">${backgroundColor}</span>-<span class="subst">${horizontalAlign}</span>-<span class="subst">${verticalAlign}</span>`</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="title function_">newInstance</span>(<span class="attr">props</span>: <span class="title class_">ICellStyleProps</span>): <span class="title class_">CellStyle</span> {</span><br><span class="line"> <span class="keyword">const</span> { textColor, backgroundColor, horizontalAlign, verticalAlign } =</span><br><span class="line"> props;</span><br><span class="line"> <span class="keyword">const</span> key = <span class="title class_">CellStyle</span>.<span class="title function_">generateKey</span>(</span><br><span class="line"> textColor,</span><br><span class="line"> backgroundColor,</span><br><span class="line"> horizontalAlign,</span><br><span class="line"> verticalAlign</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果已有相同格式的对象,则使用享元对象</span></span><br><span class="line"> <span class="keyword">const</span> cellStyle = <span class="title class_">CellStyle</span>.<span class="property">pools</span>[key];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果没有,则创建享元对象,并添加到享元对象池子</span></span><br><span class="line"> <span class="keyword">return</span> cellStyle</span><br><span class="line"> ? cellStyle</span><br><span class="line"> : (<span class="title class_">CellStyle</span>.<span class="property">pools</span>[key] = <span class="keyword">new</span> <span class="title class_">CellStyle</span>(</span><br><span class="line"> textColor,</span><br><span class="line"> backgroundColor,</span><br><span class="line"> horizontalAlign,</span><br><span class="line"> verticalAlign</span><br><span class="line"> ));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> textColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> backgroundColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> horizontalAlign: HorizontalAlign,</span></span><br><span class="line"><span class="params"> verticalAlign: VerticalAlign</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">textColor</span> = textColor || <span class="string">"#000"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">backgroundColor</span> = backgroundColor || <span class="string">"#fff"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span> = horizontalAlign || <span class="title class_">HorizontalAlign</span>.<span class="property">left</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">verticalAlign</span> = verticalAlign || <span class="title class_">VerticalAlign</span>.<span class="property">middle</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">textColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">textColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">backgroundColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">backgroundColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">horizontalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">verticalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">verticalAlign</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>相比于<code>new CellStyle()</code>的方式创建对象,我们可以使用<code>CellStyle.newInstance()</code>的方式来创建:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cellStyle = <span class="title class_">CellStyle</span>.<span class="title function_">newInstance</span>({</span><br><span class="line"> <span class="attr">textColor</span>: <span class="string">"#000"</span>,</span><br><span class="line"> <span class="attr">backgroundColor</span>: <span class="string">"#fff"</span>,</span><br><span class="line"> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>.<span class="property">center</span>,</span><br><span class="line"> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>.<span class="property">top</span>,</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>到这里,如果我们表格中 1W 个单元格的样式都是一样的,那么我们页面中只会存在一个<code>CellStyle</code>对象,大幅度减少了对象的创建和维护,降低了页面的内存占用,从而提升页面的性能。</p><p>当然,享元并不是万能的。前端性能优化的尽头往往是时间换空间、空间换时间,享元便是一个时间换空间的典型例子,我们通过<code>key</code>去获取对象时会比直接访问要多一步。</p><p>除此之外,享元对象需要十分注意对象的修改。由于对象是享元的,如果在使用的时候直接修改了,会导致许多引用到的地方都被修改。因此,一般建议通过<code>CellStyle.newInstance()</code>新建<code>CellStyle</code>对象的方式来进行修改。</p><p>最后其实还留了个小问题给小伙伴们想一想,<code>CellStyle</code>中颜色是字符串的形式提供的,但前端颜色表示可能不只有一个,比如<code>#000</code>、<code>#000000</code>和<code>rgb(0,0,0)</code>都是代表一种颜色,那么在<code>key</code>中如何让它们保持一致呢?</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>之前将性能优化都倾向于介绍比较大的解决方案,后面如果有时间的话,也会考虑一个个小的优化点拎出来简单讲讲,比如享元就是其中一个。</p><p>有时候一点小小的问题里,也会有许多学问可以学习的!</p>]]></content>
<summary type="html">
<p>之前讲到性能优化,大多数介绍的都是耗时上的一些优化,比如页面打开更快、用户交互响应更快等。不过,在最开始的<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">
</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/2024/10/27/tech-debt/"/>
<id>https://godbasin.github.io/2024/10/27/tech-debt/</id>
<published>2024-10-27T13:12:22.000Z</published>
<updated>2024-10-27T13:13:53.883Z</updated>
<content type="html"><![CDATA[<p>身为一名程序员,我们经常会调侃自己每天的工作就是在屎山上拉屎。这里的屎山还有一个更好的名称,叫做技术债务。</p><h2 id="技术债务是怎么产生的"><a href="#技术债务是怎么产生的" class="headerlink" title="技术债务是怎么产生的"></a>技术债务是怎么产生的</h2><p>我参加过许多不同的项目,而基本上每个项目都会存在或多或少的历史债务。实际上,愿意给到资源去解决历史债务的团队,更是少之又少。</p><p>业务功能的快速迭代,往往意味着缺少长期的规划和设计,架构演化和迭代更是无从谈起。我们总会吐槽自己在屎山上拉屎,但许多项目的实际情况是生命周期很短,业务或许还未稳定就已经被淘汰。</p><p>这样的情况下,技术债务的产生是必然的,我们写的每一行代码都可能成为历史债务。因为我们的业务在不停地快速试错和迭代,项目也在不断地变更和发展。技术方案没有唯一解,最合适的技术方案想必也会跟随着项目产生变化。</p><p>即使我们很幸运地遇到了生命周期较长的项目,也不可避免地在业务快速发展的时候忙于堆叠功能。直到现有架构的维护成本过高,影响到后续功能迭代时,才会想起来需要进行技术变更。</p><p>当架构设计需要进行变更、新技术引入时,过往的方案设计很容易就成为了历史债务,这是一个必然过程。</p><p>虽然技术债务躲不了,那当技术发生变更的时候,我们可以通过一些方法使其产生更少的债务。</p><h2 id="技术方案预研"><a href="#技术方案预研" class="headerlink" title="技术方案预研"></a>技术方案预研</h2><p>这些年的前端技术变更十分迅猛,很多人会在项目中引入新技术,来获得更高的开发效率或是更好的性能。除此之外,我还见过许多技术的引入,单纯是为了跟上新的技术栈,或是拿业务代码来做试验。</p><p>最糟糕的还是为了汇报引入的技术,我在工作中已经见过无数为了晋级答辩强行造的轮子或是引入新技术。在答辩通过之后,他们往往会继续去攻陷下一个“技术亮点”,留下来大堆大堆的技术债务。当然,这也不能怪留下债务的人,很多时候他们也只是想办法在规则范围内获得更多的利益。</p><p>那么,当我们遇到需要引入新的架构设计或是技术的时候,可以进行较深入的技术方案预研,来尽量避免引入更多的技术债务。确保了技术方案的最优化,可以避免开发过程遇到问题需要推翻重做,也可以提前评估预期的效果和引入的技术债务如何解决等问题。</p><p>技术预研的话,可以从几个方面考虑起:</p><ol><li>项目现状/痛点分析。</li><li>业界方案调研/方案选型。</li><li>架构可拓展性。</li></ol><h3 id="项目现状-痛点分析"><a href="#项目现状-痛点分析" class="headerlink" title="项目现状/痛点分析"></a>项目现状/痛点分析</h3><p>在进行技术方案调研的时候,我们需要首先结合自身项目的背景、存在的痛点、现状问题进行分析,只有找到项目的问题在哪里,才可以更准确、彻底地去解决这些问题。</p><p>很多人在拿到一个不熟悉的项目时,第一反应经常是重构它。说实话,要把重构这项工作做好,往往是吃力不讨好。对此,个人的建议是可以先开发和维护一段时间,确切知道项目的实际情况后,结合业务未来的规划,再来考虑是否需要进行重构工作,亦或是局部优化。如果说业务已经稳定,且不会再用什么新的功能了,除非是 bug 多到无法解决,否则就不需要投入过多的精力在这里。</p><p>项目痛点是什么?直白来说,就是我们每天在吐槽的事情,还有我们认为没有意思的事情,比如:糟糕的历史代码、枯燥又重复的开发工作、历史债务导致的开发效率低下等问题。相比于每天都在吐槽,我们可以动动手花点时间把问题解决,这样每天就可以有更多摸鱼的时间了(不是。</p><p>更多情况下,是项目现有的设计,无法支撑后续功能的快速迭代了。比如说,项目代码已经很庞大了,模块之间调用关系过于凌乱、模块状态的数量过多导致修改和监听复杂等等。那么,这种情况下,我们则需要引入新的技术或是架构设计到项目中,比如使用依赖注入来管理模块间的依赖关系,使用状态管理工具来维护应用各模块以及全局的的状态。</p><h3 id="业界方案调研-方案选型"><a href="#业界方案调研-方案选型" class="headerlink" title="业界方案调研/方案选型"></a>业界方案调研/方案选型</h3><p>具体到前端页面开发来说,前端状态管理工具也有很多,常见的比如各框架自带的 vuex、redux,以及比较热门的 mobx 等,具体的引入可以结合项目自身的情况比如使用的框架、项目技术栈等来进行选型。</p><p>除此之外,有时候我们会遇到一些现有开源工具无法直接在项目中的问题,这种时候我们往往需要“造轮子”,即参考业界成熟的技术方案,结合项目实际情况来调整落地。比如说依赖注入的方案,著名的开源项目中有 Angular 和 VsCode 都实现了依赖注入的框架,但并没有抽离出来直接可用的工具,我们可以通过研究它们的相关代码,分析其中的思路以及实现方式,然后在自己项目中使用。</p><h3 id="架构可拓展性"><a href="#架构可拓展性" class="headerlink" title="架构可拓展性"></a>架构可拓展性</h3><p>个人认为引入新架构或是新技术时,需要考虑两个很重要的点:</p><ol><li>新架构/技术是否能支持业务的未来规划。</li><li>此次引入是否彻底,是否会留下新的技术债务。</li></ol><p>不同的项目或是同一个项目的不同时期,关注的技术点都会不一样。在项目初期,关注重点往往是快速试错与功能迭代;在项目稳定期,项目的维护成本则会逐渐受到重视。</p><p>我们在引入新的技术或架构的时候,还需要考虑项目后续的发展规划。比如说我们在给项目引入依赖注入时,假设我们知道项目后续需要支持以应用中内嵌应用的功能,则可以考虑以 SDK 为维度来进行依赖注入,避免后续在同一个应用中存在两个 SDK 时,依赖注入管理混乱。</p><p>而技术变更会引入的技术债务问题,则还需要在方案设计的时候进行详细评估。举个例子,架构设计的改造,往往产生极大的工作量,面对这样的工作量是否有有效的解决方案,比如引入自动化流程进行、新增人力支援等。如果该问题无法有很好的解决方案,那么引入新技术必定会带来更多的技术债务,这种情况下就需要仔细衡量这个事情值不值得了。</p><p>至于技术方案调研相关,之前有写过一篇更详细的文章:<a href="https://godbasin.github.io/2022/12/03/research-and-design-process/">《技术方案的调研和设计过程》</a>,感兴趣的小伙伴也可以看看。</p><p>实际上,项目复盘也可以很好地解决剩余技术债务的问题,同时还能避免相同的错误再犯,之前<a href="https://godbasin.github.io/2023/03/21/why-project-reviews-are-important/">《为什么项目复盘很重要》</a>一文中也有介绍。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>日子怎么过都是过,过得好过得坏也是一天天过,但稍微对自己有点要求和期待,日子可能会一天天变好呢~</p>]]></content>
<summary type="html">
<p>身为一名程序员,我们经常会调侃自己每天的工作就是在屎山上拉屎。这里的屎山还有一个更好的名称,叫做技术债务。</p>
<h2 id="技术债务是怎么产生的"><a href="#技术债务是怎么产生的" class="headerlink" title="技术债务是怎么产生的"
</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/2024/10/15/front-end-performance-jank-monitor/"/>
<id>https://godbasin.github.io/2024/10/15/front-end-performance-jank-monitor/</id>
<published>2024-10-15T14:17:25.000Z</published>
<updated>2024-10-15T14:20:06.065Z</updated>
<content type="html"><![CDATA[<p>我们在上一篇<a href="https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/">《前端性能优化–卡顿心跳检测》</a>一文中介绍过基于<code>requestAnimationFrame</code>的卡顿的检测方案实现,这一篇文章我们将会介绍基于该心跳检测方案,要怎么实现链路追踪,来找到产生卡顿的地方。</p><h2 id="卡顿监控实现"><a href="#卡顿监控实现" class="headerlink" title="卡顿监控实现"></a>卡顿监控实现</h2><p>上一篇我们提到的心跳检测,实现的功能很简单,就是卡顿和心跳事件、开始和停止,那么我们卡顿监控使用的时候也比较简单:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="comment">// 心跳 SDK</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">heartBeatMonitor</span>: <span class="title class_">HeartbeatMonitor</span>;</span><br><span class="line"></span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 初始化并绑定事件</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span> = <span class="keyword">new</span> <span class="title class_">HeartbeatMonitor</span>();</span><br><span class="line"> <span class="comment">// PS:此处 addEventListener 为伪代码,可自行实现一个事件转发器</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span>.<span class="title function_">addEventListener</span>(<span class="string">"jank"</span>, <span class="variable language_">this</span>.<span class="property">handleJank</span>);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span>.<span class="title function_">addEventListener</span>(<span class="string">"heartbeat"</span>, <span class="variable language_">this</span>.<span class="property">handleHeartBeat</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 可以初始化的时候就启动</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span>.<span class="title function_">start</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"> * 处理卡顿</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleJank</span>(<span class="params"></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 处理心跳</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleHeartBeat</span>(<span class="params"></span>) {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这时候可以检测到卡顿了,接下来便是在卡顿发生的时候找到问题并上报了。前面<a href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/">《前端性能优化–卡顿的监控和定位》</a>中有大致介绍堆栈的方法,这里我们来介绍下具体要怎么实现吧~</p><h3 id="堆栈追踪卡顿"><a href="#堆栈追踪卡顿" class="headerlink" title="堆栈追踪卡顿"></a>堆栈追踪卡顿</h3><p>同样的,假设我们通过打堆栈的方式来追踪,堆栈信息包括:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">IJankLog</span> {</span><br><span class="line"> <span class="attr">module</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">action</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">logTime</span>: <span class="built_in">number</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么,我们的卡顿检测还需要对外提供<code>log</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="comment">// 卡顿链路堆栈</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">jankLogStack</span>: <span class="title class_">IJankLog</span>[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="title function_">log</span>(<span class="params">logPosition: { <span class="variable language_">module</span>: <span class="built_in">string</span>; action: <span class="built_in">string</span> }</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</span>.<span class="title function_">push</span>({</span><br><span class="line"> ...logPosition,</span><br><span class="line"> <span class="attr">logTime</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleHeartBeat</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 心跳的时候,可以将堆栈清空,因为正常心跳发生意味着没有卡顿,此时堆栈内信息可以移除</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</span> = [];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 清空后,添加心跳信息,方便计算耗时</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</span>.<span class="title function_">push</span>({</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...其他内容省略</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当卡顿发生时,我们可以根据堆栈计算出卡顿产生的位置:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">jankLogStack</span>: <span class="title class_">IJankLog</span>[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleJank</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> jankPosition = <span class="variable language_">this</span>.<span class="title function_">calculateJankPosition</span>();</span><br><span class="line"> <span class="comment">// 拿到卡顿位置后,可以进行上报</span></span><br><span class="line"> <span class="comment">// PS: reportJank 为伪代码,可以根据项目情况自行实现</span></span><br><span class="line"> <span class="title function_">reportJank</span>(jankPosition);</span><br><span class="line"> <span class="comment">// 打印异常</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">"产生了卡顿,位置信息为:"</span>, jankPosition);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 上报结束后,则需要清空堆栈,继续监听</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</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><br></pre></td></tr></table></figure><p>下面我们来详细看一下,要怎么计算出卡顿产生的位置。</p><h3 id="卡顿位置定位"><a href="#卡顿位置定位" class="headerlink" title="卡顿位置定位"></a>卡顿位置定位</h3><p>我们在代码中,使用<code>log</code>方法来打关键链路日志,那么我们拿到的堆栈信息大概会长这样:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">jankLogStack = [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"拉取数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime1,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"加载数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime2,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Feature 模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"处理数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime3,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"渲染数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime4,</span><br><span class="line"> },</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>当卡顿发生的时候,我们可以将堆栈取出来计算最大耗时的位置:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">jankLogStack</span>: <span class="title class_">IJankLog</span>[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">calculateJankPosition</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 记录产生卡顿的位置</span></span><br><span class="line"> <span class="keyword">let</span> jankPosition;</span><br><span class="line"> <span class="comment">// 记录最大耗时</span></span><br><span class="line"> <span class="keyword">let</span> maxCostTime = <span class="number">0</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">for</span> (<span class="keyword">let</span> i = <span class="number">1</span>; i < <span class="variable language_">this</span>.<span class="property">jankLogStack</span>.<span class="property">length</span>; i++) {</span><br><span class="line"> <span class="comment">// 上个位置</span></span><br><span class="line"> <span class="keyword">const</span> previousPosition = <span class="variable language_">this</span>.<span class="property">jankLogStack</span>[i - <span class="number">1</span>];</span><br><span class="line"> <span class="comment">// 当前位置</span></span><br><span class="line"> <span class="keyword">const</span> currentPosition = <span class="variable language_">this</span>.<span class="property">jankLogStack</span>[i];</span><br><span class="line"> <span class="comment">// 链路耗时</span></span><br><span class="line"> <span class="keyword">const</span> costTime = currentPosition.<span class="property">logTime</span> - previousPosition.<span class="property">logTime</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 可以将链路打出来,方便定位</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(</span><br><span class="line"> <span class="string">`<span class="subst">${previousPosition.<span class="variable language_">module</span>}</span>-<span class="subst">${previousPosition.action}</span> -> <span class="subst">${currentPosition.<span class="variable language_">module</span>}</span>-<span class="subst">${currentPosition.action}</span>, 耗时 <span class="subst">${costTime}</span> ms`</span></span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 找出最大耗时和最大位置</span></span><br><span class="line"> <span class="keyword">if</span> (costTime > maxCostTime) {</span><br><span class="line"> maxCostTime = costTime;</span><br><span class="line"> jankPosition = {</span><br><span class="line"> ...currentPosition,</span><br><span class="line"> costTime,</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> jankPosition;</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><br></pre></td></tr></table></figure><p>这样我们就可以计算出产生卡顿时,代码执行的整个链路(需要使用<code>log</code>记录堆栈),同时可找到耗时最大的位置并进行上报。当然,有时候卡顿产生并不只是一个地方,这里也可以调整为将执行超过一定时间的链路全部进行上报。</p><p>现在,我们可以拿到产生卡顿的有效位置,当然前提是需要使用<code>log</code>方法记录关键的链路信息。为了方便,我们可以将其做成一个装饰器来使用。</p><h3 id="jankTrace-装饰器"><a href="#jankTrace-装饰器" class="headerlink" title="@jankTrace 装饰器"></a>@jankTrace 装饰器</h3><p>该装饰器功能很简单,就是调用<code>JankMonitor.log</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 装饰器,可用于装饰类中的成员方法和箭头函数</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">JankTrace</span>: <span class="title class_">MethodDecorator</span> | <span class="title class_">PropertyDecorator</span> = <span class="function">(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function"> target,</span></span></span><br><span class="line"><span class="params"><span class="function"> propertyKey,</span></span></span><br><span class="line"><span class="params"><span class="function"> descriptor</span></span></span><br><span class="line"><span class="params"><span class="function"></span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> className = target.<span class="property">constructor</span>.<span class="property">name</span>;</span><br><span class="line"> <span class="keyword">const</span> methodName = propertyKey.<span class="title function_">toString</span>();</span><br><span class="line"> <span class="keyword">const</span> isProperty = !descriptor;</span><br><span class="line"> <span class="keyword">const</span> originalMethod = isProperty</span><br><span class="line"> ? (target <span class="keyword">as</span> <span class="built_in">any</span>)[propertyKey]</span><br><span class="line"> : descriptor.<span class="property">value</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> originalMethod !== <span class="string">"function"</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">"JankTrace decorator can only be applied to methods"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> newFunction = <span class="keyword">function</span> (<span class="params">...args: <span class="built_in">any</span>[]</span>) {</span><br><span class="line"> <span class="comment">// 打印卡顿堆栈</span></span><br><span class="line"> jankMonitor.<span class="title function_">log</span>({</span><br><span class="line"> <span class="attr">moduleValue</span>: className,</span><br><span class="line"> <span class="attr">actionValue</span>: methodName,</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">const</span> syncResult = originalMethod.<span class="title function_">apply</span>(<span class="variable language_">this</span>, args);</span><br><span class="line"> <span class="keyword">return</span> syncResult;</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (isProperty) {</span><br><span class="line"> (target <span class="keyword">as</span> <span class="built_in">any</span>)[propertyKey] = newFunction;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> descriptor!.<span class="property">value</span> = newFunction <span class="keyword">as</span> <span class="built_in">any</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>至此,我们可以直接在一些类方法上去添加装饰器,来实现自动跟踪卡顿链路:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">DataLoader</span> {</span><br><span class="line"> <span class="meta">@JankLog</span></span><br><span class="line"> <span class="title function_">getData</span>(<span class="params"></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="meta">@JankLog</span></span><br><span class="line"> <span class="title function_">loadData</span>(<span class="params"></span>) {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文简单介绍了卡顿检测的一个实现思路,实际上在项目中还有很多其他问题需要考虑,比如需要设置堆栈上限、状态管理等等。</p><p>技术方案在项目中落地时,都需要因地制宜做些调整,来更好地适配自己的项目滴~</p>]]></content>
<summary type="html">
<p>我们在上一篇<a href="https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/">《前端性能优化–卡顿心跳检测》</a>一文中介绍过基于<code>requ
</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>前端性能优化--JavaScript 数组解构</title>
<link href="https://godbasin.github.io/2024/09/03/front-end-performance-array-performance/"/>
<id>https://godbasin.github.io/2024/09/03/front-end-performance-array-performance/</id>
<published>2024-09-03T13:37:22.000Z</published>
<updated>2024-09-03T13:37:38.763Z</updated>
<content type="html"><![CDATA[<p>之前在给大家介绍性能相关内容的时候,经常说要给大家讲一些更具体的案例,而不是大的解决方案。</p><p>这不,最近刚查到一个数组的性能问题,来给大家分享一下~</p><h2 id="数组解构的性能问题"><a href="#数组解构的性能问题" class="headerlink" title="数组解构的性能问题"></a>数组解构的性能问题</h2><p>ES6 的出现,让前端开发小伙伴们着实高效工作了一番,我们常常会使用解构的方式拼接数组,比如:</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="comment">// 浅拷贝新数组</span></span><br><span class="line"><span class="keyword">const</span> newArray = [...originArray];</span><br><span class="line"><span class="comment">// 拼接数组</span></span><br><span class="line"><span class="keyword">const</span> newArray = [...array1, ...array2];</span><br></pre></td></tr></table></figure><p>这样的代码经常会出现,毕竟对于大多数场景来说,很少会因为这样简单的数组结构导致性能问题。</p><p>但实际上,如果在数据量大的场景下使用,数组解构不仅有性能问题,还可能导致 JavaScript 爆栈等问题。</p><h3 id="两者差异"><a href="#两者差异" class="headerlink" title="两者差异"></a>两者差异</h3><p>使用<code>concat</code>和<code>...</code>拓展符的最大区别是:<code>...</code>使用对象需为可迭代对象,当使用<code>...</code>解构数组时,它会尝试迭代数组的每个元素,并将它们展开到一个新数组中。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line">b = <span class="string">"test"</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a.<span class="title function_">concat</span>(b)); <span class="comment">// [1, 2, 3, 4, 'test']</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>([...a, ...b]);</span><br><span class="line"><span class="comment">// [1, 2, 3, 4, 't', 'e', 's', 't']</span></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">a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line">b = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a.<span class="title function_">concat</span>(b)); <span class="comment">// [1, 2, 3, 4, 100]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>([...a, ...b]); <span class="comment">// TypeError: b is not iterable</span></span><br></pre></td></tr></table></figure><p>除此之外,<code>concat()</code>用于在数组末尾添加元素,而<code>...</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></pre></td><td class="code"><pre><span class="line">a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line">b = [<span class="number">5</span>, <span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>];</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a.<span class="title function_">concat</span>(b)); <span class="comment">// [1, 2, 3, 4, 5, 6, 7, 8]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>([...b, ...a]); <span class="comment">// [5, 6, 7, 8, 1, 2, 3, 4]</span></span><br></pre></td></tr></table></figure><h3 id="性能差异"><a href="#性能差异" class="headerlink" title="性能差异"></a>性能差异</h3><p>由于<code>concat()</code>方法的使用对象为数组,基于次可以进行很多优化,而<code>...</code>拓展符在使用时还需要进行检测和迭代,性能上会是<code>concat()</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">let</span> big = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e5</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> i, x;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"concat-big"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [].<span class="title function_">concat</span>(big);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"concat-big"</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"spread-big"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [...big];</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"spread-big"</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> a = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> b = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> c = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> d = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"concat-many"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>].<span class="title function_">concat</span>(a, b, c, d);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"concat-many"</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"spread-many"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, ...a, ...b, ...c, ...d];</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"spread-many"</span>);</span><br></pre></td></tr></table></figure><p>上述代码在我的 Chrome 浏览器上输出结果为:</p><figure class="highlight plaintext"><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">concat-big: 35.491943359375 ms</span><br><span class="line">spread-big: 268.485107421875 ms</span><br><span class="line">concat-many: 0.55615234375 ms</span><br><span class="line">spread-many: 6.807861328125 ms</span><br></pre></td></tr></table></figure><p>也有网友提供的测试数据为:</p><table><thead><tr><th>浏览器</th><th><code>[...a, ...b]</code></th><th><code>a.concat(b)</code></th></tr></thead><tbody><tr><td>Chrome 113</td><td>350 毫秒</td><td>30 毫秒</td></tr><tr><td>Firefox 113</td><td>400 毫秒</td><td>63 毫秒</td></tr><tr><td>Safari 16.4</td><td>92 毫秒</td><td>71 毫秒</td></tr></tbody></table><p>以及不同数据量的对比数据:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-2.jpg" alt=""></p><p>更多数据可参考<a href="https://jonlinnell.co.uk/articles/spread-operator-performance">How slow is the Spread operator in JavaScript?</a>:</p><h3 id="Array-push-爆栈"><a href="#Array-push-爆栈" class="headerlink" title="Array.push()爆栈"></a><code>Array.push()</code>爆栈</h3><p>当数组数据量很大时,使用<code>Array.push(...array)</code>的组合还可能出现 JavaScript 堆栈溢出的问题,比如这段代码:</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">const</span> someArray = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">600000</span>).<span class="title function_">fill</span>(<span class="number">1</span>);</span><br><span class="line"><span class="keyword">const</span> newArray = [];</span><br><span class="line"><span class="keyword">let</span> tempArray = [];</span><br><span class="line"></span><br><span class="line">newArray.<span class="title function_">push</span>(...someArray); <span class="comment">// JS error</span></span><br><span class="line">tempArray = newArray.<span class="title function_">concat</span>(someArray); <span class="comment">// can work</span></span><br></pre></td></tr></table></figure><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-1.jpg" alt=""></p><p>这是因为解构会使用<code>apply</code>方法来调用函数,即<code>Array.prototype.push.apply(newArray, someArray)</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">newArray = [...someArray];</span><br></pre></td></tr></table></figure><h3 id="内存占用"><a href="#内存占用" class="headerlink" title="内存占用"></a>内存占用</h3><p>之前在项目中遇到的特殊场景,两份代码的差异只有数组的创建方式不一致:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-5.jpg" alt=""></p><p>使用<code>newArray = [].concat(oldArray)</code>的时候,内存占用并没有涨,因此不会触发浏览器的 GC:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-3.png" alt=""></p><p>但使用<code>newArray = [...oldArray]</code>解构数组的时候,内存占用会持续增长,因此也会带来频繁的 GC,导致函数执行耗时直线上涨:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-4.jpg" alt=""></p><p>可惜的是,对于这个困惑的程度只达到了把该问题修复,但依然无法能建立有效的 demo 复现该问题(因为项目代码过于复杂无法简单提取出可复现 demo)。</p><p>个人认为或许跟前面提到的 JavaScript 堆栈问题有些关系,但目前还没有更多的时间去往底层继续研究,只能在这里小小地记录一下。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://stackoverflow.com/questions/48865710/spread-operator-vs-array-concat">spread operator vs array.concat()</a></li><li><a href="https://jonlinnell.co.uk/articles/spread-operator-performance">How slow is the Spread operator in JavaScript?</a></li></ul><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>今天给大家介绍了一个比较具体的性能问题,可惜没有更完整深入地往下捞到 v8 的实现和内存回收相关的内容,以后有机会有时间的话,可以再翻出来看看叭~</p><p>希望有一天能有机会和能力解答今天的疑惑~</p>]]></content>
<summary type="html">
<p>之前在给大家介绍性能相关内容的时候,经常说要给大家讲一些更具体的案例,而不是大的解决方案。</p>
<p>这不,最近刚查到一个数组的性能问题,来给大家分享一下~</p>
<h2 id="数组解构的性能问题"><a href="#数组解构的性能问题" class="heade
</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/2024/08/05/front-end-performance-task-schedule/"/>
<id>https://godbasin.github.io/2024/08/05/front-end-performance-task-schedule/</id>
<published>2024-08-05T15:39:12.000Z</published>
<updated>2024-08-05T15:39:40.121Z</updated>
<content type="html"><![CDATA[<p>对于一个前端应用,最理想的性能便是任何用户的交互都不会被阻塞、且能及时得到响应。</p><p>显然,当我们应用程序里需要处理一些大任务计算的时候,这个理想状态是难以达到的。不过,努力去接近也是我们可以尽量去做好的。</p><h1 id="任务调度与性能"><a href="#任务调度与性能" class="headerlink" title="任务调度与性能"></a>任务调度与性能</h1><p>任务调度的出现,基本上是为了更合理地使用和分配资源。在前端应用中,用户的交互则是最高优先级需要响应的,用户操作是否能及时响应,往往是我们衡量一个前端应用是否性能好的重要标准。</p><h2 id="浏览器的“一帧”"><a href="#浏览器的“一帧”" class="headerlink" title="浏览器的“一帧”"></a>浏览器的“一帧”</h2><p>前面在<a href="https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/">《前端性能优化–卡顿心跳检测》</a>一文中,我们提到说使用<code>requestAnimationFrame</code>来检测是否产生了卡顿。除此之外,如果你也处理过简单的异步任务管理(闲时执行等),或许你还用过<code>requestIdleCallback</code>。</p><p>其实,<code>requestAnimationFrame</code>和<code>requestIdleCallback</code>都会在浏览器的每一帧中被执行到。我们来看<a href="https://aerotwist.com/blog/the-anatomy-of-a-frame/">下图</a>:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/anatomy-of-a-frame.svg" alt=""></p><p>每次浏览器渲染的过程顺序为:</p><ol><li>用户事件。</li><li>一个宏任务。</li><li>队列中全部微任务。</li><li><code>requestAnimationFrame</code>。</li><li>浏览器重排/重绘。</li><li><code>requestIdleCallback</code>。</li></ol><p>我们常用的事件监听的顺序则是<a href="https://medium.com/@paul_irish/requestanimationframe-scheduling-for-nerds-9c57f7438ef4">如图</a>:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/life-of-a-frame.jpg" alt=""></p><h2 id="任务切片"><a href="#任务切片" class="headerlink" title="任务切片"></a>任务切片</h2><p>之前在<a href="https://godbasin.github.io/2024/04/03/front-end-performance-long-task/">《让你的长任务在 50 毫秒内结束》</a>一文中说过:RAIL 的目标是在 100 毫秒内完成由用户输入发起的转换,让用户感觉互动是瞬时完成的。</p><p>为确保在 100 毫秒内获得可见响应,RAIL 的准则是在 50 毫秒内处理用户输入事件,这也是为什么我们使用<code>requestIdleCallback</code>处理空闲回调任务时,<code>timeRemaining()</code>有一个 50ms 的上限时间。</p><p>好的任务调度可以让页面不会产生卡顿,这个前提是每个被调度的任务的颗粒度足够细,也可理解为单个任务需要满足下述两个条件之一:</p><ol><li>在 50ms 内执行完成。</li><li>支持暂停以及继续执行。</li></ol><p>对于希望尽可能达到理想状态的系统来说,要让所以可拆卸的任务满足上述条件,都才是最难实现的部分。</p><h2 id="切片后任务执行"><a href="#切片后任务执行" class="headerlink" title="切片后任务执行"></a>切片后任务执行</h2><p>只要任务可控制在 50ms 内结束或者中断再恢复,那么我们就可以很简单地利用浏览器的每一帧渲染过程,来实现“不会产生卡顿”的任务管理。</p><p>最简单的,我们可以设置每一次执行的耗时上限,当每个任务执行完之后,检测一下本次执行耗时,超过 50ms 则通过定时器或是<code>requestAnimationFrame</code>、<code>requestIdleCallback</code>等方法,将剩余任务放到下一次渲染前后处理。</p><p>比如之前<a href="https://godbasin.github.io/2023/09/16/render-engine-calculate-split/">《复杂渲染引擎架构与设计–分片计算》</a>一文中提到的,简单的<code>setTimeout</code>便能使任务执行不阻塞用户操作:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">AsyncCalculateManager</span> {</span><br><span class="line"> <span class="comment">// 每次执行任务的耗时</span></span><br><span class="line"> <span class="keyword">static</span> timeForEveryTask = <span class="number">50</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 跑下一次任务</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">runNext</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">timer</span>) <span class="built_in">clearTimeout</span>(<span class="variable language_">this</span>.<span class="property">timer</span>);</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">timer</span> = <span class="built_in">setTimeout</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// 一个任务跑 50 ms</span></span><br><span class="line"> <span class="keyword">const</span> calculateRange = <span class="variable language_">this</span>.<span class="property">calculateRunner</span>.<span class="title function_">calculateNextTask</span>(</span><br><span class="line"> <span class="title class_">AsyncCalculateManager</span>.<span class="property">timeForEveryTask</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="variable language_">this</span>.<span class="title function_">runNext</span>();</span><br><span class="line"> }, <span class="number">10</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>除此之外,<code>requestAnimationFrame</code>适合处理影响页面渲染(比如操作 DOM)的任务,而<code>requestIdleCallback</code>可以处理与页面渲染无关的一些计算任务。</p><p>当然,常见的任务调度还需要支持这些能力:</p><ul><li>定义任务优先级</li><li>并行/串行/顺序执行任务</li></ul><p>在前端应用中,大家都比较认可和熟知的任务调度便是 React 虚拟 DOM 的计算,我们可以来看看。</p><h2 id="React-虚拟-DOM-与任务调度"><a href="#React-虚拟-DOM-与任务调度" class="headerlink" title="React 虚拟 DOM 与任务调度"></a>React 虚拟 DOM 与任务调度</h2><p>React 中使用协调器(Reconciler)与渲染器(Renderer)来优化页面的渲染性能。</p><p>我们都知道在 React 里,可以使用<code>ReactDOM.render</code>/<code>this.setState</code>/<code>this.forceUpdate</code>/<code>useState</code>等方法来触发状态更新,这些方法共用一套状态更新机制,该更新机制主要由两个步骤组成:</p><ol><li>找出变化的组件,每当有更新发生时,协调器会做如下工作:</li></ol><ul><li>调用组件 render 方法将 JSX 转化为虚拟 DOM</li><li>进行虚拟 DOM Diff 并找出变化的虚拟 DOM</li><li>通知渲染器</li></ul><ol start="2"><li>渲染器接到协调器通知,将变化的组件渲染到页面上。</li></ol><p>在 React15 及以前,协调器创建虚拟 DOM 使用的是递归的方式,该过程是无法中断的。这会导致 UI 渲染被阻塞,造成卡顿。为此,React16 中新增了调度器(Scheduler),调度器能够把可中断的任务切片处理,能够调整优先级,重置并复用任务。</p><p>调度器会根据任务的优先级去分配各自的过期时间,在过期时间之前按照优先级执行任务,可以在不影响用户体验的情况下去进行计算和更新。</p><p>简单来说,最重要的依然是两个步骤:</p><ul><li>时间切片:将更新中的 render 阶段划分一个个的小任务,通常来说这些小任务连续执行的最长时间为 5ms</li><li>限制时间执行任务:每次执行小任务,都会记录耗时,如果超过 5ms 就跳出当前任务,并设置一个宏任务开始下一轮事件循环</li></ul><p>通过这样的方式,React 可在浏览器空闲的时候进行调度并执行任务。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.qinguanghui.com/react/%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6.html">任务调度 Scheduler</a></li></ul><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>任务调度其实很简单,无非就是将所有执行代码尽可能拆分为一个个的切片任务,并在浏览器每帧渲染前后处理一部分任务,从而达到不阻塞用户操作的目的。</p><p>但实际上这件事要做好来又是很困难的,需要将几乎整个应用程序都搭建于这套任务调度之上,并拆成足够小可执行的任务,往往这才是在项目中做好性能的最大难点。</p>]]></content>
<summary type="html">
<p>对于一个前端应用,最理想的性能便是任何用户的交互都不会被阻塞、且能及时得到响应。</p>
<p>显然,当我们应用程序里需要处理一些大任务计算的时候,这个理想状态是难以达到的。不过,努力去接近也是我们可以尽量去做好的。</p>
<h1 id="任务调度与性能"><a href
</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>前端性能优化--R 树的使用</title>
<link href="https://godbasin.github.io/2024/07/17/front-end-performance-r-tree/"/>
<id>https://godbasin.github.io/2024/07/17/front-end-performance-r-tree/</id>
<published>2024-07-17T15:01:23.000Z</published>
<updated>2024-07-17T15:01:46.721Z</updated>
<content type="html"><![CDATA[<p>听说程序员里存在一个鄙视链,而前端则在鄙视链的最底端。这是因为以前大多数的前端工作内容都相对简单(或许现在也是如此),在大多数人的眼中,前端只需要写写 HTML 和 CSS,编写页面样式便完成了。</p><p>如今尽管前端的能力越来越强了,涉及到代码构建、编译等,但依然有十分丰富且成熟的工具可供使用,因此前端被认为是可替代性十分强的职位。在降本增效大时代,“前端已死”等说法也常常会被提出来。</p><p>这些说法很多时候是基于前端开发的工作较简单,但实际上并不是所有的开发工作都这么简单的,前端也会有涉及到算法与数据结构的时候。</p><p>今天我们来看看 R-tree 在前端中的应用。</p><h2 id="树的数据结构"><a href="#树的数据结构" class="headerlink" title="树的数据结构"></a>树的数据结构</h2><p>树在前端开发里其实并不应该很陌生,浏览器渲染页面过程中必不可缺,包括 HTML 代码解析完成后得到的 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。</p><p>基于这样一个渲染过程,我们页面的代码也经常是树的结构进行布局。除此之外,热门前端框架中也少不了 AST 语法树,虚拟 DOM 抽象树等等。</p><h3 id="R-tree"><a href="#R-tree" class="headerlink" title="R-tree"></a>R-tree</h3><p>我们来看一下 <a href="https://zh.wikipedia.org/wiki/R%E6%A0%91">R 树是什么(来自维基百科)</a>:</p><blockquote><p>R 树(R-tree)是用来做空间数据存储的树状数据结构,例如给地理位置,矩形和多边形这类多维数据建立索引。在现实生活中,R 树可以用来存储地图上的空间信息,例如餐馆地址,或者地图上用来构造街道,建筑,湖泊边缘和海岸线的多边形。然后可以用它来回答“查找距离我 2 千米以内的博物馆”,“检索距离我 2 千米以内的所有路段”(然后显示在导航系统中)或者“查找(直线距离)最近的加油站”这类问题。R 树还可以用来加速使用包括大圆距离在内的各种距离度量方式的最邻近搜索。</p></blockquote><p>R 树的核心思想是聚合距离相近的节点,并在树结构的上一层将其表示为这些节点的最小外接矩形,这个最小外接矩形就成为上一层的一个节点。R 树的“R”代表“Rectangle(矩形)”。因为所有节点都在它们的最小外接矩形中,所以跟某个矩形不相交的查询就一定跟这个矩形中的所有节点都不相交。</p><p>一个经典的 R 树结构如下:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/R-tree.svg.png" alt=""></p><p>至于 R 树的算法原理以及复杂度这里就不多介绍了,书上网上都有许多可以供学习的内容参考,我们主要还是介绍算法的应用场景。</p><p>在与图形相关的应用中经常会使用到 R 树,除了上述提到的地图检索以外,图形编辑中也会使用到(检索图形是否发生了碰撞)。</p><p>除此之外,还有在表格场景下,天然适合使用 R 树来管理的数据,主要是范围数据,比如函数依赖的区域范围、条件格式的范围设置、区域权限的范围数据等等。</p><h3 id="Rbush"><a href="#Rbush" class="headerlink" title="Rbush"></a>Rbush</h3><p>前端开发使用 R-tree 的场景大多数是 2D 下,包括上述提到的地图检索、图形碰撞检测、数据可视化、表格区域数据等等。</p><p>虽然我们经常在面试中会问到一些数据结构和算法,甚至有些时候还要求手写出来。但实际上在我们开发的时候,并不需要什么都自己实现一遍。学习算法的目的并不是要完全能自己实现,而是知道在什么场景下使用怎样的算法会更优,因此使用开源稳定的工具也是一种很好的方式。</p><p><a href="https://github.com/mourner/rbush">RBush</a> 是一个高性能 JavaScript 库,用于对点和矩形进行 2D 空间索引。它基于优化的 R 树数据结构,支持批量插入。其使用的算法包括:</p><ul><li>单次插入:非递归 R 树插入,最小化 R<em> 树的重叠分割例程(分割在 JS 中非常有效,而其他 R</em> 树修改,如溢出时重新插入和最小化子树重叠搜索,速度太慢,不值得)</li><li>单一删除:使用深度优先树遍历和空时释放策略进行非递归 R 树删除(下溢节点中的条目不会被重新插入,而是将下溢节点保留在树中,只有当其为空时才被删除,这是查询与删除性能之间的良好折衷)</li><li>批量加载:OMT 算法(Overlap Minimizing Top-down Bulk Loading)结合 Floyd–Rivest 选择算法</li><li>批量插入:STLT 算法(小树-大树)</li><li>搜索:标准非递归 R 树搜索</li></ul><p>我们也可以看到,<a href="https://github.com/mourner/rbush/blob/master/index.js">整个 Rbush 的实现非常简单</a>,甚至实现代码都没有 demo 和测试代码多。</p><p>使用方式很简单,我们来用个实际场景来使用看看。</p><h3 id="表格区域数据"><a href="#表格区域数据" class="headerlink" title="表格区域数据"></a>表格区域数据</h3><p>表格中使用到区域的地方十分多,前面提到了函数引用区域、条件格式区域、区域权限区域,除此之外还有区域样式、图表区域等等。这些区域因为不会覆盖,支持堆叠、交错,我们在管理的时候使用 R 树来维护,性能会更好。</p><p>基于 Rbush 实现,我们需要定义这个 Rbush 结点的数据。假设我们现有的表格区域数据为:</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">interface</span> <span class="title class_">ICellRange</span> {</span><br><span class="line"> <span class="attr">startRowIndex</span>: <span class="built_in">number</span>; <span class="comment">// 起始行位置</span></span><br><span class="line"> <span class="attr">endRowIndex</span>: <span class="built_in">number</span>; <span class="comment">// 结束行位置</span></span><br><span class="line"> <span class="attr">startColumnIndex</span>: <span class="built_in">number</span>; <span class="comment">// 起始列位置</span></span><br><span class="line"> <span class="attr">endColumnIndex</span>: <span class="built_in">number</span>; <span class="comment">// 结束列位置</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么每个区域都有对应要存储的数据(<code>data</code>),那么我们可以这么定义我们的 R 树:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">RBush</span> <span class="keyword">from</span> <span class="string">"rbush"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 树节点的数据格式</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">ITreeNode</span><T> {</span><br><span class="line"> <span class="attr">range</span>: <span class="title class_">ICellRange</span>;</span><br><span class="line"> data?: T;</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> <span class="title class_">RTree</span><T> <span class="keyword">extends</span> <span class="title class_">RBush</span><<span class="title class_">ITreeNode</span><T>> {</span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">toBBox</span>(<span class="params">treeNode: ITreeNode<T></span>) {</span><br><span class="line"> <span class="keyword">const</span> { range } = treeNode;</span><br><span class="line"> <span class="comment">// 将单元格范围,转换为 Rbush 范围</span></span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">minX</span>: range.<span class="property">startColumnIndex</span>,</span><br><span class="line"> <span class="attr">maxX</span>: range.<span class="property">endColumnIndex</span>,</span><br><span class="line"> <span class="attr">minY</span>: range.<span class="property">startRowIndex</span>,</span><br><span class="line"> <span class="attr">maxY</span>: range.<span class="property">endRowIndex</span>,</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 需要自行实现的比较</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">compareMinX</span>(<span class="params">treeNode1: ITreeNode<T>, treeNode2: ITreeNode<T></span>) {</span><br><span class="line"> <span class="keyword">return</span> treeNode1.<span class="property">range</span>.<span class="property">startColumnIndex</span> - treeNode2.<span class="property">range</span>.<span class="property">startColumnIndex</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">compareMinY</span>(<span class="params">treeNode1: ITreeNode<T>, treeNode2: ITreeNode<T></span>) {</span><br><span class="line"> <span class="keyword">return</span> treeNode1.<span class="property">range</span>.<span class="property">startRowIndex</span> - treeNode2.<span class="property">range</span>.<span class="property">startRowIndex</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 转换一下数据范围</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">searchTreeNodes</span>(<span class="attr">cellRange</span>: <span class="title class_">ICellRange</span>): <span class="title class_">ITreeNode</span><T>[] {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">search</span>({</span><br><span class="line"> <span class="attr">minX</span>: cellRange.<span class="property">startColumnIndex</span>,</span><br><span class="line"> <span class="attr">maxX</span>: cellRange.<span class="property">endColumnIndex</span>,</span><br><span class="line"> <span class="attr">minY</span>: cellRange.<span class="property">startRowIndex</span>,</span><br><span class="line"> <span class="attr">maxY</span>: cellRange.<span class="property">endRowIndex</span>,</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么,我们表格的许多数据结构都可以基于这个封装了一层的 RTree 来实现。举个区域权限的例子,我们在表格中设置了两个区域权限,显然堆叠部分会需要两个权限都满足才可以编辑:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/auth-range-tree-1.jpg" alt=""></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><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">RTree</span> } <span class="keyword">from</span> <span class="string">"../r-tree"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 区域权限数据</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">IAuthRangeData</span> {</span><br><span class="line"> <span class="attr">cellRange</span>: <span class="title class_">ICellRange</span>;</span><br><span class="line"> <span class="attr">rangeStatus</span>: <span class="string">"unreadable"</span> | <span class="string">"readonly"</span> | <span class="string">"edit"</span>;</span><br><span class="line"> userIds?: <span class="built_in">string</span>[];</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> <span class="title class_">AuthRangesTree</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">authRangeTree</span>: <span class="title class_">RTree</span><<span class="title class_">IAuthRangeData</span>> = <span class="keyword">new</span> <span class="title class_">RTree</span>(<span class="number">7</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 检索某个用户是否有该区域权限</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">hasRangesAuth</span>(</span><br><span class="line"> <span class="attr">cellRange</span>: <span class="title class_">ICellRange</span>,</span><br><span class="line"> <span class="attr">userId</span>: <span class="built_in">string</span></span><br><span class="line"> ): <span class="title class_">IAuthRangeData</span>[] {</span><br><span class="line"> <span class="keyword">const</span> authRange = <span class="variable language_">this</span>.<span class="property">authRangeTree</span>.<span class="title function_">searchTreeNodes</span>(cellRange);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 若没有设置区域权限,则默认有权限</span></span><br><span class="line"> <span class="keyword">if</span> (!authRange.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 若有设置区域权限,则判断是否全满足</span></span><br><span class="line"> <span class="keyword">return</span> !authRange.<span class="title function_">find</span>(<span class="function">(<span class="params">range</span>) =></span> !range.<span class="property">data</span>.<span class="property">userIds</span>.<span class="title function_">includes</span>(userId));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样,通过使用 R 树来存储数据的方式,我们可以极大地提升页面查询区域权限的性能。毕竟,如果我们只是单纯使用数据的方式去存储,那么每次查询都需要对整个数组遍历并进行碰撞检测,当表格单元格数量达到百万甚至千万时,这个性能问题可不是小事情了。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>前面说过后面会详细介绍一些性能优化的具体例子,本文 R 树的使用便也是其中一个。当然,使用更优的数据结构和算法可以有不少的性能优化,而更多时候我们代码本身编写的问题也经常是导致性能问题的原因,定位并解决这些问题也是零碎但必须解决的事情。</p><p>如果有机会的话,后面看看攒一批代码习惯导致的性能问题,来分享给大家哇。</p>]]></content>
<summary type="html">
<p>听说程序员里存在一个鄙视链,而前端则在鄙视链的最底端。这是因为以前大多数的前端工作内容都相对简单(或许现在也是如此),在大多数人的眼中,前端只需要写写 HTML 和 CSS,编写页面样式便完成了。</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>
</feed>