-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
544 lines (260 loc) · 273 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
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
533
534
535
536
537
538
539
540
541
542
543
544
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>默默默默燃</title>
<subtitle>路漫漫其修远兮,吾将上下而求索</subtitle>
<link href="https://shinichikudo-fe.github.io/atom.xml" rel="self"/>
<link href="https://shinichikudo-fe.github.io/"/>
<updated>2021-03-17T07:53:30.130Z</updated>
<id>https://shinichikudo-fe.github.io/</id>
<author>
<name>张白告丶</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>React 性能优化 | 包括原理、技巧、Demo、工具使用</title>
<link href="https://shinichikudo-fe.github.io/2021/03/17/React/React-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
<id>https://shinichikudo-fe.github.io/2021/03/17/React/React-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/</id>
<published>2021-03-17T02:49:52.000Z</published>
<updated>2021-03-17T07:53:30.130Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://juejin.cn/post/6935584878071119885">原文地址</a></p><p>本文分为三部分,首先介绍 React 的工作流,让读者对 React 组件更新流程有宏观的认识。然后列出笔者总结的一系列优化技巧,并为稍复杂的优化技巧准备了 CodeSandbox 源码,以便读者实操体验。最后分享笔者使用 React Profiler 的一点心得,帮助读者更快定位性能瓶颈。</p><h2 id="React-工作流"><a href="#React-工作流" class="headerlink" title="React 工作流"></a>React 工作流</h2><p>React 是声明式 UI 库,负责将 <code>State</code> 转换为页面结构(虚拟 DOM 结构)后,再转换成真实 DOM 结构,交给浏览器渲染。当 <code>State</code> 发生改变时,React 会先进行调和(<code>Reconciliation</code>)阶段,调和阶段结束后立刻进入提交(<code>Commit</code>)阶段,提交阶段结束后,新 <code>State</code> 对应的页面才被展示出来。</p><p>React 的调和阶段需要做两件事。 <strong>1、计算出目标 State 对应的虚拟 DOM 结构。2、寻找「将虚拟 DOM 结构修改为目标虚拟 DOM 结构」的最优更新方案。</strong> React 按照<strong>深度优先遍历</strong>虚拟 DOM 树的方式,在一个虚拟 DOM 上完成两件事的计算后,再计算下一个虚拟 DOM。</p><p>第一件事主要是<em>调用类组件的 render 方法或函数组件自身</em>。第二件事<em>为 React 内部实现的 <code>Diff</code> 算法</em>,<code>Diff</code> 算法会记录虚拟 DOM 的更新方式(如:Update、Mount、Unmount),为提交阶段做准备。</p><p>React 的提交阶段也需要做两件事。 <strong>1、将调和阶段记录的更新方案应用到 DOM 中。2、调用暴露给开发者的钩子方法,如:componentDidUpdate、useLayoutEffect 等</strong>。 </p><p>提交阶段中这两件事的执行时机与调和阶段不同,在提交阶段 React 会先执行 1,等 1 完成后再执行 2。因此在子组件的 componentDidMount 方法中,可以执行 <code>document.querySelector('.parentClass')</code> ,拿到父组件渲染的 <code>.parentClass</code> DOM 节点,尽管这时候父组件的 <code>componentDidMount</code> 方法还没有被执行。<code>useLayoutEffect</code> 的执行时机与 <code>componentDidMount</code> 相同,可参考<a href="https://codesandbox.io/s/cdm-yu-commit-jieduanzhixingshunxu-fzu1w?file=/src/App.js">线上代码</a>进行验证。</p><p>由于调和阶段的<code>「Diff 过程」</code>和提交阶段的<code>「应用更新方案到 </code>DOM」都属于 React 的内部实现,开发者能提供的优化能力有限,本文仅有一条优化技巧(<a href="https://juejin.cn/post/6935584878071119885#heading-7">列表项使用 key 属性</a>)与它们有关。实际工程中大部分优化方式都集中在<strong>调和阶段的「计算目标虚拟 DOM 结构」过程</strong>,该过程是优化的重点,React 内部的 <code>Fiber</code> 架构和并发模式也是在减少该过程的耗时阻塞。对于提交阶段的<code>「执行钩子函数」</code>过程,开发者应<strong>保证钩子函数中的代码尽量轻量,避免耗时阻塞</strong>,相关的优化技巧参考本文的避免在 <code>didMount、didUpdate</code> 中更新组件 State。</p><blockquote><p>拓展知识</p></blockquote><blockquote><ol><li>建议对 React 生命周期不熟悉的读者结合 React 组件的生命周期图阅读本文。记得勾选该网站上的复选框。</li><li>因为理解事件循环后才知道页面会在什么时候被更新,所以推荐一个<a href="https://www.youtube.com/watch?v=u1kqx6AenYw&t=853s">介绍事件循环的视频</a>。该视频中事件循环的伪代码如下图,非常清晰易懂。<br><a href="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cc1d13c2d24f4c63995f7d8a8a70b0ce~tplv-k3u1fbpfcp-watermark.image">事件循环</a></li></ol></blockquote><h2 id="定义-Render-的过程"><a href="#定义-Render-的过程" class="headerlink" title="定义 Render 的过程"></a>定义 Render 的过程</h2><p>本文为了叙述方便, 将调和阶段中「计算目标虚拟 DOM 结构」过程称为 <code>Render</code> 过程 。触发 React 组件的 <code>Render</code> 过程目前有三种方式,分别为 forceUpdate、State 更新、父组件 <code>Render</code> 触发子组件 <code>Render</code> 过程。</p><h3 id="优化技巧"><a href="#优化技巧" class="headerlink" title="优化技巧"></a>优化技巧</h3><h4 id="PureComponent、React-memo"><a href="#PureComponent、React-memo" class="headerlink" title="PureComponent、React.memo"></a>PureComponent、React.memo</h4><p>在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 Props 都没有修改,也会引起子组件的 Render 过程。从 React 的声明式设计理念来看,如果子组件的 Props 和 State 都没有改变,那么其生成的 DOM 结构和副作用也不应该发生改变。当子组件符合声明式设计理念时,就可以忽略子组件本次的 Render 过程。</p><p><code>PureComponent</code> 和 <code>React.memo</code> 就是应对这种场景的,<code>PureComponent</code> 是对[类组件的 Props 和 State 进行浅比较],<code>React.memo</code> 是对[函数组件的 Props 进行浅比较]。</p><h4 id="shouldComponentUpdate"><a href="#shouldComponentUpdate" class="headerlink" title="shouldComponentUpdate"></a>shouldComponentUpdate</h4><p>在 React 刚开源的那段时期,数据不可变性还没有现在这样流行。当时 <code>Flux</code> 架构就使用的模块变量来维护 State,并在状态更新时直接修改该模块变量的属性值,而不是使用<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax">展开语法</a>生成新的对象引用。例如要往数组中添加一项数据时,当时的代码很可能是 <code>state.push(item)</code>,而不是 <code>const newState = [...state, item]</code>。这点可参考 Dan Abramov 在演讲 Redux 时演示的 <code>Flux</code> 代码。</p><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c3768dc0ce2941c99caaced1ccbee351~tplv-k3u1fbpfcp-watermark.image" alt="shouldCompoentUpdate"></p><p>在此背景下,当时的开发者经常使用 <code>shouldComponentUpdate</code> 来<strong>深比较 Props</strong>,只在 Props 有修改才执行组件的 Render 过程。如今由于数据不可变性和函数组件的流行,这样的优化场景已经不会再出现了。</p><p>接下来介绍另一种可以使用 <code>shouldComponentUpdate</code> 来优化的场景。在项目初始阶段,开发者往往图方便会给子组件传递一个大对象作为 Props,后面子组件想用啥就用啥。当大对象中某个「子组件未使用的属性」发生了更新,子组件也会触发 Render 过程。在这种场景下,<strong>通过实现子组件的 <code>shouldComponentUpdate</code> 方法,仅在「子组件使用的属性」发生改变时才返回 true,便能避免子组件重新 Render</strong>。</p><p>但使用 <code>shouldComponentUpdate</code> 优化第二个场景有两个弊端。</p><ol><li>如果存在很多子孙组件,「找出所有子孙组件使用的属性」就会有很多工作量,也容易因为漏测导致 bug。</li><li>存在潜在的工程隐患。举例来说,假设组件结构如下。</li></ol><pre><code class="jsx"><A data="{data}"> {/* B 组件只使用了 data.a 和 data.b */} <B data="{data}"> {/* C 组件只使用了 data.a */} <C data="{data}"></C> </B></A></code></pre><p>B 组件的 <code>shouldComponentUpdate</code> 中只比较了 <code>data.a</code> 和 <code>data.b</code>,目前是没任何问题的。之后开发者想在 C 组件中使用 <code>data.c</code>,假设项目中 <code>data.a</code> 和 <code>data.c</code> 是一起更新的,所以也没任何问题。但这份代码已经变得脆弱了,如果某次修改导致 <code>data.a</code> 和 <code>data.c</code> 不一起更新了,那么系统就会出问题。而且实际业务中代码往往更复杂,从 B 到 C 可能还有若干中间组件,这时就很难想到是 <code>shouldComponentUpdate</code> 引起的问题了。</p><blockquote><p>拓展知识</p></blockquote><blockquote><ol><li>第二个场景最好的解决方案是使用发布者订阅者模式,只是代码改动要稍多一些,可参考本文的优化技巧「<a href="https://juejin.cn/post/6935584878071119885#heading-10">发布者订阅者跳过中间组件 Render 过程</a>」。</li><li>第二个场景也可以在父子组件间增加中间组件,中间组件负责从父组件中选出子组件关心的属性,再传给子组件。相比于 <code>shouldComponentUpdate</code> 方法,会增加组件层级,但不会有第二个弊端。</li><li>本文中的<a href="https://juejin.cn/post/6935584878071119885#heading-16">跳过回调函数改变触发的 Render 过程</a>也可以用 <code>shouldComponentUpdate</code> 实现,因为回调函数并不参与组件的 Render 过程。</li></ol></blockquote><h4 id="useMemo、useCallback-实现稳定的-Props-值"><a href="#useMemo、useCallback-实现稳定的-Props-值" class="headerlink" title="useMemo、useCallback 实现稳定的 Props 值"></a>useMemo、useCallback 实现稳定的 Props 值</h4><p>如果传给子组件的派生状态或函数,每次都是新的引用,那么 <code>PureComponent</code> 和 <code>React.memo</code> 优化就会失效。所以需要使用 <code>useMemo</code> 和 <code>useCallback</code> 来生成稳定值,并结合 <code>PureComponent</code> 或 <code>React.memo</code> 避免子组件重新 Render。</p><blockquote><p>拓展知识</p></blockquote><blockquote><p><code>useCallback</code> 是「useMemo 的返回值为函数」时的特殊情况,是 React 提供的便捷方式。在 <a href="https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-dom/src/server/ReactPartialRendererHooks.js#L452">React Server Hooks</a> 代码 中,<code>useCallback</code> 就是基于 useMemo 实现的。尽管 React Client Hooks 没有使用同一份代码,但<a href="https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-reconciler/src/ReactFiberHooks.new.js#L1590"> useCallback</a> 的代码逻辑和 <a href="https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-reconciler/src/ReactFiberHooks.new.js#L1613">useMemo</a> 的代码逻辑仍是一样的。</p></blockquote><h4 id="useMemo-减少组件-Render-过程耗时"><a href="#useMemo-减少组件-Render-过程耗时" class="headerlink" title="useMemo 减少组件 Render 过程耗时"></a>useMemo 减少组件 Render 过程耗时</h4><p><code>useMemo</code> 是一种<strong>缓存机制提速</strong>,当它的依赖未发生改变时,就不会触发重新计算。一般用在「计算派生状态的代码」非常耗时的场景中,如:遍历大列表做统计信息。</p><blockquote><p>拓展知识</p></blockquote><blockquote><ol><li>React 官方并不保证 useMemo 一定会进行缓存,所以可能在依赖不改变时,仍然执行重新计算。参考 <a href="https://reactjs.org/docs/hooks-faq.html#how-to-memoize-calculations">How to memoize calculations</a></li><li>缓存优化往往是最简单有效的优化方式,但 useMemo 缓存加速只能缓存最近一次函数执行的结果,如果想缓存更多次函数执行的结果,可使用 <a href="https://www.npmjs.com/package/memoizee">memoizee</a>。</li></ol></blockquote><h4 id="列表项使用-key-属性"><a href="#列表项使用-key-属性" class="headerlink" title="列表项使用 key 属性"></a>列表项使用 key 属性</h4><p>当渲染列表项时,如果不给组件设置不相等的属性 <code>key</code>,就会收到如下报警。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e07825b27f64c9c860838973063c35e~tplv-k3u1fbpfcp-watermark.image" alt="key"></p><p>相信很多开发者已经见过该报警成百上千次了,那 <code>key</code> 属性到底在优化了什么呢?举个 🌰,在不使用 <code>key</code> 时,组件两次 Render 的结果如下。</p><pre><code class="html"><!-- 前一次 Render 结果 --><ul> <li>Duke</li> <li>Villanova</li></ul><!-- 新的 Render 结果 --><ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li></ul></code></pre><p>此时 React 的 <code>Diff</code> 算法会按照 <code><li></code> 出现的先后顺序进行比较,得出结果为需要更新前两个<code><li></code>并创建内容为 <code>Villanova</code> 的li,一共会执行两次 DOM 更新、一次 DOM 创建。</p><p>如果加上 React 的 key 属性,两次 Render 结果如下。</p><pre><code class="html"><!-- 前一次 Render 结果 --><ul> <li key="2015">Duke</li> <li key="2016">Villanova</li></ul><!-- 新的 Render 结果 --><ul> <li key="2014">Connecticut</li> <li key="2015">Duke</li> <li key="2016">Villanova</li></ul></code></pre><p><code>React Diff</code> 算法会把 <code>key</code> 值为 2015 的虚拟 DOM 进行比较,发现 <code>key</code> 为 2015 的虚拟 DOM 没有发生修改,不用更新。同样,<code>key</code> 值为 2016 的虚拟 DOM 也不需要更新。结果就只需要创建 <code>key</code> 值为 2014 的虚拟 DOM。相比于不使用 <code>key</code> 的代码,使用 <code>key</code> 节省了两次 DOM 更新操作。</p><p>如果把例子中的 <code><li></code> 换成自定义组件,并且自定义组件使用了 <code>PureComponent</code> 或 <code>React.memo</code> 优化。那么使用 <code>key</code> 属性就不只节省了 DOM 更新,还避免了组件的 Render 过程。</p><p><a href="https://reactjs.org/docs/lists-and-keys.html#keys">React 官方推荐</a>将每项数据的 ID 作为组件的 <code>key</code>,以达到上述的优化目的。并且不推荐使用每项的索引作为 <code>key</code>,因为传索引作为 <code>key</code> 时,就会退化为不使用 <code>key</code> 时的代码。那么是否在所有列表渲染的场景下,使用 ID 都优于使用索引呢?</p><p>答案是否定的,在常见的分页列表中,第一页和第二页的列表项 ID 都是不同,假设每页展示三条数据,那么切换页面前后组件 Render 结果如下。</p><pre><code class="html"><!-- 第一页的列表项虚拟 DOM --><li key="a">dataA</li><li key="b">dataB</li><li key="c">dataC</li><!-- 切换到第二页后的虚拟 DOM --><li key="d">dataD</li><li key="e">dataE</li><li key="f">dataF</li></code></pre><p>切换到第二页后,由于所有 <code><li></code> 的 <code>key</code> 值不同,所以 Diff 算法会将第一页的所有 DOM 节点标记为删除,然后将第二页的所有 DOM 节点标记为新增。整个更新过程需要三次 DOM 删除、三次 DOM 创建。如果不使用 <code>key</code>,Diff 算法只会将三个 <code><li></code> 节点标记为更新,执行三次 DOM 更新。参考 Demo 没有<a href="https://codesandbox.io/s/meiyoutianjiashanchupaixugongnengdefenyeliebiao-d6zqr?file=/src/App.js">添加、删除、排序功能的分页列表</a>,使用 <code>key</code> 时每次翻页耗时约为 140ms,而不使用 <code>key</code> 仅为 70ms。</p><p>尽管存在以上场景,React 官方仍然推荐使用 <code>ID</code> 作为每项的 <code>key</code> 值。其原因有两:</p><blockquote><p>1.在列表中执行删除、插入、排序列表项的操作时,使用 ID 作为 key 将更高效。而翻页操作往往伴随着 API 请求,DOM 操作耗时远小于 API 请求耗时,是否使用 ID 在该场景下对用户体验影响不大。</p></blockquote><blockquote><p>2.使用 ID 做为 key 可以维护该 ID 对应的列表项组件的 State。举个例子,某表格中每列都有普通态和编辑态两个状态,起初所有列都是普通态,用户点击第一行第一列,使其进入编辑态。然后用户又拖拽第二行,将其移动到表格的第一行。如果开发者使用索引作为 key,那么第一行第一列的状态仍然为编辑态,而用户实际希望编辑的是第二行的数据,在用户看来就是不符合预期的。尽管这个问题可以通过将「是否处于编辑态」存放在数据项的数据中,利用 Props 来解决,但是使用 ID 作为 key 不是更香吗?</p></blockquote><h4 id="批量更新,减少-Render-次数"><a href="#批量更新,减少-Render-次数" class="headerlink" title="批量更新,减少 Render 次数"></a>批量更新,减少 Render 次数</h4><p>我们先回忆一道前几年的 React 面试常考题,React 类组件中 <code>setState</code> 是同步的还是异步的?如果对类组件不熟悉也没关系,可以将 <code>setState</code> 理解为 <code>useState</code> 的第二个返回值。</p><p>答案是:在 React 管理的事件回调和生命周期中,<code>setState</code> 是异步的,而其他时候 <code>setState</code> 都是同步的。这个问题根本原因就是 React 在自己管理的事件回调和生命周期中,对于 <code>setState</code> 是批量更新的,而在其他时候是立即更新的。读者可参考线上示例 <a href="https://codesandbox.io/s/setstate-tongbuhuanshiyibu-1bo16">setState 同步还是异步</a>,并自行验证。</p><p>批量更新 <code>setState</code> 时,多次执行 <code>setState</code> 只会触发一次 Render 过程。相反在立即更新 <code>setState</code> 时,每次 <code>setState</code> 都会触发一次 Render 过程,就存在性能影响。</p><p>假设有如下组件代码,该组件在 <code>getData()</code> 的 API 请求结果返回后,分别更新了两个 State 。线上代码实操参考:<a href="https://codesandbox.io/s/batchupdates-pilianggengxin-qqdsc">batchUpdates 批量更新</a>。</p><pre><code class="js">function NormalComponent(){ const [list,setList] = useState(); const [info,setInfo] = useState(); useEffect(() =>{ ;(async()=>{ const data = await getData(); setList(data.list); setInfo(data.info); })() },[]) return <div>非批量更新组件时Render 次数:{renderOnce('normal')}</div>}</code></pre><p>该组件会在 <code>setList(data.list)</code> 后触发组件的 Render 过程,然后在 <code>setInfo(data.info)</code> 后再次触发 Render 过程,造成性能损失。遇到该问题,开发者有两种实现批量更新的方式来解决该问题:</p><ol><li><p>将多个 <code>State</code> 合并为单个 <code>State</code>。例如使用 <code>const [data, setData] = useState({ list: null, info: null })</code> 替代 <code>list</code> 和 <code>info</code> 两个 <code>State。</code></p></li><li><p>使用 React 官方提供的 <code>unstable_batchedUpdates</code> 方法,将多次 <code>setState</code> 封装到 <code>unstable_batchedUpdates</code> 回调中。修改后代码如下。</p></li></ol><pre><code class="js">function BatchedComponent() { const [list, setList] = useState(null) const [info, setInfo] = useState(null) useEffect(() => { ;(async () => { const data = await getData() unstable_batchedUpdates(() => { setList(data.list) setInfo(data.info) }) })() }, []) return <div>批量更新组件时 Render 次数:{renderOnce("batched")}</div>}</code></pre><blockquote><p>拓展知识</p><ol><li>推荐阅读<a href="https://github.com/facebook/react/issues/11527#issuecomment-360199710">为什么 setState 是异步的</a>?</li><li>为什么面试官不会问“函数组件中的 setState 是同步的还是异步的?”?因为函数组件中生成的函数是通过闭包引用了 state,而不是通过 <code>this.state</code> 的方式引用 state,所以函数组件的处理函数中 state 一定是旧值,不可能是新值。可以说函数组件已经将这个问题屏蔽掉了,所以面试官也就不会问了。可参考线上示例。</li><li>根据官方文档,在 React 并发模式中,将默认以批量更新方式执行 setState。到那时候,也可能就不需要这个优化了。<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/25ccf66ab735427faf9d55556bb83525~tplv-k3u1fbpfcp-watermark.image" alt="并发模式"></li></ol></blockquote><h4 id="按优先级更新,及时响应用户"><a href="#按优先级更新,及时响应用户" class="headerlink" title="按优先级更新,及时响应用户"></a>按优先级更新,及时响应用户</h4><p>优先级更新是批量更新的逆向操作,其思想是:<strong>优先响应用户行为,再完成耗时操作</strong>。</p><p>常见的场景是:页面弹出一个 <code>Modal</code>,当用户点击 <code>Modal</code> 中的确定按钮后,代码将执行两个操作。a) 关闭 <code>Modal</code>。b) 页面处理 <code>Modal</code> 传回的数据并展示给用户。当 b) 操作需要执行 500ms 时,用户会明显感觉到从点击按钮到 <code>Modal</code> 被关闭之间的延迟。</p><p>例子参考:<a href="https://codesandbox.io/s/youxianjigengxinlijixiangyingyonghucaozuo-eb740">CodeSandbox 在线 Demo</a>。在该例子中,用户添加一个整数后,页面要隐藏输入框,并将新添加的整数加入到整数列表,将列表排序后再展示。以下为一般的实现方式,将 <code>slowHandle</code> 函数作为用户点击按钮的回调函数。</p><pre><code class="js">const slowHandle = () => { setShowInput(false) setNumbers([...numbers, +inputValue].sort((a, b) => a - b))}</code></pre><p><code>slowHandle()</code> 执行过程耗时长,用户点击按钮后会明显感觉到页面卡顿。如果让页面优先隐藏输入框,用户便能立刻感知到页面更新,不会有卡顿感。 <strong>实现优先级更新的要点是将耗时任务移动到下一个宏任务中执行,优先响应用户行为</strong>。 例如在该例中,将 <code>setNumbers</code> 移动到 <code>setTimeout</code> 的回调中,用户点击按钮后便能立即看到输入框被隐藏,不会感知到页面卡顿。优化后的代码如下。</p><pre><code class="js">const fastHandle = () =>{ setShowInput(false); setTimeout(()=>{ setNumber([...numbers, +inputValue].sort((a, b) => a - b)) })}</code></pre><h4 id="发布者订阅者跳过中间组件-Render-过程"><a href="#发布者订阅者跳过中间组件-Render-过程" class="headerlink" title="发布者订阅者跳过中间组件 Render 过程"></a>发布者订阅者跳过中间组件 Render 过程</h4><p>React 推荐将公共数据放在所有「需要该状态的组件」的公共祖先上,但将状态放在公共祖先上后,该状态就需要层层向下传递,直到传递给使用该状态的组件为止。</p><p>每次状态的更新都会涉及中间组件的 Render 过程,但中间组件并不关心该状态,它的 Render 过程只负责将该状态再传给子组件。在这种场景下<strong>可以将状态用发布者订阅者模式维护</strong>,只有关心该状态的组件才去订阅该状态,不再需要中间组件传递该状态。当状态更新时,发布者发布数据更新消息,只有订阅者组件才会触发 Render 过程,中间组件不再执行 Render 过程。</p><p>只要是发布者订阅者模式的库,都可以进行该优化。比如:<code>redux、use-global-state、React.createContext</code> 等。例子参考:<a href="https://codesandbox.io/s/fabuzhedingyuezhemoshitiaoguozhongjianzujiande-render-guocheng-nm7nt?file=/src/PubSubCommunicate.js">发布者订阅者模式跳过中间组件的渲染阶段</a>,本示例使用 <code>React.createContext</code> 进行实现。</p><pre><code class="jsx">import { useState, useEffect, createContext, useContext } from "react"const renderCntMap = {}const renderOnce = name => { return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)}// 将需要公共访问的部分移动到 Context 中进行优化// Context.Provider 就是发布者// Context.Consumer 就是消费者const ValueCtx = createContext()const CtxContainer = ({ children }) => { const [cnt, setCnt] = useState(0) useEffect(() => { const timer = window.setInterval(() => { setCnt(v => v + 1) }, 1000) return () => clearInterval(timer) }, [setCnt]) return <ValueCtx.Provider value={cnt}>{children}</ValueCtx.Provider>}function CompA({}) { const cnt = useContext(ValueCtx) // 组件内使用 cnt return <div>组件 CompA Render 次数:{renderOnce("CompA")}</div>}function CompB({}) { const cnt = useContext(ValueCtx) // 组件内使用 cnt return <div>组件 CompB Render 次数:{renderOnce("CompB")}</div>}function CompC({}) { return <div>组件 CompC Render 次数:{renderOnce("CompC")}</div>}export const PubSubCommunicate = () => { return ( <CtxContainer> <div> <h1>优化后场景</h1> <div> 将状态提升至最低公共祖先的上层,用 CtxContainer 将其内容包裹。 </div> <div style={{ marginTop: "20px" }}> 每次 Render 时,只有组件A和组件B会重新 Render 。 </div> <div style={{ marginTop: "40px" }}> 父组件 Render 次数:{renderOnce("parent")} </div> <CompA /> <CompB /> <CompC /> </div> </CtxContainer> )}export default PubSubCommunicate</code></pre><p>从图中可看出,优化后只有使用了公共状态的组件 CompA 和 CompB 发生了更新,减少了父组件和 CompC 组件的 Render 次数。</p><h4 id="useMemo-返回虚拟-DOM-可跳过该组件-Render-过程"><a href="#useMemo-返回虚拟-DOM-可跳过该组件-Render-过程" class="headerlink" title="useMemo 返回虚拟 DOM 可跳过该组件 Render 过程"></a>useMemo 返回虚拟 DOM 可跳过该组件 Render 过程</h4><p>利用 <code>useMemo</code> 可以缓存计算结果的特点,如果 <code>useMemo</code> 返回的是组件的虚拟 DOM,则将在 <code>useMemo</code> 依赖不变时,跳过组件的 Render 阶段。该方式与 <code>React.memo</code> 类似,但与 <code>React.memo</code> 相比有以下优势:</p><ol><li>更方便。<code>React.memo</code> 需要对组件进行一次包装,生成新的组件。而 <code>useMemo</code> 只需在存在性能瓶颈的地方使用,不用修改组件。</li><li>更灵活。<code>useMemo</code> 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值,也可使用 <a href="https://codesandbox.io/s/usememo-tiaoguozujian-render-guocheng-bzz9r">useDeepCompareMemo</a> 对用到的值进行深比较。</li></ol><pre><code class="jsx">import { useEffect, useMemo, useState } from "react"import "./styles.css"const renderCntMap = {}function Comp({ name }) { renderCntMap[name] = (renderCntMap[name] || 0) + 1 return ( <div> 组件「{name}」 Render 次数:{renderCntMap[name]} </div> )}export default function App() { const setCnt = useState(0)[1] useEffect(() => { const timer = window.setInterval(() => { setCnt(v => v + 1) }, 1000) return () => clearInterval(timer) }, [setCnt]) const comp = useMemo(() => { return <Comp name="使用 useMemo 作为 children" /> }, []) return ( <div className="App"> <Comp name="直接作为 children" /> {comp} </div> )}</code></pre><h4 id="debounce、throttle-优化频繁触发的回调"><a href="#debounce、throttle-优化频繁触发的回调" class="headerlink" title="debounce、throttle 优化频繁触发的回调"></a>debounce、throttle 优化频繁触发的回调</h4><p>在搜索组件中,当 input 中内容修改时就触发搜索回调。当组件能很快处理搜索结果时,用户不会感觉到输入延迟。但实际场景中,中后台应用的列表页非常复杂,组件对搜索结果的 Render 会造成页面卡顿,明显影响到用户的输入体验。</p><p>在搜索场景中一般使用 <a href="https://github.com/xnimorz/use-debounce#simple-values-debouncing">useDebounce</a> + useEffect 的方式获取数据。</p><p>例子参考:<a href="https://codesandbox.io/s/debounce-search-4dkn3">debounce-search</a>。</p><pre><code class="jsx">import { useState, useEffect } from "react"import { useDebounce } from "use-debounce"export default function App() { const [text, setText] = useState("Hello") const [debouncedValue] = useDebounce(text, 300) useEffect(() => { // 根据 debouncedValue 进行搜索 }, [debouncedValue]) return ( <div> <input defaultValue={"Hello"} onChange={e => { setText(e.target.value) }} /> <p>Actual value: {text}</p> <p>Debounce value: {debouncedValue}</p> </div> )}</code></pre><p>为什么搜索场景中是使用 <code>debounce</code>,而不是 <code>throttle</code> 呢?</p><p>throttle 是 debounce 的特殊场景,throttle 给 debounce 传了 maxWait 参数,可参考 <a href="https://github.com/xnimorz/use-debounce/blob/master/src/useThrottledCallback.ts#L57">useThrottleCallback</a>。在搜索场景中,只需响应用户最后一次输入,无需响应用户的中间输入值,debounce 更适合使用在该场景中。而 throttle 更适合需要实时响应用户的场景中更适合,如通过拖拽调整尺寸或通过拖拽进行放大缩小(如:window 的 resize 事件)。实时响应用户操作场景中,如果回调耗时小,甚至可以用 <code>requestAnimationFrame</code> 代替 throttle。</p><h4 id="懒加载"><a href="#懒加载" class="headerlink" title="懒加载"></a>懒加载</h4><p>在 SPA 中,懒加载优化一般用于从一个路由跳转到另一个路由。还可用于用户操作后才展示的复杂组件,比如点击按钮后展示的弹窗模块(有时候弹窗就是一个复杂页面 😌)。在这些场景下,结合 Code Split 收益较高。<br>懒加载的实现是通过 <code>Webpack</code> 的动态导入和 <code>React.lazy</code> 方法,</p><p>参考例子 <a href="https://codesandbox.io/s/lazy-loading-bmyd7">lazy-loading</a>。实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。</p><pre><code class="jsx">import { lazy, Suspense, Component } from "react"import "./styles.css"// 对加载失败进行容错处理class ErrorBoundary extends Component { constructor(props) { super(props) this.state = { hasError: false } } static getDerivedStateFromError(error) { return { hasError: true } } render() { if (this.state.hasError) { return <h1>这里处理出错场景</h1> } return this.props.children }}const Comp = lazy(() => { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.5) { reject(new Error("模拟网络出错")) } else { resolve(import("./Component")) } }, 2000) })})export default function App() { return ( <div className="App"> <div style={{ marginBottom: 20 }}> 实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。 </div> <ErrorBoundary> <Suspense fallback="Loading..."> <Comp /> </Suspense> </ErrorBoundary> </div> )}</code></pre><h4 id="懒渲染"><a href="#懒渲染" class="headerlink" title="懒渲染"></a>懒渲染</h4><p>懒渲染指当组件进入或即将进入可视区域时才渲染组件。常见的组件 Modal/Drawer 等,当 visible 属性为 true 时才渲染组件内容,也可以认为是懒渲染的一种实现。</p><p>懒渲染的使用场景有:</p><ol><li>页面中出现多次的组件,且组件渲染费时、或者组件中含有接口请求。如果渲染多个带有请求的组件,由于浏览器限制了同域名下并发请求的数量,就可能会阻塞可见区域内的其他组件中的请求,导致可见区域的内容被延迟展示。</li><li>需用户操作后才展示的组件。这点和懒加载一样,但懒渲染不用动态加载模块,不用考虑加载态和加载失败的兜底处理,实现上更简单。</li></ol><p>懒渲染的实现中判断组件是否出现在可视区域内是通过 <a href="https://www.npmjs.com/package/react-visibility-observer">react-visibility-observer</a> 进行监听。</p><p>例子参考:<a href="https://codesandbox.io/s/lanxuanran-ls65r">懒渲染</a></p><pre><code class="jsx">import { useState, useEffect } from "react"import VisibilityObserver, { useVisibilityObserver,} from "react-visibility-observer"const VisibilityObserverChildren = ({ callback, children }) => { const { isVisible } = useVisibilityObserver() useEffect(() => { callback(isVisible) }, [callback, isVisible]) return <>{children}</>}export const LazyRender = () => { const [isRendered, setIsRendered] = useState(false) if (!isRendered) { return ( <VisibilityObserver rootMargin={"0px 0px 0px 0px"}> <VisibilityObserverChildren callback={isVisible => { if (isVisible) { setIsRendered(true) } }} > <span /> </VisibilityObserverChildren> </VisibilityObserver> ) } console.log("滚动到可视区域才渲染") return <div>我是 LazyRender 组件</div>}export default LazyRender</code></pre><h4 id="虚拟列表"><a href="#虚拟列表" class="headerlink" title="虚拟列表"></a>虚拟列表</h4><p>虚拟列表是懒渲染的一种特殊场景。虚拟列表的组件有 <a href="https://react-window.now.sh/#/examples/list/fixed-size">react-window</a> 和 react-virtualized,它们都是同一个作者开发的。react-window 是 react-virtualized 的轻量版本,其 API 和文档更加友好。所以新项目中推荐使用 react-window,而不是使用 Star 更多的 react-virtualized。</p><p>使用 react-window 很简单,只需要计算每项的高度即可。下面代码中每一项的高度是 35px。</p><p>例子参考:<a href="https://react-window.now.sh/#/examples/list/fixed-size">官方示例</a></p><p>如果每项的高度是变化的,可给 <code>itemSize</code> 参数传一个函数。</p><p>对于这个优化点,笔者遇到一个真实案例。在公司的招聘项目中,通过下拉菜单可查看某个候选人的所有投递记录。平常这个列表也就几十条,但后来用户反馈『下拉菜单点击后要很久才能展示出投递列表』。该问题的原因就是这个候选人在我们系统中有上千条投递,一次性展示上千条投递导致页面卡住了。所以在开发过程中,遇到接口返回的是所有数据时,需提前预防这类 bug,使用虚拟列表优化。。</p><h4 id="跳过回调函数改变触发的-Render-过程"><a href="#跳过回调函数改变触发的-Render-过程" class="headerlink" title="跳过回调函数改变触发的 Render 过程"></a>跳过回调函数改变触发的 Render 过程</h4><p>React 组件的 Props 可以分为两类。a) 一类是在对组件 Render 有影响的属性,如:页面数据、<a href="https://ant.design/components/dropdown/">getPopupContainer</a> 和 renderProps 函数。b) 另一类是组件 Render 后的回调函数,如:onClick、<a href="https://ant.design/components/dropdown/">onVisibleChange</a>。<br>b) 类属性并不参与到组件的 Render 过程,因为可以对 b) 类属性进行优化。当 b)类属性发生改变时,不触发组件的重新 Render ,而是在回调触发时调用最新的回调函数。</p><p>Dan Abramov 在 <a href="https://overreacted.io/a-complete-guide-to-useeffect/#each-render-has-its-own-event-handlers">A Complete Guide to useEffect</a> 文章中认为,每次 Render 都有自己的事件回调是一件很酷的特性。但该特性要求每次回调函数改变就触发组件的重新 Render ,这在性能优化过程中是可以取舍的。</p><p>例子参考:<a href="https://codesandbox.io/s/tiaoguohuidiaohanshugaibianhongfade-render-guocheng-3i59n">跳过回调函数改变触发的 Render 过程</a>。以下代码比较难以理解,可通过调试该例子,帮助理解消化。</p><h4 id="动画库直接修改-DOM-属性,跳过组件-Render-阶段"><a href="#动画库直接修改-DOM-属性,跳过组件-Render-阶段" class="headerlink" title="动画库直接修改 DOM 属性,跳过组件 Render 阶段"></a>动画库直接修改 DOM 属性,跳过组件 Render 阶段</h4><p>这个优化在业务中应该用不上,但还是非常值得学习的,将来可以应用到组件库中。参考 <a href="https://github.com/pmndrs/react-spring">react-spring</a> 的动画实现,当一个动画启动后,每次动画属性改变不会引起组件重新 Render ,而是直接修改了 dom 上相关属性值。</p><p>例子演示:<a href="https://codesandbox.io/s/donghuakuzhijiexiugai-domtiaoguoxuanranjieduan-ij7px">CodeSandbox 在线 Demo</a></p><h4 id="避免在-didMount、didUpdate-中更新组件-State"><a href="#避免在-didMount、didUpdate-中更新组件-State" class="headerlink" title="避免在 didMount、didUpdate 中更新组件 State"></a>避免在 didMount、didUpdate 中更新组件 State</h4><p>这个技巧不仅仅适用于 <code>didMount、didUpdate</code>,还包括 <code>willUnmount、useLayoutEffect</code> 和特殊场景下的 <code>useEffect</code>(当父组件的 cDU/cDM 触发时,子组件的 useEffect 会同步调用),本文为叙述方便将他们统称为「提交阶段钩子」。</p><p>React 工作流提交阶段的第二步就是执行提交阶段钩子,它们的执行会阻塞浏览器更新页面。如果在提交阶段钩子函数中更新组件 State,会再次触发组件的更新流程,造成两倍耗时。</p><p>一般在提交阶段的钩子中更新组件状态的场景有:</p><ol><li><p>计算并更新组件的派生状态(Derived State)。在该场景中,类组件应使用 <a href="https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops">getDerivedStateFromProps</a> 钩子方法代替,函数组件<a href="https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops">应使用函数调用时执行 setState的方式</a>代替。使用上面两种方式后,React 会将新状态和派生状态在一次更新内完成。</p></li><li><p>根据 DOM 信息,修改组件状态。在该场景中,除非想办法不依赖 DOM 信息,否则两次更新过程是少不了的,就只能用其他优化技巧了。</p></li></ol><p><a href="https://github.com/vercel/swr/blob/0.3.8/src/use-swr.ts#L536">use-swr</a> 的源码就使用了该优化技巧。当某个接口存在缓存数据时,use-swr 会先使用该接口的缓存数据,并在 <code>requestIdleCallback</code> 时再重新发起请求,获取最新数据。如果 use-swr 不做该优化的话,就会在 <code>useLayoutEffect</code> 中触发重新验证并设置 <code>isValidating</code> 状态为 true,引起组件的更新流程,造成性能损失。</p><h3 id="React-Profiler-定位-Render-过程瓶颈"><a href="#React-Profiler-定位-Render-过程瓶颈" class="headerlink" title="React Profiler 定位 Render 过程瓶颈"></a>React Profiler 定位 Render 过程瓶颈</h3><p><a href="">React Profiler</a> 是 React 官方提供的性能审查工具,</p><h4 id="Profiler-只记录了-Render-过程耗时"><a href="#Profiler-只记录了-Render-过程耗时" class="headerlink" title="Profiler 只记录了 Render 过程耗时"></a>Profiler 只记录了 Render 过程耗时</h4><p>通过 <code>React Profiler</code>,开发者可以查看组件 Render 过程耗时,但无法知晓提交阶段的耗时。尽管 Profiler 面板中有 Committed at 字段,但这个字段是相对于录制开始时间,根本没有意义。所以提醒读者<strong>不要通过 Profiler 定位非 Render 过程的性能瓶颈问题</strong>。</p><p>通过在 React v16 版本上进行实验,同时开启 Chrome 的 Performance 和 React Profiler 统计。如下图,在 Performance 面板中,调和阶段和提交阶段耗时分别为 642ms 和 300ms,而 Profiler 面板中只显示了 642ms。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a0b1a42188ca4a89a6828a96b7d4d0fe~tplv-k3u1fbpfcp-watermark.image" alt="profiler"></p><blockquote><p>拓展知识</p><ol><li>React 在 v17 版本后已移除 User Timing 统计功能,具体原因可参考 <a href="https://github.com/facebook/react/pull/18417">PR#18417</a>。</li><li>在 v17 版本上,笔者也通过测试代码验证了 <a href="https://codesandbox.io/s/react-profiler-shifoutongji-componentdidmount-zhixingshijian-yosid">Profiler</a> 中的统计信息并不包含提交阶段,有兴趣的读者可以看看。</li></ol></blockquote><h4 id="开启「记录组件更新原因」"><a href="#开启「记录组件更新原因」" class="headerlink" title="开启「记录组件更新原因」"></a>开启「记录组件更新原因」</h4><p>点击面板上的齿轮,然后勾选「Record why each component rendered while profiling.」,如下图。</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e7a8dc5bbda040cab31fce9905684c1f~tplv-k3u1fbpfcp-watermark.image" alt="Record"></p><p>然后点击面板中的虚拟 DOM 节点,右侧便会展示该组件重新 Render 的原因。</p><p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd269110256a4f7c93cdc9a1b59365bb~tplv-k3u1fbpfcp-watermark.image" alt="Render"></p><h4 id="定位产生本次-Render-过程原因"><a href="#定位产生本次-Render-过程原因" class="headerlink" title="定位产生本次 Render 过程原因"></a>定位产生本次 Render 过程原因</h4><p>由于 React 的批量更新(Batch Update)机制,产生一次 Render 过程可能涉及到很多个组件的状态更新。那么如何定位是哪些组件状态更新导致的呢?</p><p><strong>在 Profiler 面板左侧的虚拟 DOM 树结构中,从上到下审查每个发生了渲染的(不会灰色的)组件</strong>。如果组件是由于 State 或 Hook 改变触发了 Render 过程,那它就是我们要找的组件,如下图。</p><p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e042df82947445539ee0068835637e3d~tplv-k3u1fbpfcp-watermark.image" alt="profiler"></p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://juejin.cn/post/6935584878071119885">原文地址</a></p>
<p>本文分为三部分,首先介绍 React 的工</summary>
<category term="React" scheme="https://shinichikudo-fe.github.io/categories/React/"/>
<category term="React" scheme="https://shinichikudo-fe.github.io/tags/React/"/>
</entry>
<entry>
<title>TypeScript 高级用法</title>
<link href="https://shinichikudo-fe.github.io/2021/02/18/Ts/TypeScript-%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/"/>
<id>https://shinichikudo-fe.github.io/2021/02/18/Ts/TypeScript-%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</id>
<published>2021-02-18T08:55:58.000Z</published>
<updated>2021-02-19T01:34:41.474Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://juejin.cn/post/6926794697553739784#heading-1">原文地址</a></p><h2 id="类型"><a href="#类型" class="headerlink" title="类型"></a>类型</h2><h3 id="unknow"><a href="#unknow" class="headerlink" title="unknow"></a>unknow</h3><p><code>unknown</code> 指的是<strong>不可预先定义的类型</strong>,在很多场景下,它可以替代 <code>any</code> 的功能同时保留静态检查的能力。</p><pre><code class="ts">const num: number = 10;(num as unknown as string).split(''); // 注意,这里和any一样完全可以通过静态检查</code></pre><p>这个时候 <code>unknown</code> 的作用就跟 <code>any</code> 高度类似了,你可以把它转化成任何类型,不同的地方是,<strong>在静态编译的时候,unknown 不能调用任何方法,而 any 可以</strong>。</p><pre><code class="ts">const foo: unknown = 'string';foo.substr(1); // Error: 静态检查不通过报错const bar: any = 10;any.substr(1); // Pass: any类型相当于放弃了静态检查</code></pre><p>unknown 的一个使用场景是,<strong>避免使用 any 作为函数的参数类型而导致的静态类型检查 bug</strong>:</p><pre><code class="ts">function test(input: unknown):number { if(Array.isArray(input)){ return input.length } return input.length}</code></pre><h3 id="void"><a href="#void" class="headerlink" title="void"></a>void</h3><p>在 TS 中,<code>void</code> 和 <code>undefined</code> 功能高度类似,可以在逻辑上避免不小心使用了空指针导致的错误。</p><pre><code class="ts">function foo() {} // 这个空函数没有返回任何值,返回类型缺省为voidconst a = foo(); // 此时a的类型定义为void,你也不能调用a的任何属性方法</code></pre><p>void 和 undefined 类型最大的区别是,你可以理解为 <strong>undefined 是 void 的一个子集,当你对函数返回值并不在意时,使用 void 而不是 undefined</strong>。举一个 React 中的实际的例子。</p><pre><code class="ts">// Parent.tsxfunction Parent(): JSX.Element { const getValue = (): number => { return 2 }; /* 这里函数返回的是number类型 */ // const getValue = (): string => { return 'str' }; /* 这里函数返回的string类型,同样可以传给子属性 */ return <Child getValue={getValue} />}</code></pre><pre><code class="ts">// Child.tsxtype Props = { getValue: () => void; // 这里的void表示逻辑上不关注具体的返回值类型,number、string、undefined等都可以}function Child({ getValue }: Props) => <div>{getValue()}</div></code></pre><h3 id="never"><a href="#never" class="headerlink" title="never"></a>never</h3><p><code>never</code> 是<em>指没法正常结束返回的类型,<strong>一个必定会报错或者死循环的函数</strong>会返回这样的类型</em>。</p><pre><code class="ts">function foo(): never { throw new Error('error message') } // throw error 返回值是neverfunction foo(): never { while(true){} } // 这个死循环的也会无法正常退出function foo(): never { let count = 1; while(count){ count ++; } } // Error: 这个无法将返回值定义为never,因为无法在静态编译阶段直接识别出</code></pre><p>还有就是永远没有相交的类型:</p><pre><code class="ts">type human = 'boy' & 'girl' // 这两个单独的字符串类型并不可能相交,故human为never类型</code></pre><p>不过任何类型联合上 never 类型,还是原来的类型:</p><pre><code class="ts">type language = 'ts' | never // language的类型还是'ts'类型</code></pre><p>关于 never 有如下特性:</p><ul><li>在一个函数中调用了返回 never 的函数后,之后的代码都会变成<code>deadcode</code></li></ul><pre><code class="ts">function test() { foo(); // 这里的foo指上面返回never的函数 console.log(111); // Error: 编译器报错,此行代码永远不会执行到}</code></pre><ul><li>无法把其他类型赋给 <code>never</code>:</li></ul><pre><code class="ts">let n: never;let o: any = {};n = o; // Error: 不能把一个非never类型赋值给never类型,包括any</code></pre><h2 id="运算符"><a href="#运算符" class="headerlink" title="运算符"></a>运算符</h2><h3 id="非空断言运算符"><a href="#非空断言运算符" class="headerlink" title="非空断言运算符 !"></a>非空断言运算符 <code>!</code></h3><p>这个运算符可以用在变量名或者函数名之后,用来强调对应的元素是非 <code>null|undefined</code> 的</p><pre><code class="ts">function onClick(callback?: () => void) { callback!(); // 参数是可选入参,加了这个感叹号!之后,TS编译不报错}</code></pre><p>你可以查看编译后的 ES5 代码,居然没有做任何防空判断。</p><pre><code class="js">function onClick(callback) { callback();}</code></pre><p>这个符号的场景,特别适用于我们已经明确知道不会返回空值的场景,从而减少冗余的代码判断,如 React 的 <code>Ref</code>。</p><pre><code class="js">function Demo(): JSX.Elememt { const divRef = useRef<HTMLDivElement>(); useEffect(() => { divRef.current!.scrollIntoView(); // 当组件Mount后才会触发useEffect,故current一定是有值的 }, []); return <div ref={divRef}>Demo</div>}</code></pre><h3 id="可选链运算符"><a href="#可选链运算符" class="headerlink" title="可选链运算符 ?."></a>可选链运算符 <code>?.</code></h3><p>相比上面<code>!</code>作用于编译阶段的非空判断,<code>?.</code>这个是开发者最需要的运行时(当然编译时也有效)的非空判断。</p><pre><code class="ts">obj?.prop obj?.[index] func?.(args)</code></pre><p><code>?.</code>用来判断左侧的表达式是否是 null | undefined,如果是则会停止表达式运行,可以减少我们大量的<code>&&</code>运算。</p><p>比如我们写出<code>a?.b</code>时,编译器会自动生成如下代码</p><pre><code class="ts">a === null || a === void 0 ? void 0 : a.b;</code></pre><p>这里涉及到一个小知识点:<code>undefined</code>这个值在<strong>非严格模式下会被重新赋值</strong>,使用<code>void 0</code>必定返回真正的 undefined。</p><h3 id="数字分隔符"><a href="#数字分隔符" class="headerlink" title="数字分隔符_"></a>数字分隔符_</h3><pre><code class="ts">let num:number = 1_2_345.6_78_9</code></pre><p>_可以用来对长数字做任意的分隔,主要设计是为了便于数字的阅读,编译出来的代码是没有下划线的,请放心食用。</p><h2 id="操作符"><a href="#操作符" class="headerlink" title="操作符"></a>操作符</h2><h3 id="键值获取-keyof"><a href="#键值获取-keyof" class="headerlink" title="键值获取 keyof"></a>键值获取 <code>keyof</code></h3><p><code>keyof</code> 可以<strong>获取一个类型所有键值,返回一个联合类型</strong>,如下:</p><pre><code class="js">type Person = { name: string; age: number;}type PersonKey = keyof Person; // PersonKey得到的类型为 'name' | 'age'</code></pre><p>keyof 的一个典型用途是<strong>限制访问对象的 key 合法化</strong>,因为 any 做索引是不被接受的。</p><pre><code class="ts">function getValue (p: Person, k: keyof Person) { return p[k]; // 如果k不如此定义,则无法以p[k]的代码格式通过编译}</code></pre><p>总结起来 <code>keyof</code> 的语法格式如下</p><blockquote><p>类型 = keyof 类型</p></blockquote><h3 id="实例类型获取-typeof"><a href="#实例类型获取-typeof" class="headerlink" title="实例类型获取 typeof"></a>实例类型获取 <code>typeof</code></h3><p>typeof 是获取一个对象/实例的类型,如下:</p><pre><code class="ts">const me: Person = { name: 'gzx', age: 16 };type P = typeof me; // { name: string, age: number | undefined }const you: typeof me = { name: 'mabaoguo', age: 69 } // 可以通过编译</code></pre><p><code>typeof</code> <strong>只能用在具体的对象上</strong>,这与 js 中的 <code>typeof</code> 是一致的,并且它会根据左侧值自动决定应该执行哪种行为。</p><pre><code class="ts">const typestr = typeof me; // typestr的值为"object"</code></pre><p><code>typeof</code> 可以和 <code>keyof</code> 一起使用(因为 <code>typeof</code> 是返回一个类型嘛),如下:</p><pre><code class="ts">type PersonKey = keyof typeof me; // 'name' | 'age'</code></pre><p>总结起来 typeof 的语法格式如下:</p><blockquote><p>类型 = typeof 实例对象</p></blockquote><h3 id="遍历属性-in"><a href="#遍历属性-in" class="headerlink" title="遍历属性 in"></a>遍历属性 <code>in</code></h3><p><code>in</code> 只能用在类型的定义中,可以对枚举类型进行遍历,如下:</p><pre><code class="js">// 这个类型可以将任何类型的键值转化成number类型type TypeToNumber<T> = { [key in keyof T]: number}</code></pre><p><code>keyof</code> 返回泛型 <code>T</code> 的所有键枚举类型,<code>key</code>是自定义的任何变量名,中间用in链接,外围用<code>[]</code>包裹起来(这个是固定搭配),冒号右侧<code>number</code>将所有的key定义为<code>number</code>类型。</p><p>于是可以这样使用了:</p><pre><code class="ts">const obj: TypeToNumber<Person> = { name: 10, age: 10 }</code></pre><p>总结起来 in 的语法格式如下:</p><blockquote></blockquote><h2 id="泛型"><a href="#泛型" class="headerlink" title="泛型"></a>泛型</h2><p><code>泛型</code>在 TS 中可以说是一个非常重要的属性,它承载了从静态定义到动态调用的桥梁,同时也是 TS 对自己类型定义的<code>元编程</code>。泛型可以说是 TS 类型工具的精髓所在,也是整个 TS 最难学习的部分,这里专门分两章总结一下。</p><h3 id="基本使用"><a href="#基本使用" class="headerlink" title="基本使用"></a>基本使用</h3><p>泛型可以用在普通类型定义,类定义、函数定义上,如下:</p><pre><code class="ts">// 普通类型定义type Dog<T> = { name: string, type: T }// 普通类型使用const dog: Dog<number> = { name: 'ww', type: 20 }// 类定义class Cat<T> { private type: T; constructor(type: T) { this.type = type; }}// 类使用const cat: Cat<number> = new Cat<number>(20); // 或简写 const cat = new Cat(20)// 函数定义function swipe<T, U>(value: [T, U]): [U, T] { return [value[1], value[0]];}// 函数使用swipe<Cat<number>, Dog<number>>([cat, dog]) // 或简写 swipe([cat, dog])</code></pre><p>注意,<strong>如果对一个类型名定义了泛型,那么使用此类型名的时候一定要把泛型类型也写上去</strong>。而对于变量来说,它的类型可以在调用时推断出来的话,就可以省略泛型书写。</p><p>泛型的语法格式简单总结如下:</p><blockquote><p>类型名<泛型列表> 具体类型定义</p></blockquote><h3 id="泛型推导与默认值"><a href="#泛型推导与默认值" class="headerlink" title="泛型推导与默认值"></a>泛型推导与默认值</h3><p>上面提到了,我们可以简化对泛型类型定义的书写,因为TS会自动根据变量定义时的类型推导出变量类型,这一般是发生在函数调用的场合的。</p><pre><code class="js">type Dog<T> = { name: string, type: T }function adopt<T>(dog: Dog<T>) { return dog };const dog = { name: 'ww', type: 'hsq' }; // 这里按照Dog类型的定义一个type为string的对象adopt(dog); // Pass: 函数会根据入参类型推断出type为string</code></pre><p>若不适用函数泛型推导,我们若需要定义变量类型则必须指定泛型类型。</p><pre><code class="js">const dog: Dog<string> = { name: 'ww', type: 'hsq' } // 不可省略<string>这部分</code></pre><p>如果我们想不指定,可以使用泛型默认值的方案。</p><pre><code class="js">type Dog<T = any> = { name: string, type: T }const dog: Dog = { name: 'ww', type: 'hsq' }dog.type = 123; // 不过这样type类型就是any了,无法自动推导出来,失去了泛型的意义</code></pre><p>泛型默认值的语法格式简单总结如下:</p><blockquote><p>泛型名 = 默认类型</p></blockquote><h3 id="泛型约束"><a href="#泛型约束" class="headerlink" title="泛型约束"></a>泛型约束</h3><p>有的时候,我们可以不用关注泛型具体的类型,如:</p><pre><code class="ts">function fill<T>(length: number, value: T): T[] { return new Array(length).fill(value);}</code></pre><p>这个函数接受一个长度参数和默认值,结果就是生成使用默认值填充好对应个数的数组。我们不用对传入的参数做判断,直接填充就行了,但是有时候,我们需要限定类型,这时候使用<code>extends</code>关键字即可。</p><pre><code class="ts">function sum<T extends number>(value: T[]): number{ let count = 0 value.forEach( v => count += v) return count}</code></pre><p>这样你就可以以<code>sum([1,2,3])</code>这种方式调用求和函数,而像<code>sum(['1', '2'])</code>这种是无法通过编译的。</p><p><code>泛型约束</code>也可以用在多个泛型参数的情况。</p><pre><code class="ts">function pick<T, U extends keyof T>(){};</code></pre><p>这里的意思是<strong>限制了 U 一定是 T 的 key 类型中的子集</strong>,这种用法常常出现在一些泛型工具库中。</p><p><code>extends</code> 的语法格式简单总结如下,注意下面的类型既可以是一般意义上的类型也可以是泛型。</p><blockquote><p>泛型名 extends 类型</p></blockquote><h3 id="泛型条件"><a href="#泛型条件" class="headerlink" title="泛型条件"></a>泛型条件</h3><p>上面提到 extends,其实也可以当做一个三元运算符,如下:</p><pre><code class="ts">T extends U? X: Y</code></pre><p>上面提到 extends,其实也可以当做一个三元运算符,如下:</p><pre><code class="ts">T extends U? X: Y</code></pre><p>这里便不限制 T 一定要是 U 的子类型,如果是 U 子类型,则将 T 定义为 X 类型,否则定义为 Y 类型。</p><p>注意,生成的结果是分配式的。</p><p>举个例子,如果我们把 X 换成 T,如此形式:</p><pre><code class="ts">T extends U? T: never。</code></pre><p>此时返回的 T,是满足原来的 T 中包含 U 的部分,可以理解为 T 和 U 的交集。</p><p>所以,extends 的语法格式可以扩展为</p><blockquote><p>泛型名A extends 类型B ? 类型C: 类型D</p></blockquote><h3 id="泛型推断-infer"><a href="#泛型推断-infer" class="headerlink" title="泛型推断 infer"></a>泛型推断 <code>infer</code></h3><p><code>infer</code> 的中文是“推断”的意思,一般是搭配上面的泛型条件语句使用的,所谓推断,就是你不用预先指定在泛型列表中,在运行时会自动判断,不过你得先预定义好整体的结构。举个例子</p><pre><code class="ts">type Foo<T> = T extends {t: infer Test} ? Test: string</code></pre><p>首选看 <code>extends</code> 后面的内容,<code>{t: infer Test}</code>可以看成是一个包含t属性的类型定义,这个t属性的 value 类型通过<code>infer</code>进行推断后会赋值给Test类型,如果泛型实际参数符合<code>{t: infer Test}</code>的定义那么返回的就是Test类型,否则默认给缺省的string类型。</p><p>举个例子加深下理解:</p><pre><code class="ts">type One = Foo<number> // string,因为number不是一个包含t的对象类型type Two = Foo<{t: boolean}> // boolean,因为泛型参数匹配上了,使用了infer对应的typetype Three = Foo<{a: number, t: () => void}> // () => void,泛型定义是参数的子集,同样适配</code></pre><p><code>infer</code>用来<strong>对满足的泛型类型进行子类型的抽取</strong>,有很多高级的泛型工具也巧妙的使用了这个方法。</p><h2 id="泛型工具"><a href="#泛型工具" class="headerlink" title="泛型工具"></a>泛型工具</h2><h3 id="Partical"><a href="#Partical" class="headerlink" title="Partical"></a>Partical<T></h3><p>此工具的作用就是将泛型中全部属性变为可选的。</p><pre><code class="ts">type Partial<T> = { [P in keyof T]?: T[P]}</code></pre><p>举个例子,这个类型定义在下面也会用到。</p><pre><code class="ts">type Animal = { name: string, category: string, age: number, eat: () => number}</code></pre><p>使用 Partical 包裹一下。</p><pre><code class="ts">type PartOfAnimal = Partical<Animal>;const ww: PartOfAnimal = { name: 'ww' }; // 属性全部可选后,可以只赋值部分属性了</code></pre><h3 id="Record-lt-K-T-gt"><a href="#Record-lt-K-T-gt" class="headerlink" title="Record<K, T>"></a>Record<K, T></h3><p>此工具的作用是<code>将 K 中所有属性值转化为 T 类型</code>,我们常用它来申明一个普通 object 对象。</p><pre><code class="ts">type Record<K extends keyof any,T> = { [key in K]: T}</code></pre><p>这里特别说明一下,<code>keyof any</code>对应的类型为<code>number | string | symbol</code>,也就是可以做对象键(专业说法叫索引 index)的类型集合。</p><p>举个例子:</p><pre><code class="ts">const obj: Record<string, string> = { 'name': 'mbg', 'tag': '年轻人不讲武德' }</code></pre><h3 id="Pick-lt-T-K-gt"><a href="#Pick-lt-T-K-gt" class="headerlink" title="Pick<T, K>"></a>Pick<T, K></h3><p>此工具的作用是<code>将 T 类型中的 K 键列表提取出来</code>,生成新的子键值对类型。</p><pre><code class="ts">type Pick<T, K extends keyof T> = { [P in K]: T[P]}</code></pre><p>我们还是用上面的Animal定义,看一下 Pick 如何使用。</p><pre><code class="ts">const bird: Pick<Animal, "name" | "age"> = { name: 'bird', age: 1 }</code></pre><h3 id="Exclude-lt-T-U-gt"><a href="#Exclude-lt-T-U-gt" class="headerlink" title="Exclude<T, U>"></a>Exclude<T, U></h3><p>此工具是在 T 类型中,去除 T 类型和 U 类型的交集,返回剩余的部分。</p><pre><code class="ts">type Exclude<T, U> = T extends U ? never : T</code></pre><p>注意这里的 extends 返回的 T 是原来的 T 中和 U 无交集的属性,而任何属性联合 never 都是自身,具体可在上文查阅。</p><p>举个例子</p><pre><code class="ts">type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"type T2 = Exclude<string | number | (() => void), Function>; // string | number</code></pre><h3 id="Omit-lt-T-K-gt"><a href="#Omit-lt-T-K-gt" class="headerlink" title="Omit<T, K>"></a>Omit<T, K></h3><p>此工具可认为是适用于键值对对象的 Exclude,它会<strong>去除类型 T 中包含 K 的键值对</strong>。</p><pre><code class="ts">type Omit = Pick<T, Exclude<keyof T, K>></code></pre><p>还是用上面的 Animal 举个例子:</p><pre><code class="ts">const OmitAnimal:Omit<Animal, 'name'|'age'> = { category: 'lion', eat: () => { console.log('eat') } }</code></pre><p>可以发现,Omit 与 Pick 得到的结果完全相反,一个是取非结果,一个取交结果。</p><h3 id="ReturnType"><a href="#ReturnType" class="headerlink" title="ReturnType"></a>ReturnType<T></h3><p>此工具就是获取 T 类型(函数)对应的返回值类型:</p><pre><code class="ts">type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;</code></pre><p>看源码其实有点多,其实可以稍微简化成下面的样子:</p><pre><code class="ts">type ReturnType<T extends func> = T extends () => infer R ? R: any;</code></pre><p>通过使用 <code>infer</code> 推断返回值类型,然后返回此类型,如果你彻底理解了 <code>infer</code> 的含义,那这段就很好理解。</p><p>举个例子:</p><pre><code class="ts">function foo(x: string | number): string | number { /*..*/ }type FooType = ReturnType<foo>; // string | number</code></pre><h3 id="Required"><a href="#Required" class="headerlink" title="Required"></a>Required<T></h3><p>此工具可以将类型 T 中所有的属性变为必选项。</p><pre><code class="ts">type Required<T> = { [P in keyof T]-?: T[P]}</code></pre><p>这里有一个很有意思的语法<code>-?</code>,你可以理解为就是 TS 中把?可选属性减去的意思。</p><p>除了这些以外,还有很多的内置的类型工具,可以参考<a href="https://www.typescriptlang.org/docs/handbook/utility-types.html">TypeScript Handbook</a>获得更详细的信息,同时 Github 上也有很多第三方类型辅助工具,如<a href="https://github.com/piotrwitek/utility-types">utility-types</a>等。</p><h2 id="项目实战"><a href="#项目实战" class="headerlink" title="项目实战"></a>项目实战</h2><h3 id="Q-偏好使用-interface-还是-type-来定义类型?"><a href="#Q-偏好使用-interface-还是-type-来定义类型?" class="headerlink" title="Q: 偏好使用 interface 还是 type 来定义类型?"></a>Q: 偏好使用 interface 还是 type 来定义类型?</h3><p><strong>A:</strong> 从用法上来说两者本质上没有区别,大家使用 React 项目做业务开发的话,主要就是用来定义 Props 以及接口数据类型。</p><p>但是从扩展的角度来说,<code>type</code> 比 <code>interface</code> 更方便拓展一些,假如有以下两个定义:</p><pre><code class="ts">type Name = { name: string };interface IName { name: string };</code></pre><p><code>想要做类型的扩展的话,type</code> 只需要一个&,而 <code>interface</code> 要多写不少代码。</p><pre><code class="ts">type Person = Name & { age: number };interface IPerson extends IName { age: number };</code></pre><p>另外 <code>type</code> 有一些 <code>interface</code> 做不到的事情,比如使用<code>|</code>进行枚举类型的组合,使用<code>typeof</code>获取定义的类型等等。</p><p>不过 interface 有一个比较强大的地方就是<strong>可以重复定义添加属性</strong>,比如我们需要给window对象添加一个自定义的属性或者方法,那么我们直接基于其 Interface 新增属性就可以了。</p><pre><code class="ts">declare global { interface Window { MyNamespace: any; }}</code></pre><p>总体来说,大家知道 TS 是<strong>类型兼容而不是类型名称匹配的</strong>,所以一般不需用面向对象的场景或者不需要修改全局类型的场合,我一般都是用 <code>type</code> 来定义类型。</p><h3 id="Q-是否允许-any-类型的出现"><a href="#Q-是否允许-any-类型的出现" class="headerlink" title="Q: 是否允许 any 类型的出现"></a>Q: 是否允许 any 类型的出现</h3><p>**A:**说实话,刚开始使用 TS 的时候还是挺喜欢用 any 的,毕竟大家都是从 JS 过渡过来的,对这种影响效率的代码开发方式并不能完全接受,因此不管是出于偷懒还是找不到合适定义的情况,使用 any 的情况都比较多。</p><p>随着使用时间的增加和对 TS 学习理解的加深,逐步离不开了 TS 带来的类型定义红利,不希望代码中出现 any,所有类型都必须要一个一个找到对应的定义,甚至已经丧失了裸写 JS 的勇气。</p><p>这是一个目前没有正确答案的问题,总是要在效率和时间等等因素中找一个最适合自己的平衡。不过我还是推荐使用 TS,随着前端工程化演进和地位的提高,强类型语言一定是多人协作和代码健壮最可靠的保障之一,<strong>多用 TS,少用 any</strong>,也是前端界的一个普遍共识。</p><h3 id="Q-类型定义文件-d-ts-如何放置"><a href="#Q-类型定义文件-d-ts-如何放置" class="headerlink" title="Q: 类型定义文件(.d.ts)如何放置"></a>Q: 类型定义文件(.d.ts)如何放置</h3><p>**A:**这个好像业界也没有特别统一的规范,我的想法如下:</p><p><strong>临时的类型,直接在使用时定义</strong></p><p>如自己写了一个组件内部的 Helper,函数的入参和出参只供内部使用也不存在复用的可能,可以直接在定义函数的时候就在后面定义。</p><pre><code class="ts">function format(input: {k: string}[]): number[] { /***/ }</code></pre><p><strong>组件个性化类型,直接定义在 ts(x)文件中</strong></p><p>如 <code>AntD</code> 组件设计,每个单独组件的 <code>Props、State</code> 等专门定义了类型并 <code>export</code> 出去。</p><pre><code class="ts">// Table.tsxexport type TableProps = { /***/ }export type ColumnProps = { /***/ }export default function Table() { /***/ }</code></pre><p>这样使用者如果需要这些类型可以通过 <code>import type</code> 的方式引入来使用。</p><p><strong>范围/全局数据,定义在.d.ts 文件中</strong></p><p>全局类型数据,这个大家毫无异议,一般根目录下有个 <code>typings</code> 文件夹,里面会存放一些全局类型定义。</p><p>假如我们使用了 <code>css module</code>,那么我们需要让 TS 识别<code>.less</code> 文件(或者<code>.scss</code>)引入后是一个对象,可以如此定义:</p><pre><code class="ts">declare module '*.less' { const resource: { [key: string]: string }; export = resource;}</code></pre><p>而对于一些全局的数据类型,如后端返回的通用的数据类型,我也习惯将其放在 <code>typings</code> 文件夹下,使用 <code>Namespace</code> 的方式来避免名字冲突,如此可以节省组件 <code>import</code> 类型定义的语句。</p><pre><code class="ts">declare namespace EdgApi { interface Department { description: string; gmt_create: string; gmt_modify: string; id: number; name: string; }}</code></pre><p>这样,每次使用的时候,只需要const department: EdgeApi.Department即可,节省了不少导入的精力。开发者只要能约定规范,避免命名冲突即可。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://juejin.cn/post/6926794697553739784#heading-1">原文地址</a></p>
<h2 id="类型"><a</summary>
<category term="Ts" scheme="https://shinichikudo-fe.github.io/categories/Ts/"/>
<category term="Ts" scheme="https://shinichikudo-fe.github.io/tags/Ts/"/>
</entry>
<entry>
<title>学习JavaScript应懂得33</title>
<link href="https://shinichikudo-fe.github.io/2021/01/20/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0JavaScript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93/"/>
<id>https://shinichikudo-fe.github.io/2021/01/20/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0JavaScript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93/</id>
<published>2021-01-20T01:41:54.000Z</published>
<updated>2021-01-20T03:42:43.100Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://www.cnblogs.com/edisonchou/p/edc_clean_code_notes.html">原文地址</a><br><a href="https://juejin.cn/post/6844903550468767758">https://juejin.cn/post/6844903550468767758</a></p><p><img src="https://raw.githubusercontent.com/ShinichiKudo-FE/PictureBed/master/blogImg/clean%20code.png" alt="clean code"></p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://www.cnblogs.com/edisonchou/p/edc_clean_code_notes.html">原文地址</a><br><a hr</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念之代码整洁之道" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93/"/>
</entry>
<entry>
<title>偏函数, 柯里化, Compose 和 Pipe</title>
<link href="https://shinichikudo-fe.github.io/2021/01/19/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E5%81%8F%E5%87%BD%E6%95%B0-%E6%9F%AF%E9%87%8C%E5%8C%96-Compose-%E5%92%8C-Pipe/"/>
<id>https://shinichikudo-fe.github.io/2021/01/19/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E5%81%8F%E5%87%BD%E6%95%B0-%E6%9F%AF%E9%87%8C%E5%8C%96-Compose-%E5%92%8C-Pipe/</id>
<published>2021-01-19T08:13:58.000Z</published>
<updated>2021-01-19T10:08:39.929Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p>原文地址<br><img src="https://blog.csdn.net/qq_42129063/article/details/81874314" alt="https://blog.csdn.net/qq_42129063/article/details/81874314"><br><img src="https://blog.csdn.net/neweastsun/article/details/75947785" alt="https://blog.csdn.net/neweastsun/article/details/75947785"><br><img src="https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch5.html" alt="https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch5.html"></p><h2 id="偏函数"><a href="#偏函数" class="headerlink" title="偏函数"></a>偏函数</h2><p>所谓偏函数,就是<strong>固定一个函数的一个或者多个参数,返回一个新的函数,这个函数用于接受剩余的参数</strong></p><p>为什么我们通常使用偏函数?</p><p>这里我们偏函数的好处是:<strong>通过创建一个名称易懂的独立函数<code>(double,triple)</code>,调用是无需每次传入第一个参数,因为第一个参数通过<code>bind</code>提供了固定值</strong>。</p><p>另一种使用偏函数情况是,<strong>当我们有一个很通用的函数,为了方便提供一个较常用的变体</strong>。</p><p>首先,我们来看一个简单的例子,下面只是一个普普通通的函数,</p><pre><code class="js">function add(a,b){ return a + b;}console.log(add(1,2));//结果3console.log(add(1,3));//结果4console.log(add(1,4));//结果5console.log(add(1,5));//结果6console.log(add(1,6));//结果7console.log(add(1,7));//结果8</code></pre><p>不知道大家有没有发现,往<code>add()</code>传入的第一个参数全都是相同的,也就是1,对于这样相同的参数,我们已经重复输入了6次。参数少的情况还好办,那参数多的时候就非常不方便了,请往下看:</p><pre><code class="js">function add(a,b,c,d,e){ return a + b + c + d + e;}console.log(add(1,2,3,4,5));console.log(add(1,2,3,1,2));console.log(add(1,2,3,3,5));console.log(add(1,2,3,2,11));console.log(add(1,2,3,3,8));console.log(add(1,2,3,7,5));</code></pre><p>我们可以定义一个函数,把它命名为<code>partial</code>,这个函数就相当于一个工厂,需要接受一个入参函数,这个工厂用来生产偏函数,按照这个思路,可以写出大概的构架如下:</p><pre><code class="js">//入参函数function add(a,b){ return a + b;}//生产偏函数的工厂,接受一个入参函数,返回一个新的函数,用于接受剩余的参数function partial(fn,a){ return function(b){ return fn(a,b); }}var parAdd = partial(add,1);//变量parAdd接受返回的新函数console.log(parAdd(2));//在调用的时候传入剩余的参数console.log(parAdd(3));//在调用的时候传入剩余的参数console.log(parAdd(4));//在调用的时候传入剩余的参数console.log(parAdd(5));//在调用的时候传入剩余的参数</code></pre><h3 id="偏函数的简单应用"><a href="#偏函数的简单应用" class="headerlink" title="偏函数的简单应用"></a>偏函数的简单应用</h3><ul><li><code>Function.prototype.bind()</code></li></ul><p>这是MDN对于<code>bind()</code>的描述:<code>bind()</code> 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当新函数被调用时 this 值绑定到 <code>bind()</code> 的第一个参数,该参数不能被重写。绑定函数被调用时,<code>bind()</code> 也接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。<br>不知道大家有木有发现在上面的描述中,出现了几个很重要的表述,创建一个新函数,具有相同的函数体,调用时的参数被提供给模拟函数,哈哈,这不正和我刚刚说的生产偏函数工厂相似吗。所以对于上面的应用,我们还可以这样改写:</p><pre><code class="js">function add(a,b){ return a + b;}var obj = {};obj.parAdd = add.bind(obj,1);console.log(obj.parAdd(2));//结果3</code></pre><h3 id="使用没有上下文的偏函数"><a href="#使用没有上下文的偏函数" class="headerlink" title="使用没有上下文的偏函数"></a>使用没有上下文的偏函数</h3><p>如果想固定一些参数,但不绑定<code>this</code>呢?</p><p>内置的<code>bind</code>不允许这样,我们不能忽略上下文并跳转到参数。幸运的是,可以仅绑定参数<code>partial</code>函数容易实现。</p><pre><code class="js">function partial(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); }}// Usage:let user = { firstName: "John", say(time, phrase) { alert(`[${time}] ${this.firstName}: ${phrase}!`); }};// add a partial method that says something now by fixing the first argumentuser.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());user.sayNow("Hello");// Something like:// [10:00] Hello, John!</code></pre><p>调用<code>partial(func[, arg1, arg2...])</code>函数的结果为调用func的包装器(*号行):</p><p>this一致(因为<code>user.sayNow</code>是通过<code>user</code>调用的)<br>然后给其<code>...garsBound—— partial</code>使用该参数(“10:00”)进行调用。<br>然后提供参数<code>...gars</code>——提供给包装器的参数(“Hello“)<br>所以使用<code>spread</code>运算符很容易实现,是吗?<br>loadash库也提供了<code>—.partial</code>实现。</p><h2 id="柯里化"><a href="#柯里化" class="headerlink" title="柯里化"></a>柯里化</h2><p>有时人们混淆上面提及的偏函数和另一个名称为<code>“柯里化”</code>函数功能,柯里化是另一个有趣的处理函数技术,这里我们必须要涉及。</p><p><strong>与柯里化的区别</strong>:</p><ul><li><p>柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。</p></li><li><p>局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。</p></li></ul><p><strong>柯里化(Currying)</strong>:转换一个调用函数<code>f(a,b,c)</code>为<code>f(a)(b)(c)</code>方式调用。</p><p>让我们实现柯里化函数,执行一个两元参数函数,即转换<code>f(a,b)</code>至<code>f(a)(b)</code>:</p><pre><code class="js">function curry(func) { return function(a) { return function(b) { return func(a, b); }; };}// usagefunction sum(a, b) { return a + b;}let carriedSum = curry(sum);alert( carriedSum(1)(2) ); // 3</code></pre><p>上面是通过一系列包装器实现的。</p><p><code>curry(func)</code>的结果是<code>function(a)</code>的一个包装器。<br>当调用<code>sum(1)</code>是,参数被保存在词法环境中,然后返回新的包装器<code>function(b)</code><br>然后<code>sum(1)(2)</code>提供2并最终调用<code>function(b)</code>,然后传递调用给原始多参数函数<code>sum</code>。</p><p>有一些柯里化的高级实现,如lodash库中<code>_.curry</code>可以实现更复杂功能。其返回一个包装器,它允许函数提供全部参数被正常调用或返回偏函数。</p><pre><code class="js">function curry(func) { return function curried(...args) { if (args.length >= func.length) { return func.apply(this, args); } else { return function(...args2) { return curried.apply(this, args.concat(args2)); } } };}function sum(a, b, c) { return a + b + c;}let curriedSum = curry(sum);// still callable normallyalert( curriedSum(1, 2, 3) ); // 6// get the partial with curried(1) and call it with 2 other argumentsalert( curriedSum(1)(2,3) ); // 6</code></pre><h2 id="Compose"><a href="#Compose" class="headerlink" title="Compose"></a>Compose</h2><p>compose就是执行一系列的任务(函数),比如有以下任务队列,</p><pre><code class="js">let tasks = [step1, step2, step3, step4]</code></pre><p>每一个step都是一个步骤,按照步骤一步一步的执行到结尾,这就是一个compose<br>compose在函数式编程中是一个很重要的工具函数,在这里实现的compose有三点说明</p><ul><li>第一个函数是多元的(接受多个参数),后面的函数都是单元的(接受一个参数)</li><li>执行顺序的自右向左的</li><li>所有函数的执行都是同步的(异步的后面文章会讲到)</li></ul><p>还是用一个例子来说,比如有以下几个函数</p><pre><code class="js">let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)let step2 = (val) => val + 2let step3 = (val) => val + 3let step4 = (val) => val + 4</code></pre><p>这几个函数组成一个任务队列</p><pre><code class="js">steps = [step4, step3, step2, init]</code></pre><p>使用compose组合这个队列并执行</p><pre><code class="js">let composeFunc = compose(...steps)console.log(composeFunc(1, 2, 3)) //执行过程//6 -> 6 + 2 = 8 -> 8 + 3 = 11 -> 11 + 4 = 15</code></pre><p>所以流程就是从init自右到左依次执行,下一个任务的参数是上一个任务的返回结果,并且任务都是同步的,这样就能保证任务可以按照有序的方向和有序的时间执行。</p><h3 id="实现compose的五种思路"><a href="#实现compose的五种思路" class="headerlink" title="实现compose的五种思路"></a>实现compose的五种思路</h3><p>所有思路的执行过程都是上面的例子,以下只讲compose实现</p><h4 id="面向过程"><a href="#面向过程" class="headerlink" title="面向过程"></a>面向过程</h4><p>这个思路就是使用递归的过程思想,不断的检测队列中是否还有任务,如果有任务就执行,并把执行结果往后传递,这里是一个局部的思维,无法预知任务何时结束。直观上最容易结束和理解。</p><pre><code class="js">const compose = function(...args) { let length = args.length let count = length - 1 let result return function f1 (...arg1) { result = args[count].apply(this, arg1) if (count <= 0) { count = length - 1 return result } count-- return f1.call(null, result) }}</code></pre><h4 id="函数组合"><a href="#函数组合" class="headerlink" title="函数组合"></a>函数组合</h4><p>这个思路是一种函数组合的思想,将函数两两组合,不断的生成新的函数,生成的新函数挟裹了函数执行的逻辑信息,然后再两两组合,不断的传递下去,这种思路可以提前遍历所有任务,将任务组合成一个可以展开的组合结构,最后执行的时候就像推导多米诺骨牌一样。</p><p>函数的组合过程</p><pre><code class="js">f1 = (...arg) => step2.call(null, init.apply(null, arg))f2 = (...arg) => step3.call(null, f1.apply(null, arg))f3 = (...arg) => step4.call(null, f2.apply(null, arg))</code></pre><p><code>compose实现</code></p><pre><code class="js">const _pipe = (f, g) => (...arg) => g.call(null, f.apply(null, arg))const compose = (...args) => args.reverse().reduce(_pipe, args.shift())</code></pre><h4 id="函数交织(AOP)"><a href="#函数交织(AOP)" class="headerlink" title="函数交织(AOP)"></a>函数交织(AOP)</h4><p>这个实现的灵感来自javascript设计模式中的高阶函数,因为<code>compose</code>的任务在本质上就是<strong>函数执行,再加上顺序</strong>,所以可以把实现顺序执行放到函数本身,对函数的原型进行方法的绑定。方法的作用对象是函数,面向对象封装的数据,面向函数封装的是函数的行为。</p><p>需要对函数绑定两个行为 <code>before</code> 和 <code>after</code>,<code>before</code>执行函数多元部分(启动),after执行函数单元部分</p><pre><code class="js">Function.prototype.before = function(fn) { const self = this return function(...args) { let result = fn.apply(null, args) return self.call(null, result) }}Function.prototype.after = function(fn) { const self = this return function(...args) { let result = self.apply(null, args) return fn.call(null, result) }}</code></pre><p>这里对函数进行方法的绑定,返回的是带着函数执行的规则的另外一个函数,在这里是次序的排列规则,对返回的函数依然可以进行链式调用。</p><p><code>compose 实现</code></p><pre><code class="js">const compose = function(...args) { let before = args.pop() let start = args.pop() if (args.length) { return args.reduce(function(f1, f2) { return f1.after(f2) }, start.before(before)) } return start.before(before)}</code></pre><p>函数执行过程</p><pre><code class="js">step2.before(init).after(step3).after(step4)fn3.after(step4)fn3 = fn2.after(step3)fn2 = fn1.before(step1)fn1 = init -> step2 -> step3 -> step4</code></pre><h4 id="Promise"><a href="#Promise" class="headerlink" title="Promise"></a>Promise</h4><p>ES6引入了<code>Promise</code>,<code>Promise</code>可以指定一个<code>sequence</code>,来规定一个执行<code>then</code>的过程,<code>then</code>函数会等到执行完成后,再执行下一个<code>then</code>的处理。启动<code>sequence</code>可以使用<br><code>Promise.resolve()</code>这个函数。构建<code>sequence</code>可以使用reduce</p><p>compose实现</p><pre><code class="js">const compose = function(...args) { let init = args.pop() return function(...arg) { return args.reverse().reduce(function(sequence, func) { return sequence.then(function(result) { return func.call(null, result) }) }, Promise.resolve(init.apply(null, arg))) }}</code></pre><h4 id="Generator"><a href="#Generator" class="headerlink" title="Generator"></a>Generator</h4><p><code>Generator</code>主要使用<code>yield</code>来构建协程,采用中断,处理,再中断的流程。可以事先规定好协程的执行顺序,然后再下次处理的时候进行参数(结果)交接,有一点要注意的是,由于执行的第一个next是不能传递参数的,所以第一个函数的执行需要手动调用,再空耗一个<code>next</code>,后面的就可以同步执行了。<br><code>generator</code>构建</p><pre><code class="js">function* iterateSteps(steps) { let n for (let i = 0; i < steps.length; i++) { if (n) { n = yield steps[i].call(null, n) } else { n = yield } }}</code></pre><p><code>compose实现</code></p><pre><code class="js">const compose = function(...steps) { let g = iterateSteps(steps) return function(...args) { let val = steps.pop().apply(null, args) // 这里是第一个值 console.log(val) // 因为无法传参数 所以无所谓执行 就是空耗一个yield g.next() return steps.reverse.reduce((val, val1) => g.next(val).value, val) }}</code></pre><h2 id="Pipe"><a href="#Pipe" class="headerlink" title="Pipe"></a>Pipe</h2><p><code>pipe</code>函数跟<code>compose</code>函数的左右是一样的,也是将参数平铺,只不过他的顺序是<strong>从左往右</strong>。我们来实现下,只需要将reduceRight改成reduce就行了:</p><pre><code class="js">const pipe = function(){ const args = [].slice.apply(arguments); return function(x) { return args.reduce((res, cb) => cb(res), x); }}// 参数顺序改为从左往右let calculate = pipe(add, multiply);let res = calculate(10);console.log(res); // 结果还是200</code></pre><p>ES6写法: </p><pre><code class="js">const pipe = (...args) => x => args.reduce((res, cb) => cb(res), x)</code></pre>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p>原文地址<br><img src="https://blog.csdn.net/qq_42129063/article/details/81874314" alt="https:/</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之二进制,十进制,十六进制,科学计数法</title>
<link href="https://shinichikudo-fe.github.io/2021/01/18/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E4%BA%8C%E8%BF%9B%E5%88%B6%EF%BC%8C%E5%8D%81%E8%BF%9B%E5%88%B6%EF%BC%8C%E5%8D%81%E5%85%AD%E8%BF%9B%E5%88%B6%EF%BC%8C%E7%A7%91%E5%AD%A6%E8%AE%A1%E6%95%B0%E6%B3%95/"/>
<id>https://shinichikudo-fe.github.io/2021/01/18/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E4%BA%8C%E8%BF%9B%E5%88%B6%EF%BC%8C%E5%8D%81%E8%BF%9B%E5%88%B6%EF%BC%8C%E5%8D%81%E5%85%AD%E8%BF%9B%E5%88%B6%EF%BC%8C%E7%A7%91%E5%AD%A6%E8%AE%A1%E6%95%B0%E6%B3%95/</id>
<published>2021-01-18T10:08:11.000Z</published>
<updated>2021-01-18T10:44:59.188Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://www.cnblogs.com/gaizai/p/4233780.html#_labelConvert11">原文地址</a></p><h1 id="进制转换算法(Convert)"><a href="#进制转换算法(Convert)" class="headerlink" title="进制转换算法(Convert)"></a>进制转换算法(Convert)</h1><h2 id="(二、八、十六进制)-→-(十进制)"><a href="#(二、八、十六进制)-→-(十进制)" class="headerlink" title="(二、八、十六进制) → (十进制)"></a>(二、八、十六进制) → (十进制)</h2><p><img src="https://images0.cnblogs.com/blog/48305/201501/191445576568629.png" alt="(二、八、十六进制) → (十进制)"></p><h3 id="二进制-→-十进制"><a href="#二进制-→-十进制" class="headerlink" title="二进制 → 十进制"></a>二进制 → 十进制</h3><p><strong>方法:二进制数从低位到高位(即从右往左)计算</strong>,第0位的权值是2的0次方,第1位的权值是2的1次方,第2位的权值是2的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。</p><p>例:将二进制的(101011)B转换为十进制的步骤如下:</p><ol><li><p>第0位 1 x 2^0 = 1;</p></li><li><p>第1位 1 x 2^1 = 2;</p></li><li><p>第2位 0 x 2^2 = 0;</p></li><li><p>第3位 1 x 2^3 = 8;</p></li><li><p>第4位 0 x 2^4 = 0;</p></li><li><p>第5位 1 x 2^5 = 32;</p></li><li><p>读数,把结果值相加,1+2+0+8+0+32=43,即(101011)B=(43)D。</p></li></ol><h3 id="八进制-→-十进制"><a href="#八进制-→-十进制" class="headerlink" title="八进制 → 十进制"></a>八进制 → 十进制</h3><p><strong>方法:八进制数从低位到高位(即从右往左)计算</strong>,第0位的权值是8的0次方,第1位的权值是8的1次方,第2位的权值是8的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。</p><blockquote><p>八进制就是逢8进1,八进制数采用 0~7这八数来表达一个数。</p></blockquote><p>例:将八进制的(53)O转换为十进制的步骤如下:</p><ol><li><p>第0位 3 x 8^0 = 3;</p></li><li><p>第1位 5 x 8^1 = 40;</p></li><li><p>读数,把结果值相加,3+40=43,即(53)O=(43)D。</p></li></ol><h3 id="十六进制-→-十进制"><a href="#十六进制-→-十进制" class="headerlink" title="十六进制 → 十进制"></a>十六进制 → 十进制</h3><p><strong>方法:十六进制数从低位到高位(即从右往左)计算</strong>,第0位的权值是16的0次方,第1位的权值是16的1次方,第2位的权值是16的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。</p><blockquote><p>十六进制就是逢16进1,十六进制的16个数为0123456789ABCDEF。</p></blockquote><p>例:将十六进制的(2B)H转换为十进制的步骤如下:</p><ol><li><p>第0位 B x 16^0 = 11;</p></li><li><p>第1位 2 x 16^1 = 32;</p></li><li><p>读数,把结果值相加,11+32=43,即(2B)H=(43)D。</p></li></ol><h2 id="(十进制)-→-(二、八、十六进制)"><a href="#(十进制)-→-(二、八、十六进制)" class="headerlink" title="(十进制) → (二、八、十六进制)"></a>(十进制) → (二、八、十六进制)</h2><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446004698089.png" alt="(十进制) → (二、八、十六进制)"></p><h3 id="十进制-→-二进制"><a href="#十进制-→-二进制" class="headerlink" title="十进制 → 二进制"></a>十进制 → 二进制</h3><p><strong>方法:除2取余法</strong>,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。 </p><p>例:将十进制的(43)D转换为二进制的步骤如下:</p><ol><li><p>将商43除以2,商21余数为1;</p></li><li><p>将商21除以2,商10余数为1;</p></li><li><p>将商10除以2,商5余数为0;</p></li><li><p>将商5除以2,商2余数为1;</p></li><li><p>将商2除以2,商1余数为0; </p></li><li><p>将商1除以2,商0余数为1; </p></li><li><p>读数,因为最后一位是经过多次除以2才得到的,因此它是最高位,读数字从最后的余数向前读,101011,即(43)D=(101011)B。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446019539875.png" alt="十进制 → 二进制"></p><h3 id="十进制-→-八进制"><a href="#十进制-→-八进制" class="headerlink" title="十进制 → 八进制"></a>十进制 → 八进制</h3><p><strong>方法1:除8取余法</strong>,即每次将整数部分除以8,余数为该位权上的数,而商继续除以8,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数起,一直到最前面的一个余数。</p><p>例:将十进制的(796)D转换为八进制的步骤如下:</p><ol><li><p>将商796除以8,商99余数为4;</p></li><li><p>将商99除以8,商12余数为3;</p></li><li><p>将商12除以8,商1余数为4;</p></li><li><p>将商1除以8,商0余数为1;</p></li><li><p>读数,因为最后一位是经过多次除以8才得到的,因此它是最高位,读数字从最后的余数向前读,1434,即(796)D=(1434)O。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446033751877.png" alt="十进制 → 八进制"></p><p><strong>方法2:使用间接法,先将十进制转换成二进制,然后将二进制又转换成八进制</strong>;</p><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446059386793.png" alt="十进制 → 二进制 → 八进制"></p><h3 id="十进制-→-十六进制"><a href="#十进制-→-十六进制" class="headerlink" title="十进制 → 十六进制"></a>十进制 → 十六进制</h3><p><strong>方法1:除16取余法</strong>,即每次将整数部分除以16,余数为该位权上的数,而商继续除以16,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数起,一直到最前面的一个余数。</p><p>例:将十进制的(796)D转换为十六进制的步骤如下:</p><ol><li><p>将商796除以16,商49余数为12,对应十六进制的C;</p></li><li><p>将商49除以16,商3余数为1;</p></li><li><p>将商3除以16,商0余数为3;</p></li><li><p>读数,因为最后一位是经过多次除以16才得到的,因此它是最高位,读数字从最后的余数向前读,31C,即(796)D=(31C)H。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446083759737.png" alt="十进制 → 十六进制"></p><p><strong>方法2:使用间接法,先将十进制转换成二进制,然后将二进制又转换成十六进制</strong></p><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446107502896.png" alt="十进制 → 二进制 → 十六进制"></p><h2 id="(二进制)-↔-(八、十六进制)"><a href="#(二进制)-↔-(八、十六进制)" class="headerlink" title="(二进制) ↔ (八、十六进制)"></a>(二进制) ↔ (八、十六进制)</h2><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446126101198.png" alt="(二进制) ↔ (八、十六进制)"></p><h3 id="二进制-→-八进制"><a href="#二进制-→-八进制" class="headerlink" title="二进制 → 八进制"></a>二进制 → 八进制</h3><p><strong>方法:取三合一法</strong>,即从二进制的小数点为分界点,向左(向右)每三位取成一位,接着将这三位二进制按权相加,然后,按顺序进行排列,小数点的位置不变,得到的数字就是我们所求的八进制数。如果向左(向右)取三位后,取到最高(最低)位时候,如果无法凑足三位,可以在小数点最左边(最右边),即整数的最高位(最低位)添0,凑足三位。</p><p>例:将二进制的(11010111.0100111)B转换为八进制的步骤如下:</p><ol><li><p>小数点前111 = 7;</p></li><li><p>010 = 2;</p></li><li><p>11补全为011,011 = 3;</p></li><li><p>小数点后010 = 2;</p></li><li><p>011 = 3;</p></li><li><p>1补全为100,100 = 4;</p></li><li><p>读数,读数从高位到低位,即(11010111.0100111)B=(327.234)O。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446144855271.png" alt="二进制 → 八进制"></p><p>二进制与八进制编码对应表:</p><p>可以使用冒号来定义表格的对齐方式,如下:</p><table><thead><tr><th align="left">二进制</th><th align="right">八进制</th></tr></thead><tbody><tr><td align="left">000</td><td align="right">0</td></tr><tr><td align="left">001</td><td align="right">1</td></tr><tr><td align="left">010</td><td align="right">2</td></tr><tr><td align="left">011</td><td align="right">3</td></tr><tr><td align="left">100</td><td align="right">4</td></tr><tr><td align="left">101</td><td align="right">5</td></tr><tr><td align="left">110</td><td align="right">6</td></tr><tr><td align="left">111</td><td align="right">7</td></tr></tbody></table><h3 id="八进制-→-二进制"><a href="#八进制-→-二进制" class="headerlink" title="八进制 → 二进制"></a>八进制 → 二进制</h3><p><strong>方法:取一分三法</strong>,即将一位八进制数分解成三位二进制数,用三位二进制按权相加去凑这位八进制数,小数点位置照旧。</p><p>例:将八进制的(327)O转换为二进制的步骤如下:</p><ol><li><p>3 = 011;</p></li><li><p>2 = 010;</p></li><li><p>7 = 111;</p></li><li><p>读数,读数从高位到低位,011010111,即(327)O=(11010111)B。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446161888058.png" alt="八进制 → 二进制"></p><h3 id="二进制-→-十六进制"><a href="#二进制-→-十六进制" class="headerlink" title="二进制 → 十六进制"></a>二进制 → 十六进制</h3><p><strong>方法:取四合一法</strong>,即从二进制的小数点为分界点,向左(向右)每四位取成一位,接着将这四位二进制按权相加,然后,按顺序进行排列,小数点的位置不变,得到的数字就是我们所求的十六进制数。如果向左(向右)取四位后,取到最高(最低)位时候,如果无法凑足四位,可以在小数点最左边(最右边),即整数的最高位(最低位)添0,凑足四位。</p><p>例:将二进制的(11010111)B转换为十六进制的步骤如下:</p><ol><li><p>0111 = 7;</p></li><li><p>1101 = D;</p></li><li><p>读数,读数从高位到低位,即(11010111)B=(D7)H。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446167506929.png" alt="二进制 → 十六进制"></p><h3 id="十六进制-→-二进制"><a href="#十六进制-→-二进制" class="headerlink" title="十六进制 → 二进制"></a>十六进制 → 二进制</h3><p><strong>方法:取一分四法</strong>,即将一位十六进制数分解成四位二进制数,用四位二进制按权相加去凑这位十六进制数,小数点位置照旧。</p><p>例:将十六进制的(D7)H转换为二进制的步骤如下:</p><ol><li><p>D = 1101;</p></li><li><p>7 = 0111;</p></li><li><p>读数,读数从高位到低位,即(D7)H=(11010111)B。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446194223347.png" alt="十六进制 → 二进制"></p><h2 id="(八进制)-↔-(十六进制)"><a href="#(八进制)-↔-(十六进制)" class="headerlink" title="(八进制) ↔ (十六进制)"></a>(八进制) ↔ (十六进制)</h2><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446216102748.png" alt="(八进制) ↔ (十六进制)"></p><h3 id="八进制-→-十六进制"><a href="#八进制-→-十六进制" class="headerlink" title="八进制 → 十六进制"></a>八进制 → 十六进制</h3><p>方法:<strong>将八进制转换为二进制,然后再将二进制转换为十六进制,小数点位置不变</strong>。</p><p>例:将八进制的(327)O转换为十六进制的步骤如下:</p><ol><li><p>3 = 011;</p></li><li><p>2 = 010;</p></li><li><p>7 = 111;</p></li><li><p>0111 = 7;</p></li><li><p>1101 = D;</p></li><li><p>读数,读数从高位到低位,D7,即(327)O=(D7)H。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446233756320.png" alt="八进制 → 十六进制"></p><h3 id="十六进制-→-八进制"><a href="#十六进制-→-八进制" class="headerlink" title="十六进制 → 八进制"></a>十六进制 → 八进制</h3><p><strong>方法:将十六进制转换为二进制,然后再将二进制转换为八进制,小数点位置不变</strong>。</p><p>例:将十六进制的(D7)H转换为八进制的步骤如下:</p><ol><li><p>7 = 0111;</p></li><li><p>D = 1101;</p></li><li><p>0111 = 7;</p></li><li><p>010 = 2;</p></li><li><p>011 = 3;</p></li><li><p>读数,读数从高位到低位,327,即(D7)H=(327)O。</p></li></ol><p><img src="https://images0.cnblogs.com/blog/48305/201501/191446253919136.png" alt="十六进制 → 八进制"></p><h2 id="扩展阅读"><a href="#扩展阅读" class="headerlink" title="扩展阅读"></a>扩展阅读</h2><ol><li>包含小数的进制换算:</li></ol><pre><code class="js">(ABC.8C)H=10x16^2+11x16^1+12x16^0+8x16^-1+12x16^-2=2560+176+12+0.5+0.046875=(2748.546875)D</code></pre><ol start="2"><li>负次幂的计算:</li></ol><pre><code class="js">2^-5=2^(0-5)=2^0/2^5=1/2^5</code></pre><p>同底数幂相除,底数不变,指数相减,反过来</p><ol start="3"><li>我们需要了解一个数学关系,即23=8,24=16,而八进制和十六进制是用这关系衍生而来的,即用三位二进制表示一位八进制,用四位二进制表示一位十六进制数。接着,记住4个数字8、4、2、1(23=8、22=4、21=2、20=1)。</li></ol>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://www.cnblogs.com/gaizai/p/4233780.html#_labelConvert11">原文地址</a></p>
<h1 i</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之Javascript引擎</title>
<link href="https://shinichikudo-fe.github.io/2021/01/15/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BJavascript%E5%BC%95%E6%93%8E/"/>
<id>https://shinichikudo-fe.github.io/2021/01/15/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BJavascript%E5%BC%95%E6%93%8E/</id>
<published>2021-01-15T01:33:38.000Z</published>
<updated>2021-01-15T02:45:19.865Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><h2 id="各个浏览器引擎"><a href="#各个浏览器引擎" class="headerlink" title="各个浏览器引擎"></a>各个浏览器引擎</h2><table><thead><tr><th align="left">应用程序(实现)</th><th align="center">方言和最后版本</th><th align="right">ECMAScript版本</th></tr></thead><tbody><tr><td align="left">Chrome浏览器,V8引擎</td><td align="center">JavaScript</td><td align="right">ECMA-262,版本3</td></tr><tr><td align="left">Mozilla Firefox,Gecko排版引擎,SpiderMonkey和Rhino</td><td align="center">JavaScript 1.8.1</td><td align="right">ECMA-262,版本3</td></tr><tr><td align="left">Opera</td><td align="center">一些JavaScript 1.5特性及一些JScript扩展</td><td align="right">ECMA-262,版本3</td></tr><tr><td align="left">KHTML排版引擎,KDE项目的Konqueror与苹果的Safari</td><td align="center">JavaScript 1.5</td><td align="right">ECMA-262,版本3</td></tr><tr><td align="left">Adobe Acrobat</td><td align="center">JavaScript 1.5</td><td align="right">ECMA-262,版本3</td></tr><tr><td align="left">OpenLaszlo Platform</td><td align="center">JavaScript 1.4</td><td align="right">ECMA-262,版本3</td></tr><tr><td align="left">Max/MSP</td><td align="center">JavaScript 1.5</td><td align="right">ECMA-262,版本3</td></tr><tr><td align="left">ANT Galio 3</td><td align="center">JavaScript 1.5附带RMAI扩展</td><td align="right">ECMA-262,版本3</td></tr></tbody></table><p>我们一般说的javascript引擎指的就是就是Chrome的V8 引擎</p><h2 id="V8-引擎"><a href="#V8-引擎" class="headerlink" title="V8 引擎"></a>V8 引擎</h2><p>V8是Google Chrome浏览器内置的<code>JavaScript</code>脚本引擎。<br>Google Chrome使用V8的API,但引擎的内核部分是独立于浏览器之外的。<br>V8引擎编译和执行<code>JavaScript</code>源代码。<br>速度是V8追求的主要设计目标之一,它把<code>JavaScript</code>代码直接编译成机器码运行,比起传统的“中间代码+解释器”的引擎,优势不言而喻。<br>V8的团队说Chrome对脚本的解析和执行速度是Firefox和Safari的10倍,是IE的56倍。</p><p>V8 是一个全新的 <code>JavaScript</code> 引擎,它在设计之初就以高效地执行大型的 <code>JavaScript</code> 应用程序为目的。V8的<code>JavaScript</code>渲染引擎亮点在于更快速更强壮的<code>JavaScript</code>解析。V8是一个非常反传统的<code>JavaScript</code>引擎,它能够在后台动态的对JS的对象进行分类——一个在其他高级语言中很常见但JS本身不支持的特性。V8对JS的解析不是基于反复loop源代码进行解释而是直接将JS代码编译成机器码运行。换句话说,V8引擎实际上可以看做是JS的扩展和编译器——而传统上类似于JS的解释型语言恰恰是不需要编译器的。最后,高级语言的内存管理效能一直是决定其运行效率的重要因素,而当前的JS虚拟机在这方面做的比较基本,对内存的回收也非常保守。V8使用的是非常强势的内存管理策略,一切在运行堆栈里无用的数据都会被强行回收,从而可以大大提高JS代码的运行效率。+</p><h2 id="了解Javascript引擎的执行机制"><a href="#了解Javascript引擎的执行机制" class="headerlink" title="了解Javascript引擎的执行机制"></a>了解Javascript引擎的执行机制</h2><p><strong>首先,请牢记2点:</strong></p><blockquote><p>(1) JS是单线程语言</p><p>(2) JS的Event Loop是JS的执行机制。深入了解JS的执行,就等于深入了解JS里的event loop</p></blockquote><h3 id="三问"><a href="#三问" class="headerlink" title="三问"></a>三问</h3><p>技术的出现,都跟现实世界里的应用场景密切相关的。</p><p>同样的,我们就结合现实场景,来回答这三个问题</p><h4 id="JS为什么是单线程的"><a href="#JS为什么是单线程的" class="headerlink" title="JS为什么是单线程的?"></a>JS为什么是单线程的?</h4><p>JS最初被设计用在浏览器中,那么想象一下,如果浏览器中的JS是多线程的。</p><pre><code class="html">场景描述:那么现在有2个线程,process1 process2,由于是多线程的JS,所以他们对同一个dom,同时进行操作process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?</code></pre><p>这样想,JS为什么被设计成单线程应该就容易理解了吧。</p><h4 id="JS为什么需要异步"><a href="#JS为什么需要异步" class="headerlink" title="JS为什么需要异步?"></a>JS为什么需要异步?</h4><pre><code class="html">场景描述:如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验</code></pre><p>所以,JS中存在异步执行。</p><h4 id="JS单线程又是如何实现异步的呢"><a href="#JS单线程又是如何实现异步的呢" class="headerlink" title="JS单线程又是如何实现异步的呢?"></a>JS单线程又是如何实现异步的呢?</h4><p>既然JS是单线程的,只能在一条线程上执行,又是如何实现的异步呢?</p><p><strong><em>是通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制</em></strong></p><h3 id="JS中的event-loop"><a href="#JS中的event-loop" class="headerlink" title="JS中的event loop"></a>JS中的event loop</h3><p>例1,观察它的执行顺序</p><pre><code class="js"> console.log(1) setTimeout(function(){ console.log(2) },0) console.log(3) // 1 3 2</code></pre><p>也就是说,setTimeout里的函数并没有立即执行,而是延迟了一段时间,满足一定条件后,才去执行的,这类代码,我们叫异步代码。</p><p>所以,这里我们首先知道了JS里的一种分类方式,就是将任务分为: <strong>同步任务和异步任务</strong></p><p>按照这种分类方式:JS的执行机制是</p><ol><li>首先判断JS是同步还是异步,同步就进入主线程,异步就进入event table</li><li>异步任务在event table中注册函数,当满足触发条件后,被推入event queue</li><li>同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中</li></ol><p>以上三步循环执行,这就是event loop</p><p>所以上面的例子,你是否可以描述它的执行顺序了呢?</p><pre><code class="html">console.log(1) 是同步任务,放入主线程里setTimeout() 是异步任务,被放入event table, 0秒之后被推入event queue里console.log(3 是同步任务,放到主线程里当 1、 3在控制条被打印后,主线程去event queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数</code></pre><p>所以,上面关于event loop就是我对JS执行机制的理解,直到我遇到了下面这段代码</p><pre><code class="js"> setTimeout(function(){ console.log('定时器开始啦') }); new Promise(function(resolve){ console.log('马上执行for循环啦'); for(var i = 0; i < 10000; i++){ i == 99 && resolve(); } }).then(function(){ console.log('执行then函数啦') }); console.log('代码执行结束');// 马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦</code></pre><p>那么,难道是异步任务的执行顺序,不是前后顺序,而是另有规定? 事实上,按照异步和同步的划分方式,并不准确。</p><p>而准确的划分方式是:</p><ul><li>**macro-task(宏任务)**:包括整体代码script,setTimeout,setInterval</li><li>**micro-task(微任务)**:Promise,process.nextTick</li></ul><p><img src="https://segmentfault.com/img/bV1TKz?w=879&h=723" alt="js event loop"></p><p>按照这种分类方式:JS的执行机制是</p><ul><li>执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里</li><li>当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完</li></ul><p>尝试按照刚学的执行机制,去分析例2:</p><pre><code class="html">首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里遇到 new Promise直接执行,打印"马上执行for循环啦"遇到then方法,是微任务,将其放到微任务的【队列里】打印 "代码执行结束"本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"到此,本轮的event loop 全部完成。下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始啦"</code></pre><h3 id="谈谈setTimeout"><a href="#谈谈setTimeout" class="headerlink" title="谈谈setTimeout"></a>谈谈setTimeout</h3><p>这段setTimeout代码什么意思? 我们一般说: 3秒后,会执行setTimeout里的那个函数</p><pre><code class="js"> setTimeout(function(){ console.log('执行了') },3000) </code></pre><p>准确的解释是: 3秒后,<code>setTimeout</code>里的函数被会推入<code>event queue</code>,而<code>event queue</code>(事件队列)里的任务,只有在主线程空闲时才会执行。</p><p>如果主线程执行内容很多,执行时间超过3秒,比如执行了10秒,那么这个函数只能10秒后执行了</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<h2 id="各个浏览器引擎"><a href="#各个浏览器引擎" class="headerlink" title="各个浏览器引擎"></a>各个浏览器引擎</h2><table</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之耗性能操作和时间复杂度</title>
<link href="https://shinichikudo-fe.github.io/2021/01/14/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E8%80%97%E6%80%A7%E8%83%BD%E6%93%8D%E4%BD%9C%E5%92%8C%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6/"/>
<id>https://shinichikudo-fe.github.io/2021/01/14/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E8%80%97%E6%80%A7%E8%83%BD%E6%93%8D%E4%BD%9C%E5%92%8C%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6/</id>
<published>2021-01-14T02:42:05.000Z</published>
<updated>2021-01-14T05:46:52.428Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://juejin.cn/post/6844903543640424461">原文地址</a></p><h2 id="算法定义"><a href="#算法定义" class="headerlink" title="算法定义"></a>算法定义</h2><p>算法的定义是这样的:解题方案的准确而完善的描述,是一系列解决问题的清晰指令。</p><h2 id="算法的效率"><a href="#算法的效率" class="headerlink" title="算法的效率"></a>算法的效率</h2><p>既然算法是解决问题的描述,那么就像一千个人眼中有一千个阿姆雷特他大姨夫一样,解决同一个问题的办法也是多种多样的,只是在这过程中我们所使用/消耗的时间或者时间以外的代价(计算机消耗的则为内存了)不一样。为了更快、更好、更强的发扬奥利奥..哦不,提高算法的效率。所以很多时候<strong>一个优秀的算法就在于它与其他实现同一个问题的算法相比,在时间或空间(内存)或者时间和空间(内存)上都得到明显的降低</strong>。</p><p>所以呢,算法的效率主要由以下两个复杂度来评估:</p><blockquote><p>时间复杂度:评估执行程序所需的时间。可以估算出程序对处理器的使用程度。</p></blockquote><blockquote><p>空间复杂度:评估执行程序所需的存储空间。可以估算出程序对计算机内存的使用程度。</p></blockquote><p>设计算法时,<strong>时间复杂度要比空间复杂度更容易出问题</strong>,所以一般情况一下我们只对时间复杂度进行研究。<br>一般面试或者工作的时候没有特别说明的话,复杂度就是指时间复杂度。</p><h3 id="时间复杂度"><a href="#时间复杂度" class="headerlink" title="时间复杂度"></a>时间复杂度</h3><p>接下来我们还需要知道另一个概念:<strong>时间频度</strong>。这个时候你可能会说:“不是说好一起学算法吗,这些东东是什么?赠品吗?”。非也非也,这是非卖品。</p><p>因为一个算法执行所消耗的时间理论上是不能算出来的,没错正是理论上,so我们任然可以在程序中测试获得。但是我们不可能又没必要对每个算法进行测试,只需要知道大概的哪个算法执行所花费的时间多,哪个花费的时间少就行了。如果一个算法所花费的时间与算法中代码语句执行次数成正比,那么那个算法执行语句越多,它的花费时间也就越多。我们把一个算法中的语句执行次数称为时间频度。通常(ps:很想知道通常是谁)用<code>T(n)</code>表示。</p><p>在时间频度<code>T(n)</code>中,n又代表着问题的规模,当n不断变化时,<code>T(n)</code>也会不断地随之变化。为了了解这个变化的规律,时间复杂度这一概念就被引入了。一般情况下算法基础本操作的重复执行次数为问题规模n的某个函数,用也就是时间频度<code>T(n)</code>。如果有某个辅助函数f(n),当趋于无穷大的时候,<code>T(n)/f(n)</code>的极限值是不为零的某个常数,那么<code>f(n)</code>是<code>T(n)</code>的同数量级函数,记作<code>T(n)=O(f(n))</code>,被称为算法的渐进时间复杂度,又简称为时间复杂度。</p><h3 id="大O表示法"><a href="#大O表示法" class="headerlink" title="大O表示法"></a>大O表示法</h3><p><strong>用O(n)来体现算法时间复杂度的记法被称作大O表示法</strong></p><p>一般我们我们评估一个算法都是直接评估它的最坏的复杂度。</p><p>大O表示法O<code>(f(n))</code>中的<code>f(n)</code>的值可以为<code>1、n、logn、n^2</code> 等,所以我们将<code>O(1)、O(n)、O(logn)、O( n^2 )</code>分别称为常数阶、线性阶、对数阶和平方阶。下面我们来看看推导大O阶的方法:</p><p><strong>推导大O阶</strong></p><ul><li>1.用常数1取代运行时间中的所有加法常数</li><li>2.只保留最高阶项</li><li>3.去除最高阶的常数</li></ul><p><strong>举例</strong></p><ul><li>常数例</li></ul><pre><code class="js">let sum = 0, n = 10; // 语句执行一次 let sum = (1+n)*n/2; // 语句执行一次 console.log(`The sum is : ${sum}`) //语句执行一次 </code></pre><p>这样的一段代码它的执行次数为 3 ,然后我们套用规则1,则这个算法的时间复杂度为O(1),也就是常数阶。</p><ul><li>线性阶</li></ul><pre><code class="js">let i =0; // 语句执行一次 while (i < n) { // 语句执行n次 console.log(`Current i is ${i}`); //语句执行n次 i++; // 语句执行n次}</code></pre><p>这个算法中代码总共执行了 <code>3n + 1</code>次,根据规则 2->3,因此该算法的时间复杂度是<code>O(n)</code>。</p><ul><li>对数阶</li></ul><pre><code class="js">let number = 1 // 语句执行一次while (number < n) { // 语句执行logn次 number *= 2; // 语句执行logn次}</code></pre><p>上面的算法中,<code>number</code>每次都放大两倍,我们假设这个循环体执行了m次,那么2^m = n即m = logn,所以整段代码执行次数为1 + 2*logn,则<code>f(n) = logn</code>,时间复杂度为O(logn)。</p><ul><li>平方阶</li></ul><pre><code class="js">for (let i = 0; i < n; i++) { // 语句执行n次 for (let j = 0; j < n; j++) { // 语句执行n^2次 console.log('I am here!'); // 语句执行n^2 }}</code></pre><p>上面的嵌套循环中,代码共执行 <code>2*n^2 + n</code>,则<code>f(n) = n^2</code>。所以该算法的时间复杂度为<code>O(n^2 )</code></p><h3 id="常见时间复杂度的比较"><a href="#常见时间复杂度的比较" class="headerlink" title="常见时间复杂度的比较"></a>常见时间复杂度的比较</h3><p>常见的时间复杂度函数相信大家在大学中都已经见过了,这里也不多做解释了:</p><p><code>O(1)<O(log n)<O(n)<O(n log n)<O(n²)<O(n³)<O(2ⁿ)<O(n!)</code></p><p>先从简单直观的 <code>O(1)</code> 和 <code>O(n)</code> 复杂度说起。<code>O(1)</code> 表示一次操作即可直接取得目标元素(比如字典或哈希表),<code>O(n)</code> 意味着先要检查 n 个元素来搜索目标,但是 <code>O(log n)</code> 是什么意思呢?</p><p>你第一次听说 <code>O(log n)</code> 时间复杂度可能是在学二分搜索算法的时候。二分搜索一定有某种行为使其时间复杂度为 log n。我们来看看是二分搜索是如何实现的。</p><p>因为在最好情况下二分搜索的时间复杂度是 O(1),最坏情况(平均情况)下 <code>O(log n)</code>,我们直接来看最坏情况下的例子。已知有 16 个元素的有序数组。</p><p>举个最坏情况的例子,比如我们要找的是数字 13。</p><p><img src="https://camo.githubusercontent.com/40f76a3e99b36549d2f5ac233780ef275daacf4e/68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f3830302f312a644f4e586b583670635a6c4a735734704a54326134772e6a706567" alt="demo1"></p><p>选中间的元素作为中心点(长度的一半)</p><p><img src="https://user-gold-cdn.xitu.io/2017/6/13/12726f93c83b795478e688fbe973ce77?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo2"></p><p>13 小于中心点,所以不用考虑数组的后一半</p><p><img src="https://user-gold-cdn.xitu.io/2017/6/13/bc3501220fdd4c705893aeeca3a286fc?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo3"></p><p>重复这个过程,每次都寻找子数组的中间元素</p><p><img src="https://user-gold-cdn.xitu.io/2017/6/13/3e61716a0b5ac34ffb52e35cb9ab39d0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo4"><br><img src="https://user-gold-cdn.xitu.io/2017/6/13/68d35559a005b7b177b804938ae0d3ee?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo5"></p><p>每次和中间元素比较都会使搜索范围减半。</p><p>所以为了从 16 个元素中找到目标元素,我们需要把数组平均分割 4 次,也就是说,</p><p><img src="https://user-gold-cdn.xitu.io/2017/6/13/faa38db12044c0150ead404b953ca668?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo6"></p><p>简化后的公式</p><p>类似的,如果有 n 个元素,</p><p><img src="https://user-gold-cdn.xitu.io/2017/6/13/873e481a75e31a6d23d57cff15094b95?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo7"></p><p>归纳一下<br><img src="https://user-gold-cdn.xitu.io/2017/6/13/0ef16a72ef31ea8bee0002afb703dbaa?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo8"></p><p>分子和分母代入指数<br><img src="https://user-gold-cdn.xitu.io/2017/6/13/710b9141e0f9e1e76b0b7ca6bf120589?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo9"></p><p>等式两边同时乘以 2^k<br><img src="https://user-gold-cdn.xitu.io/2017/6/13/c0d863682bbc66aa5ac1471edf0f3d17?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="demo10"></p><p>对数形式</p><p>所以 log n 的确是有意义的,不是吗?没有其他什么可以表示这种行为。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://juejin.cn/post/6844903543640424461">原文地址</a></p>
<h2 id="算法定义"><a href="#</summary>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之纯函数,函数副作用和状态变化</title>
<link href="https://shinichikudo-fe.github.io/2021/01/13/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E7%BA%AF%E5%87%BD%E6%95%B0%EF%BC%8C%E5%87%BD%E6%95%B0%E5%89%AF%E4%BD%9C%E7%94%A8%E5%92%8C%E7%8A%B6%E6%80%81%E5%8F%98%E5%8C%96/"/>
<id>https://shinichikudo-fe.github.io/2021/01/13/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E7%BA%AF%E5%87%BD%E6%95%B0%EF%BC%8C%E5%87%BD%E6%95%B0%E5%89%AF%E4%BD%9C%E7%94%A8%E5%92%8C%E7%8A%B6%E6%80%81%E5%8F%98%E5%8C%96/</id>
<published>2021-01-13T02:34:05.000Z</published>
<updated>2021-01-13T05:53:47.620Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><h2 id="纯函数(pure-function)"><a href="#纯函数(pure-function)" class="headerlink" title="纯函数(pure function)"></a>纯函数(pure function)</h2><p><a href="http://huziketang.mangojuice.top/books/react/lesson32">原文地址</a></p><p>简单的来说,<strong>一个函数返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数</strong></p><p>1.函数的返回结果只依赖于它的参数<br>2.函数执行过程里面没有副作用</p><h3 id="函数的返回结果只依赖于它的参数"><a href="#函数的返回结果只依赖于它的参数" class="headerlink" title="函数的返回结果只依赖于它的参数"></a>函数的返回结果只依赖于它的参数</h3><pre><code class="js">const a = 1const foo = (b) => a + bfoo(2) // 3</code></pre><p><code>foo</code> 函数不是一个纯函数,因为它返回的结果依赖于外部变量 <code>a</code>,我们在不知道 <code>a</code> 的值的情况下,并不能保证 <code>foo(2)</code> 的返回值是 3。虽然 <code>foo</code> 函数的代码实现并没有变化,传入的参数也没有变化,但它的返回值却是不可预料的,现在 <code>foo(2)</code> 是 3,可能过了一会就是 4 了,因为 <code>a</code> 可能发生了变化变成了 2。</p><pre><code class="js">const a = 1const foo = (x, b) => x + bfoo(1, 2) // => 3</code></pre><p>现在 foo 的返回结果只依赖于它的参数 <code>x</code> 和 <code>b</code>,foo(1, 2) 永远是 3。今天是 3,明天也是 3,在服务器跑是 3,在客户端跑也 3,不管你外部发生了什么变化,foo(1, 2) 永远是 3。<em>只要 foo 代码不改变,你传入的参数是确定的,那么 foo(1, 2) 的值永远是可预料的</em>。</p><p>这就是纯函数的第一个条件:<strong>一个函数的返回结果只依赖于它的参数</strong>。</p><h3 id="函数执行过程没有副作用"><a href="#函数执行过程没有副作用" class="headerlink" title="函数执行过程没有副作用"></a>函数执行过程没有副作用</h3><p>一个函数执行过程对产生了外部可观察的变化那么就说这个函数是有副作用的。</p><p>我们修改了一下 <code>foo</code>:</p><pre><code class="js">const a = 1const foo = (obj, b) => { return obj.x + b}const counter = { x: 1 }foo(counter, 2) // => 3counter.x // => 1</code></pre><p>我们把原来的 <code>x</code> 换成了 <code>obj</code>,我现在可以往里面传一个对象进行计算,计算的过程里面并不会对传入的对象进行修改,计算前后的 <code>counter</code> 不会发生任何变化,计算前是 1,计算后也是 1,它现在是纯的。但是我再稍微修改一下它:</p><pre><code class="js">const a = 1const foo = (obj, b) => { obj.x = 2 return obj.x + b}const counter = { x: 1 }foo(counter, 2) // => 4counter.x // => 2</code></pre><p>现在情况发生了变化,我在 <code>foo</code> 内部加了一句 <code>obj.x = 2</code>,计算前 <code>counter.x</code> 是 1,但是计算以后 <code>counter.x</code> 是 2。foo 函数的执行对外部的 <code>counter</code> 产生了影响,它产生了副作用,因为它修改了外部传进来的对象,现在它是不纯的。</p><p>但是你在函数内部构建的变量,然后进行数据的修改不是副作用:</p><pre><code class="js">const foo = (b) => { const obj = { x: 1 } obj.x = 2 return obj.x + b}</code></pre><p>虽然 <code>foo</code> 函数内部修改了 <code>obj</code>,但是 <code>obj</code> 是内部变量,外部程序根本观察不到,修改 <code>obj</code> 并不会产生外部可观察的变化,这个函数是没有副作用的,因此它是一个纯函数。</p><p>除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 <code>Ajax</code> 请求,还有调用 <code>window.reload</code> 刷新浏览器,甚至是 <code>console.log</code> 往控制台打印数据也是副作用。</p><p>纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据。</p><p>为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。</p><h2 id="js副作用"><a href="#js副作用" class="headerlink" title="js副作用"></a>js副作用</h2><p><a href="https://www.jb51.net/article/28079.htm">原文地址</a></p><blockquote><p>函数副作用 指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。严格的函数式语言要求函数必须无副作用。</p></blockquote><p>函数的副作用相关的几个概念, <code>Pure Function</code>、 <code>Impure Function</code>、 <code>Referential Transparent</code>。</p><p><code>纯函数 ( Pure Function )</code><br>输入输出数据流全是显式(Explicit)的。 显式(Explicit)的意思是,函数与外界交换数据只有一个唯一渠道——参数和返回值。函数从函数外部接受的所有输入信息都通过参数传递到该函数内部。函数输出到函数外部的所有信息都通过返回值传递到该函数外部。</p><p><code>非纯函数 ( Impure Function )</code><br>与之相反。 隐式(Implicit)的意思是,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如读取/修改全局变量,都叫作以隐式的方式和外界进行数据交换。</p><p><code>引用透明 ( Referential Transparent )</code><br>引用透明的概念与函数的副作用相关,且受其影响。 如果程序中两个相同值的表达式能在该程序的任何地方互相替换,而不影响程序的动作,那么该程序就具有引用透明性。它的优点是比非引用透明的语言的语义更容易理解,不那么晦涩。纯函数式语言没有变量,所以它们都具有引用透明性</p><p>以下示例说明了引用透明与函数副作用的结合</p><pre><code class="js">result1 = (fun(a) + b) / (fun(a) -c);temp = fun(a);result2 = (temp + b) / (temp -c);</code></pre><p>如果函数没有副作用,那么result1和result2将是等价的。然而如果fun有副作用,比如让b或c加1,那么result1和result2将不相等。因此,副作用违背了引用透明性。</p><p>在JavaScript中,引入了函数。但显然JS中的函数可以访问、修改全局变量(或定义在函数外的变量),如下</p><pre><code class="js">var a = 5;function fun(){a = 10;}fun(); // a 变成了10</code></pre><blockquote><p>JS中要想保证函数无副作用这项特性,只能依靠编程人员的习惯,即</p></blockquote><ul><li>1.函数入口使用参数运算,而不修改它</li><li>2.函数内不修改函数外的变量,如全局变量</li><li>3.运算结果通过函数返回给外部(出口)</li></ul>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<h2 id="纯函数(pure-function)"><a href="#纯函数(pure-function)" class="headerlink" title="纯函数(pure </summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之Memoization</title>
<link href="https://shinichikudo-fe.github.io/2021/01/12/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BMemoization/"/>
<id>https://shinichikudo-fe.github.io/2021/01/12/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BMemoization/</id>
<published>2021-01-12T01:19:00.000Z</published>
<updated>2021-01-12T02:11:29.895Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://segmentfault.com/a/1190000016703106">原文地址</a></p><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><strong>memoization 来源于拉丁语 memorandum (“to be remembered”),不要与 memorization 混淆了。</strong></p><p>简单来说,<code>memoization</code> 是一种优化技术,主要用于<strong>通过存储昂贵的函数调用的结果来加速计算机程序,并在再次发生相同的输入时返回缓存的结果</strong>。</p><h2 id="阶乘"><a href="#阶乘" class="headerlink" title="阶乘"></a>阶乘</h2><h3 id="不使用memoization"><a href="#不使用memoization" class="headerlink" title="不使用memoization"></a>不使用memoization</h3><pre><code class="js">const factorial = n =>{ if(n === 1){ return 1 }else{ return factorial(n - 1) * n }}</code></pre><h3 id="使用memoiztion"><a href="#使用memoiztion" class="headerlink" title="使用memoiztion"></a>使用memoiztion</h3><pre><code class="js">const cache = []const factorial = n =>{ if(n === 1){ return 1 }else if(cache[n - 1]){ return cache[n - 1] }else{ let result = factorial(n - 1) * n cache[n - 1] = result return result }}</code></pre><h3 id="使用闭包和memoiztion"><a href="#使用闭包和memoiztion" class="headerlink" title="使用闭包和memoiztion"></a>使用闭包和memoiztion</h3><p>常见的方式就是 闭包 和 memoiztion 一起搭配使用:</p><pre><code class="js">const factorialMemo = () =>{ const cache = [] const factorial = n =>{ if(n === 1){ return 1 } else if(cache[n - 1]){ console.log(`get factorial(${n}) from cache...`) return cache[n - 1] }else { let result = factorial(n - 1) * n cache[n - 1] = result return result } }}</code></pre><p>继续变形,下面这种编写方式是最常见的形式。</p><pre><code class="js">const factorMemo = func =>{ const cache = [] return function(n){ if(cache[n - 1]){ console.log(`get factorial(${n}) from cache...`) return cache[n - 1] }else{ const result = func.apply(null,arguments) cache[n - 1] = result return result } }}const factorial = factorMemo(function(n){ return n === 1 ? 1 : factorial(n - 1) * n})</code></pre><p>从阶乘的这个例子可以知道 <code>memoization</code> 是一个<em>空间换时间的方式</em>,存储执行结果,下次再次发生相同的输入会直接输出结果,提高了执行的速度。</p><h2 id="underscore-源码中的-memoization"><a href="#underscore-源码中的-memoization" class="headerlink" title="underscore 源码中的 memoization"></a>underscore 源码中的 memoization</h2><pre><code class="js">// Memoize an expensive function by storing its results._.memoize = function(func, hasher) { var memoize = function(key) { var cache = memoize.cache; var address = '' + (hasher ? hasher.apply(this, arguments) : key); if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); return cache[address]; }; memoize.cache = {}; return memoize;};</code></pre><p>代码一目了然,使用 <code>_.memoize</code> 来实现阶乘如下:</p><pre><code class="js">const factorial = _.memoize(function(n) { return n === 1 ? 1 : factorial(n - 1) * n});</code></pre><p>参照这个源码,上面的阶乘继续可以变形如下:</p><pre><code class="js">const factorialMemo = func => { const memoize = function(n) { const cache = memoize.cache if (cache[n - 1]) { console.log(`get factorial(${n}) from cache...`) return cache[n - 1] } else { const result = func.apply(null, arguments) cache[n - 1] = result return result } } // CHANGE memoize.cache = [] return memoize}const factorial = factorialMemo(function(n) { return n === 1 ? 1 : factorial(n - 1) * n});</code></pre><h2 id="reselect-源码中的-memoization"><a href="#reselect-源码中的-memoization" class="headerlink" title="reselect 源码中的 memoization"></a>reselect 源码中的 memoization</h2><pre><code class="js">export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) { let lastArgs = null let lastResult = null // we reference arguments instead of spreading them for performance reasons return function () { if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) { // apply arguments instead of spreading for performance. lastResult = func.apply(null, arguments) } lastArgs = arguments return lastResult }};</code></pre><p>从源码可以知道当 <code>lastArgs</code> 与 <code>arguments</code> 相同的时候,就不会再执行 <code>func</code>。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://segmentfault.com/a/1190000016703106">原文地址</a></p>
<h1 id="前言"><a href="#前</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之设计模式</title>
<link href="https://shinichikudo-fe.github.io/2021/01/11/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
<id>https://shinichikudo-fe.github.io/2021/01/11/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</id>
<published>2021-01-11T01:15:43.000Z</published>
<updated>2021-01-11T02:00:42.392Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://juejin.cn/post/6844903503266054157">原文地址</a></p><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><blockquote><p>设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案</p></blockquote><p>当然我们可以用一个通俗的说法:设计模式是解决某个特定场景下对某种问题的解决方案。因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。</p><h2 id="单例模式"><a href="#单例模式" class="headerlink" title="单例模式"></a>单例模式</h2><p>单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。</p><p><strong>适用场景:</strong> 一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。</p><pre><code class="js">class CreateUser { contructor(name){ this.name = name; this.getName() } getName(){ return this.name+ }}// 代理实现单例模式var ProxyMode = (function(){ var instance = null return function(name) { if(!instance){ instance = new CreateUser(name) } return instance }})// 测试单例模式var a = new ProxyMode('aaa')var b = new ProxyMode('bbb')console.log(a === b) //true</code></pre><h2 id="策略模式"><a href="#策略模式" class="headerlink" title="策略模式"></a>策略模式</h2><blockquote><p>策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。</p></blockquote><p>一个基于策略模式的程序至少由两部分组成。第一个部分<strong>是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程</strong>。第二个部分是<strong>环境类Context(不变),Context接受客户的请求,随后将请求委托给某一个策略类</strong>。要做到这一点,说明Context中要维持对某个策略对象的引用。</p><pre><code class="js">/*策略类*/var levelOBJ = { "A": function(money) { return money * 4; }, "B" : function(money) { return money * 3; }, "C" : function(money) { return money * 2; } };/*环境类*/var calculateBouns =function(level,money) { return levelOBJ[level](money);};console.log(calculateBouns('A',10000)); // 40000</code></pre><h2 id="代理模式"><a href="#代理模式" class="headerlink" title="代理模式"></a>代理模式</h2><blockquote><p>代理模式的定义:为一个对象提供一个代用品或占位符,以便控制对它的访问</p></blockquote><p>常用的虚拟代理形式:<strong>某一个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建</strong>(例:使用虚拟代理实现图片懒加载)</p><pre><code class="js">var imgFunc = (function(){ var imgNode = document.createElement("img"); document.body.appendChild(imgNode) return { setSrc(src){ img.src = src } }})()var proxyImg = (function(){ var img = new Image(); img.onload = function(){ imgFunc.setSrc(this.src) } return { setSrc(src){ imgFunc.setSrc('./loading.gif') img.src = src } }})()propxyImg.setSrc('./test.png')</code></pre><p>使用代理模式实现图片懒加载的优点还有符合单一职责原则。减少一个类或方法的粒度和耦合度。</p><h2 id="中介模式"><a href="#中介模式" class="headerlink" title="中介模式"></a>中介模式</h2><blockquote><p>中介者模式的定义:通过一个中介者对象,其他所有的相关对象都通过该中介者对象来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即可。通过中介者模式可以解除对象与对象之间的紧耦合关系。</p></blockquote><p>例如:现实生活中,航线上的飞机只需要和机场的塔台通信就能确定航线和飞行状态,而不需要和所有飞机通信。同时塔台作为中介者,知道每架飞机的飞行状态,所以可以安排所有飞机的起降和航线安排。</p><p>中介者模式适用的场景:例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。</p><pre><code class="js">var goods = { //手机库存 'red|32G': 3, 'red|64G': 1, 'blue|32G': 7, 'blue|32G': 6,};//中介者var mediator = (function() { var colorSelect = document.getElementById('colorSelect'); var memorySelect = document.getElementById('memorySelect'); var numSelect = document.getElementById('numSelect'); return { changed: function(obj) { switch(obj){ case colorSelect: //TODO break; case memorySelect: //TODO break; case numSelect: //TODO break; } } }})();colorSelect.onchange = function() { mediator.changed(this);};memorySelect.onchange = function() { mediator.changed(this);};numSelect.onchange = function() { mediator.changed(this);};</code></pre><h2 id="装饰者模式"><a href="#装饰者模式" class="headerlink" title="装饰者模式"></a>装饰者模式</h2><blockquote><p>装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。</p></blockquote><p>例如:现有4种型号的自行车分别被定义成一个单独的类,如果给每辆自行车都加上前灯、尾灯、铃铛这3个配件,如果用类继承的方式,需要创建4*3=12个子类。但如果通过装饰者模式,只需要创建3个类。</p><p>装饰者模式适用的场景:<strong>原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求</strong>;函数的解耦,将函数拆分成多个可复用的函数,再将拆分出来的函数挂载到某个函数上,实现相同的效果但增强了复用性</p><p>例:用AOP装饰函数实现装饰者模式</p><pre><code class="js">Function.prototype.before = function(){ var self = this; return function(){ beforefn.apply(this,arguments) return self.apply(this,arguments) }}Function.prototype.after = function () { return function(){ var ret = self.apply(this,arguments) afterfn.apply(this,arguments) return ret }}var func = function(){ console.log('2')}var func1 = function(){ console.log('1')}var func2 = function(){ console.log('2')}func = func.before(func1).after(func2)func()</code></pre>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://juejin.cn/post/6844903503266054157">原文地址</a></p>
<h1 id="前言"><a href="#前言</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之类和工厂模式</title>
<link href="https://shinichikudo-fe.github.io/2021/01/08/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E7%B1%BB%E5%92%8C%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/"/>
<id>https://shinichikudo-fe.github.io/2021/01/08/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E7%B1%BB%E5%92%8C%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/</id>
<published>2021-01-08T01:19:07.000Z</published>
<updated>2021-01-08T02:47:42.489Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><h2 id="类"><a href="#类" class="headerlink" title="类"></a>类</h2><p>类是用于创建对象的模板。他们用代码封装数据以处理该数据。 JS中的类建立在原型上,但也具有某些语法和语义未与ES5类相似语义共享。</p><h3 id="定义类"><a href="#定义类" class="headerlink" title="定义类"></a>定义类</h3><p>实际上类是特殊的函数。类的语法有两个组成部分:<strong>类表达式和类声明</strong></p><p><strong>类声明</strong></p><pre><code class="js"> class Rectangle{ constructor(height,width){ this.height = height this.width = width } }</code></pre><p><strong>类表达式</strong></p><pre><code class="js">let Rectangle = class Rectangle2 { constructor(height, width) { this.height = height; this.width = width; }}</code></pre><h3 id="使用-extends-扩展子类"><a href="#使用-extends-扩展子类" class="headerlink" title="使用 extends 扩展子类"></a>使用 extends 扩展子类</h3><p><code>extends</code>关键字在 类声明 或 类表达式 中用于创建一个类作为另一个类的一个子类。</p><pre><code class="js">class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); }}class Dog extends Animal { constructor(name) { super(name); // 调用超类构造函数并传入name参数 } speak() { console.log(`${this.name} barks.`); }}var d = new Dog('Mitzie');d.speak();// 'Mitzie barks.'</code></pre><h3 id="使用-super-调用超类"><a href="#使用-super-调用超类" class="headerlink" title="使用 super 调用超类"></a>使用 super 调用超类</h3><p><code>super</code> 关键字用于调用对象的父对象上的函数。</p><pre><code class="js">class Cat { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); }}class Lion extends Cat { speak() { super.speak(); console.log(this.name + ' roars.'); }}</code></pre><h2 id="工厂模式"><a href="#工厂模式" class="headerlink" title="工厂模式"></a>工厂模式</h2><p>Abstract Factory(抽象工厂)属于创建型模式,工厂类模式抽象程度从低到高分为:简单工厂模式 -> 工厂模式 -> 抽象工厂模式。</p><p><strong>提供一个接口以创建一系列相关或相互依赖的对象,而无须指定它们具体的类。</strong></p><p>使用工厂函数,你可以根据需要创建任意数量的用户对象。假如你正在开发一个聊天应用,你会用一个用户对象表示当前用户,以及用很多个用户对象表示其他已登录和在聊天的用户,以便显示他们的名字和头像等等。</p><p>让我们把 user 对象转换为一个 <code>createUser()</code> 工厂方法:</p><pre><code class="js">const createUser = ({ userName, avatar }) => ({ userName, avatar, setUserName (userName) { this.userName = userName; return this; }});console.log(createUser({ userName: 'echo', avatar: 'echo.png' }));/*{ "avatar": "echo.png", "userName": "echo", "setUserName": [Function setUserName]}*/</code></pre><p>最后,你还要切记,不要把事情搞复杂,工厂函数不是必需的,对于某个问题,你的解决思路应当是:</p><p><code>纯函数 > 工厂函数 > 函数式 Mixin > 类</code></p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<h2 id="类"><a href="#类" class="headerlink" title="类"></a>类</h2><p>类是用于创建对象的模板。他们用代码封装数据以处理该数据</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得的33个概念之Object.create和Object.assign</title>
<link href="https://shinichikudo-fe.github.io/2021/01/07/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BObject-create%E5%92%8CObject-assign/"/>
<id>https://shinichikudo-fe.github.io/2021/01/07/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BObject-create%E5%92%8CObject-assign/</id>
<published>2021-01-07T01:23:17.000Z</published>
<updated>2021-01-08T01:19:46.025Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p>来源<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create">MDN</a></p><h2 id="Object-create"><a href="#Object-create" class="headerlink" title="Object.create()"></a>Object.create()</h2><p><code>Object.create()</code>方法创建一个新对象,使用现有的对象来提供新创建的对象的<code>__proto__</code>。</p><pre><code class="js">const person = { isHuman: false, printIntroduction: function() { console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`); }};const me = Object.create(person);me.name = 'Matthew'; // "name" is a property set on "me", but not on "person"me.isHuman = true; // inherited properties can be overwrittenme.printIntroduction();</code></pre><h2 id="Object-assign-target-…source"><a href="#Object-assign-target-…source" class="headerlink" title="Object.assign(target,…source)"></a>Object.assign(target,…source)</h2><p><code>Object.assign()</code> 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。如果目标对象中的属性具有相同的键,则属性将被源中的属性覆盖。后来的源的属性将类似地覆盖早先的属性。</p><p><code>null</code> 或 <code>undefined</code> 源被视为空对象一样对待,不会对目标对象产生任何影响。”</p><pre><code class="js">const target1 = {a: 1, b: 2}const target2 = {c: 3, d: 4}const returnTarget = Object.assign(target1, target2)console.log(target1)console.log(returnTarget)</code></pre><h3 id="深拷贝的问题"><a href="#深拷贝的问题" class="headerlink" title="深拷贝的问题"></a>深拷贝的问题</h3><p>针对深拷贝,需要使用其他办法,因为 <code>Object.assign()</code>拷贝的是(可枚举)属性值。</p><p>假如源值是一个对象的引用,它仅仅会复制其引用值。</p><pre><code class="js">const log = console.log;function test() { 'use strict'; let obj1 = { a: 0 , b: { c: 0}}; let obj2 = Object.assign({}, obj1); log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj1.a = 1; log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj2.a = 2; log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}} obj2.b.c = 3; log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}} log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}} // Deep Clone obj1 = { a: 0 , b: { c: 0}}; let obj3 = JSON.parse(JSON.stringify(obj1)); obj1.a = 4; obj1.b.c = 4; log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}}test();</code></pre>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p>来源<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Object</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之原型继承及原型链</title>
<link href="https://shinichikudo-fe.github.io/2021/01/06/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E5%8E%9F%E5%9E%8B%E7%BB%A7%E6%89%BF%E5%8F%8A%E5%8E%9F%E5%9E%8B%E9%93%BE/"/>
<id>https://shinichikudo-fe.github.io/2021/01/06/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8B%E5%8E%9F%E5%9E%8B%E7%BB%A7%E6%89%BF%E5%8F%8A%E5%8E%9F%E5%9E%8B%E9%93%BE/</id>
<published>2021-01-06T01:45:25.000Z</published>
<updated>2021-01-06T03:17:02.004Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><h2 id="原型链"><a href="#原型链" class="headerlink" title="原型链"></a>原型链</h2><p>JavaScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是<strong>利用原型让一个引用类型继承另一个引用类型的属性和方法</strong>。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针</p><p><img src="https://segmentfault.com/img/bVvUCD" alt="原型,构造函数,实例"></p><p>那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。</p><p>ECMAScript中无法实现接口继承.ECMAScript只支持<code>实现继承</code>,而且其 <strong>实现继承 主要是依靠原型链来实现的</strong>.</p><p>使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢? 方法一般有两种.</p><blockquote><p>第一种是使用 <code>instanceof</code> 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 以下几行代码就说明了这点.</p></blockquote><pre><code class="js">alert(instance instanceof Object);//truealert(instance instanceof Father);//truealert(instance instanceof Son);//true</code></pre><blockquote><p>第二种是使用 <code>isPrototypeOf()</code> 方法, 同样只要是原型链中出现过的原型,<code>isPrototypeOf()</code> 方法就会返回true, 如下所示.</p></blockquote><pre><code class="js">alert(Object.prototype.isPrototypeOf(instance));//truealert(Father.prototype.isPrototypeOf(instance));//truealert(Son.prototype.isPrototypeOf(instance));//true</code></pre><h2 id="继承"><a href="#继承" class="headerlink" title="继承"></a>继承</h2><h3 id="原型链的问题"><a href="#原型链的问题" class="headerlink" title="原型链的问题"></a>原型链的问题</h3><p>原型链并非十分完美, 它包含如下两个问题.</p><blockquote><p>问题一: 当原型链中包含引用类型值的原型时,该引用类型值<strong>会被所有实例共享</strong>;<br>问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.</p></blockquote><h3 id="借用构造函数"><a href="#借用构造函数" class="headerlink" title="借用构造函数"></a>借用构造函数</h3><p>为解决原型链中上述两个问题, 我们开始使用一种叫做<strong>借用构造函数</strong>(constructor stealing)的技术(也叫经典继承).</p><blockquote><p>基本思想:即在子类型构造函数的内部调用超类型构造函数.</p></blockquote><pre><code class="js">function Father(){ this.colors = ["red","blue","green"];}function Son(){ Father.call(this);//继承了Father,且向父类型传递参数}var instance1 = new Son();instance1.colors.push("black");console.log(instance1.colors);//"red,blue,green,black"var instance2 = new Son();console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的</code></pre><p>很明显,借用构造函数一举解决了原型链的两大问题:</p><p>其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;<br>其二, 子类型创建时也能够向父类型传递参数.</p><p>随之而来的是, 如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题–<strong>方法都在构造函数中定义</strong>, 因此函数复用也就不可用了.而且超类型(如Father)中定义的方法,对子类型而言也是不可见的. 考虑此,借用构造函数的技术也很少单独使用.</p><h3 id="组合继承"><a href="#组合继承" class="headerlink" title="组合继承"></a>组合继承</h3><p>组合继承, 有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式.</p><blockquote><p>基本思路: 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.</p></blockquote><p>这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性. 如下所示.</p><pre><code class="js">function Father(name){ this.name = name; this.colors = ["red","blue","green"];}Father.prototype.sayName = function(){ alert(this.name);};function Son(name,age){ Father.call(this,name);//继承实例属性,第一次调用Father() this.age = age;}Son.prototype = new Father();//继承父类方法,第二次调用Father()Son.prototype.sayAge = function(){ alert(this.age);}var instance1 = new Son("louis",5);instance1.colors.push("black");console.log(instance1.colors);//"red,blue,green,black"instance1.sayName();//louisinstance1.sayAge();//5var instance1 = new Son("zhai",10);console.log(instance1.colors);//"red,blue,green"instance1.sayName();//zhaiinstance1.sayAge();//10</code></pre><p>组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式. 而且, instanceof 和 isPrototypeOf( )也能用于识别基于组合继承创建的对象.<br>同时我们还注意到组合继承其实调用了两次父类构造函数, 造成了不必要的消耗, 那么怎样才能避免这种不必要的消耗呢, 这个我们将在后面讲到.</p><h3 id="原型继承"><a href="#原型继承" class="headerlink" title="原型继承"></a>原型继承</h3><blockquote><p>在<code>object()</code>函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.</p></blockquote><pre><code class="js">function object(o){ function F(){} F.prototype = o; return new F();}</code></pre><p>从本质上讲, <code>object()</code> 返回了一个引用传入对象的新对象. 这样<strong>可能带来一些共享数据的问题</strong>,如下.</p><pre><code class="js">var person = { friends : ["Van","Louis","Nick"]};var anotherPerson = object(person);anotherPerson.friends.push("Rob");var yetAnotherPerson = object(person);yetAnotherPerson.friends.push("Style");alert(person.friends);//"Van,Louis,Nick,Rob,Style"</code></pre><p>在 ECMAScript5 中,通过新增 <code>object.create()</code> 方法规范化了上面的原型式继承.</p><p><code>object.create()</code> 接收两个参数:</p><ul><li>一个用作新对象原型的对象</li><li>(可选的)一个为新对象定义额外属性的对象</li></ul><pre><code class="js">var person = { name : "Van"};var anotherPerson = Object.create(person, { name : { value : "Louis" }});alert(anotherPerson.name);//"Louis"</code></pre><h3 id="寄生式继承"><a href="#寄生式继承" class="headerlink" title="寄生式继承"></a>寄生式继承</h3><blockquote><p>寄生式继承的思路与(寄生)构造函数和工厂模式类似, 即<strong>创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象</strong>. 如下.</p></blockquote><pre><code class="js">function createAnother(original){ var clone = object(original);//通过调用object函数创建一个新对象 clone.sayHi = function(){//以某种方式来增强这个对象 alert("hi"); }; return clone;//返回这个对象}</code></pre><p>这个例子中的代码基于person返回了一个新对象–<code>anotherPerson</code>. 新对象不仅具有 <code>person</code> 的所有属性和方法, 而且还被增强了, 拥有了<code>sayH()</code>方法.</p><p><strong>注意</strong>: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.</p><h3 id="寄生组合式继承"><a href="#寄生组合式继承" class="headerlink" title="寄生组合式继承"></a>寄生组合式继承</h3><p>前面讲过,组合继承是 <code>JavaScript</code> 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用<strong>两次父类构造函数</strong>;</p><p>一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部. </p><p><strong>寄生组合式继承就是为了降低调用父类构造函数的开销而出现的 .</strong></p><blockquote><p>其背后的基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数</p></blockquote><pre><code class="js">function extend(subClass,superClass){ var prototype = object(superClass.prototype);//创建对象 prototype.constructor = subClass;//增强对象 subClass.prototype = prototype;//指定对象}</code></pre><p><code>extend</code>的高效率体现在它没有调用<code>superClass</code>构造函数,因此避免了在<code>subClass.prototype</code>上面创建不必要,多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 <code>instanceof</code> 和 <code>isPrototypeOf() </code>方法.</p><p>下面我们来看下<code>extend</code>的另一种更为有效的扩展.</p><pre><code class="js">function extend(subClass, superClass) { var F = function() {}; F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass; subClass.superclass = superClass.prototype; if(superClass.prototype.constructor == Object.prototype.constructor) { superClass.prototype.constructor = superClass; }}</code></pre><p>我一直不太明白的是为什么要 “new F()”, 既然extend的目的是将子类型的 <code>prototype</code> 指向超类型的<code>prototype</code>,为什么不直接做如下操作呢?</p><pre><code class="js">subClass.prototype = superClass.prototype;//直接指向超类型prototype</code></pre><p>显然, 基于如上操作, 子类型原型将与超类型原型共用, 根本就没有继承关系.</p><p>那么最终,原型链继承可以这么实现,例如:</p><pre><code class="js">function Father(name){ this.name = name; this.colors = ["red","blue","green"];}Father.prototype.sayName = function(){ alert(this.name);};function Son(name,age){ Father.call(this,name);//继承实例属性,第一次调用Father() this.age = age;}extend(Son,Father)//继承父类方法,此处并不会第二次调用Father()Son.prototype.sayAge = function(){ alert(this.age);}var instance1 = new Son("louis",5);instance1.colors.push("black");console.log(instance1.colors);//"red,blue,green,black"instance1.sayName();//louisinstance1.sayAge();//5var instance1 = new Son("zhai",10);console.log(instance1.colors);//"red,blue,green"instance1.sayName();//zhaiinstance1.sayAge();//10</code></pre>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<h2 id="原型链"><a href="#原型链" class="headerlink" title="原型链"></a>原型链</h2><p>JavaScript 中描述了原型链的</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之new 与构造函数,instanceof 与实例</title>
<link href="https://shinichikudo-fe.github.io/2021/01/05/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8Bnew-%E4%B8%8E%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%EF%BC%8Cinstanceof-%E4%B8%8E%E5%AE%9E%E4%BE%8B/"/>
<id>https://shinichikudo-fe.github.io/2021/01/05/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8Bnew-%E4%B8%8E%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%EF%BC%8Cinstanceof-%E4%B8%8E%E5%AE%9E%E4%BE%8B/</id>
<published>2021-01-05T07:42:16.000Z</published>
<updated>2021-01-05T09:00:46.607Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><h2 id="构造函数"><a href="#构造函数" class="headerlink" title="构造函数"></a>构造函数</h2><p>在Javascript中,万物皆对象,如果需要在JavaScript中生成一个对象,而对象是单个实物的抽象,通常需要一个模版,表示某一类实物的共同特征,然后根据这个模板生成</p><p>在Javascript中构造函数就称为模板。一个构造函数可以生成多个实例对象,这些实例对象都有相同的结构,构造函数名字的<strong>第一个字母通常大写</strong></p><p>构造函数有两个特点:</p><blockquote><p>函数内部使用了this关键字,代表了所要生成的对象实例<br>生成对象的时候,必须使用new命令</p></blockquote><h2 id="new"><a href="#new" class="headerlink" title="new"></a>new</h2><p>new命令的作用,就是执行构造函数,返回一个实例对象。</p><p>new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示这里是函数调用,推荐使用括号。</p><p>如果忘了使用new命令,直接调用构造函数会发生什么事?</p><p>这种情况下,<strong>构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果</strong>。</p><h3 id="new的原理"><a href="#new的原理" class="headerlink" title="new的原理"></a>new的原理</h3><blockquote><ol><li>创建一个空对象,作为将要返回的对象实例。</li><li>将这个空对象的原型,指向构造函数的prototype属性。</li><li>将这个空对象赋值给函数内部的this关键字。</li><li>开始执行构造函数内部的代码。</li></ol></blockquote><h2 id="实例"><a href="#实例" class="headerlink" title="实例"></a>实例</h2><p>实例就是通过构造函数创建出来的对象</p><pre><code class="js">var M = function(){ this.name = 'Jane';}var obj = new M();</code></pre><h2 id="原型链"><a href="#原型链" class="headerlink" title="原型链"></a>原型链</h2><p><code>instanceof</code>表示的就是一种继承关系,或者原型链的结构。</p><p>由于所有的对象的原型链都会找到<code>Object.prototype</code>,因此所有的对象都会有<code>Object.prototype</code>的方法。这就是所谓的“继承”</p><h2 id="instanceof"><a href="#instanceof" class="headerlink" title="instanceof"></a>instanceof</h2><pre><code class="js">function instance_of(L, R) {//L 表示左表达式,R 表示右表达式 var O = R.prototype;// 取 R 的显示原型 L = L.__proto__;// 取 L 的隐式原型 while (true) { if (L === null) return false; if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true return true; L = L.__proto__; } }</code></pre><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>创建一个新的对象可以通过<code>new</code>关键字 + 构造函数 来生成新的实例对象,通过<code>instanceof</code> 可以找到实例的构造函数的原型</p><p><code>instanceof</code>找到的是实例在原型链中所有的构造函数,不容易找到直接创建实例的构造函数;</p><p><code>constructor</code>找到的是构造函数只有一个,就是直接创建这个实例的构造函数,所以用<code>constructor</code>找实例的构造函数更严谨。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<h2 id="构造函数"><a href="#构造函数" class="headerlink" title="构造函数"></a>构造函数</h2><p>在Javascript中,万物</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
<entry>
<title>如何在React中完美运用?</title>
<link href="https://shinichikudo-fe.github.io/2020/12/28/React/%E5%A6%82%E4%BD%95%E5%9C%A8React%E4%B8%AD%E5%AE%8C%E7%BE%8E%E8%BF%90%E7%94%A8%EF%BC%9F/"/>
<id>https://shinichikudo-fe.github.io/2020/12/28/React/%E5%A6%82%E4%BD%95%E5%9C%A8React%E4%B8%AD%E5%AE%8C%E7%BE%8E%E8%BF%90%E7%94%A8%EF%BC%9F/</id>
<published>2020-12-28T08:27:03.000Z</published>
<updated>2020-12-28T10:12:52.391Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://juejin.cn/post/6910863689260204039">原文地址</a></p><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>其实如果运用熟练的话,TS 只是在第一次开发的时候稍微多花一些时间去编写类型,后续维护、重构的时候就会发挥它神奇的作用了,还是非常推荐长期维护的项目使用它的。</p><h2 id="前置基础"><a href="#前置基础" class="headerlink" title="前置基础"></a>前置基础</h2><p>阅读本文的前提条件是:</p><p>熟悉 React 的使用。<br>熟悉 TypeScript 中的类型知识。<br>本文会侧重使用 React Hook 作为示例,当然大部分类型知识都是通用的。</p><p>也就是说,这篇文章侧重点在于 「<strong>React 和 TypeScript 的结合」</strong>,而不是基础知识,基础知识阅读文档即可学习。</p><h2 id="组件-Props"><a href="#组件-Props" class="headerlink" title="组件 Props"></a>组件 Props</h2><p>先看几种定义 Props 经常用到的类型:</p><h3 id="基础类型"><a href="#基础类型" class="headerlink" title="基础类型"></a>基础类型</h3><pre><code class="js">type BasicProps = { message: string; count: number; disabled: boolean; /** 数组类型 */ names: string[]; /** 用「联合类型」限制为下面两种「字符串字面量」类型 */ status: "waiting" | "success";};</code></pre><h3 id="对象类型"><a href="#对象类型" class="headerlink" title="对象类型"></a>对象类型</h3><pre><code class="js">type ObjectOrArrayProps = { /** 如果你不需要用到具体的属性 可以这样模糊规定是个对象 ❌ 不推荐 */ obj: object; obj2: {}; // 同上 /** 拥有具体属性的对象类型 ✅ 推荐 */ obj3: { id: string; title: string; }; /** 对象数组 😁 常用 */ objArr: { id: string; title: string; }[]; /** key 可以为任意 string,值限制为 MyTypeHere 类型 */ dict1: { [key: string]: MyTypeHere; }; dict2: Record<string, MyTypeHere>; // 基本上和 dict1 相同,用了 TS 内置的 Record 类型。}</code></pre><h3 id="函数类型"><a href="#函数类型" class="headerlink" title="函数类型"></a>函数类型</h3><pre><code class="js">type FunctionProps = { /** 任意的函数类型 ❌ 不推荐 不能规定参数以及返回值类型 */ onSomething: Function; /** 没有参数的函数 不需要返回值 😁 常用 */ onClick: () => void; /** 带函数的参数 😁 非常常用 */ onChange: (id: number) => void; /** 另一种函数语法 参数是 React 的按钮事件 😁 非常常用 */ onClick(event: React.MouseEvent<HTMLButtonElement>): void; /** 可选参数类型 😁 非常常用 */ optional?: OptionalType;}</code></pre><h3 id="React-相关类型"><a href="#React-相关类型" class="headerlink" title="React 相关类型"></a>React 相关类型</h3><pre><code class="js">export declare interface AppProps { children1: JSX.Element; // ❌ 不推荐 没有考虑数组 children2: JSX.Element | JSX.Element[]; // ❌ 不推荐 没有考虑字符串 children children4: React.ReactChild[]; // 稍微好点 但是没考虑 null children: React.ReactNode; // ✅ 包含所有 children 情况 functionChildren: (name: string) => React.ReactNode; // ✅ 返回 React 节点的函数 style?: React.CSSProperties; // ✅ 推荐 在内联 style 时使用 // ✅ 推荐原生 button 标签自带的所有 props 类型 // 也可以在泛型的位置传入组件 提取组件的 Props 类型 props: React.ComponentProps<"button">; // ✅ 推荐 利用上一步的做法 再进一步的提取出原生的 onClick 函数类型 // 此时函数的第一个参数会自动推断为 React 的点击事件类型 onClickButton:React.ComponentProps<"button">["onClick"]}</code></pre><h3 id="函数式组件"><a href="#函数式组件" class="headerlink" title="函数式组件"></a>函数式组件</h3><p>最简单的:</p><pre><code class="js">interface AppProps = { message: string };const App = ({ message }: AppProps) => <div>{message}</div>;</code></pre><p>包含 <code>children</code> 的:</p><p>利用 <code>React.FC</code> 内置类型的话,不光会包含你定义的 <code>AppProps</code> 还会自动加上一个 <code>children</code> 类型,以及其他组件上会出现的类型:</p><pre><code class="js">// 等同于AppProps & { children: React.ReactNode propTypes?: WeakValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string;}// 使用interface AppProps = { message: string };const App: React.FC<AppProps> = ({ message, children }) => { return ( <> {children} <div>{message}</div> </> )};</code></pre><h2 id="Hooks"><a href="#Hooks" class="headerlink" title="Hooks"></a>Hooks</h2><p><code>@types/react</code> 包在 16.8 以上的版本开始对 Hooks 的支持。</p><h3 id="useState"><a href="#useState" class="headerlink" title="useState"></a>useState</h3><p>如果你的默认值已经可以说明类型,那么不用手动声明类型,交给 TS 自动推断即可:</p><pre><code class="js">// val: booleanconst [val, toggle] = React.useState(false);toggle(false)toggle(true)</code></pre><p>如果初始值是 <code>null</code> 或 <code>undefined</code>,那就要通过泛型手动传入你期望的类型。</p><pre><code class="js">const [user, setUser] = React.useState<IUser | null>(null);// later...setUser(newUser);</code></pre><p>这样也可以保证在你直接访问 <code>user</code> 上的属性时,提示你它有可能是 <code>null</code>。</p><p>通过 <code>optional-chaining</code> 语法(TS 3.7 以上支持),可以避免这个错误。</p><pre><code class="js">// ✅ okconst name = user?.name</code></pre><h3 id="useReducer"><a href="#useReducer" class="headerlink" title="useReducer"></a>useReducer</h3><p>需要用 <a href="https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions">Discriminated Unions</a> 来标注 Action 的类型。</p><pre><code class="js">const initialState = { count: 0 };type ACTIONTYPE = | { type: "increment"; payload: number } | { type: "decrement"; payload: string };function reducer(state: typeof initialState, action: ACTIONTYPE) { switch (action.type) { case "increment": return { count: state.count + action.payload }; case "decrement": return { count: state.count - Number(action.payload) }; default: throw new Error(); }}function Counter() { const [state, dispatch] = React.useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: "decrement", payload: "5" })}> - </button> <button onClick={() => dispatch({ type: "increment", payload: 5 })}> + </button> </> );}</code></pre><p>「<code>Discriminated Unions</code>」一般是一个联合类型,其中每一个类型都需要通过类似 type 这种特定的字段来区分,当你传入特定的 type 时,剩下的类型 payload 就会自动匹配推断。</p><p>这样:</p><ul><li>当你写入的 <code>type</code> 匹配到 <code>decrement</code> 的时候,TS 会自动推断出相应的 payload 应该是 string 类型。</li><li>当你写入的 <code>type</code> 匹配到 <code>increment</code> 的时候,则 payload 应该是 number 类型。</li></ul><p>这样在你 dispatch 的时候,输入对应的 <code>type</code>,就自动提示你剩余的参数类型啦。</p><h3 id="useEffect"><a href="#useEffect" class="headerlink" title="useEffect"></a>useEffect</h3><p>这里主要需要注意的是,<code>useEffect</code> 传入的函数,它的返回值要么是一个方法(清理函数),要么就是<code>undefined</code>,其他情况都会报错。</p><p>比较常见的一个情况是,我们的 <code>useEffect</code> 需要执行一个 <code>async</code> 函数,比如:</p><pre><code class="js">// ❌ // Type 'Promise<void>' provides no match // for the signature '(): void | undefined'useEffect(async () => { const user = await getUser() setUser(user)}, [])</code></pre><p>虽然没有在 <code>async</code> 函数里显式的返回值,但是 <code>async</code> 函数默认会返回一个 <code>Promise</code>,这会导致 TS 的报错。</p><p>推荐这样改写:</p><pre><code class="js">useEffect(() => { const getUser = async () => { const user = await getUser() setUser(user) } getUser()}, [])</code></pre><p>或者用自执行函数?不推荐,可读性不好。</p><pre><code class="js">useEffect(() => { (async () => { const user = await getUser() setUser(user) })()}, [])</code></pre><h3 id="useRef"><a href="#useRef" class="headerlink" title="useRef"></a>useRef</h3><p>这个 <code>Hook</code> 在很多时候是没有初始值的,这样可以声明返回对象中 <code>current</code> 属性的类型:</p><pre><code class="js">const ref2 = useRef<HTMLElement>(null);</code></pre><p>以一个按钮场景为例:</p><pre><code class="js">function TextInputWithFocusButton() { const inputEl = React.useRef<HTMLInputElement>(null); const onButtonClick = () => { if (inputEl && inputEl.current) { inputEl.current.focus(); } }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> );}</code></pre><p>当 <code>onButtonClick</code> 事件触发时,可以肯定 <code>inputEl</code> 也是有值的,因为组件是同级别渲染的,但是还是依然要做冗余的非空判断。</p><p><strong>有一种办法可以绕过去。</strong></p><pre><code class="js">const ref1 = useRef<HTMLElement>(null!);</code></pre><p>null! 这种语法是非空断言,跟在一个值后面表示你断定它是有值的,所以在你使用 <code>inputEl.current.focus()</code> 的时候,TS 不会给出报错。</p><p>但是这种语法比较危险,需要尽量减少使用。</p><p>在绝大部分情况下,<code>inputEl.current?.focus()</code> 是个更安全的选择,除非这个值真的不可能为空。(比如在使用之前就赋值了)</p><h3 id="useImperativeHandle"><a href="#useImperativeHandle" class="headerlink" title="useImperativeHandle"></a>useImperativeHandle</h3><p>推荐使用一个自定义的 <code>innerRef</code> 来代替原生的 <code>ref</code>,否则要用到 <code>forwardRef</code> 会搞的类型很复杂。</p><pre><code class="js">type ListProps = { innerRef?: React.Ref<{ scrollToTop(): void }>}function List(props: ListProps) { useImperativeHandle(props.innerRef, () => ({ scrollToTop() { } })) return null}</code></pre><p>结合刚刚 <code>useRef</code> 的知识,使用是这样的:</p><pre><code class="js">function Use() { const listRef = useRef<{ scrollToTop(): void }>(null!) useEffect(() => { listRef.current.scrollToTop() }, []) return ( <List innerRef={listRef} /> )}</code></pre><p>很完美,是不是?</p><p>可以在线调试 <a href="https://www.typescriptlang.org/play#code/JYWwDg9gTgLgBAKjgQwM5wEoFNkGN4BmUEIcA5FDvmQNwCwAUKJLHAN5wCuqWAyjMhhYANFx4BRAgSz5R3LNgJyeASXBYog4ADcsACWQA7ACYAbLHAC+cIiXKU8MWo0YwAnmAsAZYKhgAFYjB0AF52Rjg4YENDDUUAfgAuTCoYADpFAB4OVFxiU1MAFQhisAAKAEpk7QhgYysAPkZLFwYCTkN8YAhDOB8-MrAg1GT+gOGK8IZI+TVPTRgdfSMzLEHhtOjYqEVRSrgQhrgytgjIuFz8opKIcsmOFumrCoqzyhhOKF7DTgLm1vanUWPTgAFUePtTk9cD0-HBTL4YIoDmIFFgCNkLnkIAViqVKtVavVLA0yj8CgBCV4MM7ySTSfBlfaHKbneGIxRpXCfSiGdKXHHXfHUyKWUQAbQAutS3lgPl9jmdIpkxlEYnF0SE2Ai-IprAB6JpPamWIA">useImperativeHandle</a> 的例子。</p><h3 id="自定义-Hook"><a href="#自定义-Hook" class="headerlink" title="自定义 Hook"></a>自定义 Hook</h3><p>如果你想仿照 useState 的形式,返回一个数组给用户使用,一定要记得在适当的时候使用 <code>as const</code>,标记这个返回值是个常量,告诉 TS 数组里的值不会删除,改变顺序等等……</p><p>否则,你的每一项都会被推断成是「所有类型可能性的联合类型」,这会影响用户使用。</p><pre><code class="js">export function useLoading() { const [isLoading, setState] = React.useState(false); const load = (aPromise: Promise<any>) => { setState(true); return aPromise.finally(() => setState(false)); }; // ✅ 加了 as const 会推断出 [boolean, typeof load] // ❌ 否则会是 (boolean | typeof load)[] return [isLoading, load] as const;[]}</code></pre><p>对了,如果你在用 <code>React Hook</code> 写一个库,别忘了把类型也导出给用户使用。</p><h2 id="React-API"><a href="#React-API" class="headerlink" title="React API"></a>React API</h2><h3 id="forwardRef"><a href="#forwardRef" class="headerlink" title="forwardRef"></a>forwardRef</h3><p>函数式组件默认不可以加 ref,它不像类组件那样有自己的实例。这个 API 一般是函数式组件用来接收父组件传来的 ref。</p><p>所以需要标注好实例类型,也就是父组件通过 ref 可以拿到什么样类型的值。</p><pre><code class="js">type Props = { };export type Ref = HTMLButtonElement;export const FancyButton = React.forwardRef<Ref, Props>((props, ref) => ( <button ref={ref} className="MyClassName"> {props.children} </button>));</code></pre><p>由于这个例子里直接把 ref 转发给 button 了,所以直接把类型标注为 <code>HTMLButtonElement</code> 即可。</p><p>父组件这样调用,就可以拿到正确类型:</p><pre><code class="js">export const App = () => { const ref = useRef<HTMLButtonElement>() return ( <FancyButton ref={ref} /> )}</code></pre>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://juejin.cn/post/6910863689260204039">原文地址</a></p>
<h1 id="前言"><a href="#前言</summary>
<category term="React" scheme="https://shinichikudo-fe.github.io/categories/React/"/>
<category term="React" scheme="https://shinichikudo-fe.github.io/tags/React/"/>
</entry>
<entry>
<title>谈谈前端CodeReview,埋点和监控</title>
<link href="https://shinichikudo-fe.github.io/2020/12/23/%E6%9D%82%E6%96%87/CodeReview-%E5%9F%8B%E7%82%B9-%E7%9B%91%E6%8E%A7/"/>
<id>https://shinichikudo-fe.github.io/2020/12/23/%E6%9D%82%E6%96%87/CodeReview-%E5%9F%8B%E7%82%B9-%E7%9B%91%E6%8E%A7/</id>
<published>2020-12-23T07:09:49.000Z</published>
<updated>2020-12-23T07:54:28.370Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://juejin.cn/post/6860365929927639047">原文地址(codereview)</a><br><a href="https://juejin.cn/post/6872583452160425997">原文地址(埋点和监控)</a></p><h1 id="代码review"><a href="#代码review" class="headerlink" title="代码review"></a>代码review</h1><h2 id="现状分析和意义"><a href="#现状分析和意义" class="headerlink" title="现状分析和意义"></a>现状分析和意义</h2><p>大多数开发人员,所认知的代码review,就是把代码拿出来给别人看,大家讨论讨论,脑子里面没有特别深刻的概念。</p><p>其实代码review所涉及的东西包括:<code>review的工具、review的规范、review的流程、review的方式、甚至人员参与度、时间把控</code>等等。</p><p>代码review的意义,<strong>当然就是通过流程管理,尽可能的减少线上问题</strong>。对于TL,通过这样的过程管理,能够对项目质量把控。</p><p>当然它也有一定的<strong>缺点</strong>,增加了软件开发的流程,一定程度上降低了效率。(不过这是假设我们的项目都是高质量的,如果项目的质量本身存在问题,等到线上修复的时候,代价会大的多)</p><h2 id="一些软性的见解"><a href="#一些软性的见解" class="headerlink" title="一些软性的见解"></a>一些软性的见解</h2><ul><li>1、代码review的环境塑造</li></ul><p>代码review是一个交流的过程,如果代码写的好,那就是展示自我的平台,向大家分享优秀的实践。<br>如果代码有问题,也没有关系,发现问题,并且解决问题。<br>一般而言,TL在这个过程的态度很重要,如果TL以一个批评的口气来说谁谁谁有问题,那天然的大家会对这件事起反感。<br>如果TL能在这个过程,一直输出一些优秀的思维方式,代码架构方式,其实是给底下人传授经验的过程,大家感觉有收获,自然会喜欢review。<br>特别的要注意态度的问题,人的情绪是非常敏感的,如果团队有人说出类似“这行代码真垃圾”这样的话,TL关注的点一定不能说这个代码的问题,而是团队中的某些人的语气问题了。<br>探讨别人的代码的时候,可以说,这行代码可以怎么怎么实现,表达自己的认知就行了,不要表达特殊(不好)情绪。<br>个人反而非常喜欢赞赏别人,比如代码量多,可以说别人这段时间幸苦了。逻辑复杂,可以说这个确实耗费了不少心力之类的话。</p><ul><li>2、代码review——如何推动</li></ul><p>其实人总是不想把自己的东西暴露出来,类似我写的代码,就是我的东西的感觉。<br>在推动代码review的时候,要从两方面下手,相辅相成。</p><p><strong>一方面是价值宣导,让大家认可这件事,认可代码review的重要性,另一方面是需要一定的强制性,因为这个是团队代码质量的需要,也是公司业务稳定性的</strong>需要。</p><p>依照个人经验,价值观宣导,可以从行业内大厂的做法,以及必要性,甚至团队成员的职业发展上说明,大概率是可以让大家认可的。<br>每次遇到重大问题的时候,其实也是机会,就看我们会不会利用,假如出现线上问题,提出代码review的事情,大家应该会接受。</p><ul><li>3、代码review——不忘初心</li></ul><p>时刻谨记,我们的目的是什么。<br>不要改一行样式代码,就要进行一次review。因为这个东西本身没有什么探讨的价值。<br>一般而言,代码量超过一定量的时候,其业务复杂度,代码的设计都是可以进行探讨的。</p><h2 id="review的规范"><a href="#review的规范" class="headerlink" title="review的规范"></a>review的规范</h2><p>review的规范,应该结合团队具体的情况,举行施行。</p><p>不过还是有一些共性的东西:比如review的要点有那些?review的参与人都有那些?review的粒度是什么?</p><h3 id="review的要点"><a href="#review的要点" class="headerlink" title="review的要点"></a>review的要点</h3><ul><li><p>1、代码格式是否符合规范<br>这一部分包括lint规范、命名规范等。lint规范一般使用工具可以解决,但是命名是否规范需要我们去审查。</p></li><li><p>2、代码的可读性<br>是不是有深层的if else嵌套。<br>是不是有难以理解的函数、或者一个函数过于长。</p></li><li><p>3、边界问题<br>是不是有开发人员没有想到的异常情况,这一般是和具体的业务场景相关。</p></li><li><p>4、代码架构<br>代码的组织方式,是不是有调整的空间。是不是有可复用的代码,提取出来?</p></li></ul><h3 id="参与人员"><a href="#参与人员" class="headerlink" title="参与人员"></a>参与人员</h3><p>如果比较小的团队,比如三四个人,大家做的东西,应该都比较了解,可以全员参与<br>如果比较大型的团队,其实也是按照所熟悉的业务,分为不同的方向,相关人员可以参与reivew。<br>不过具体执行层面,需要具体问题,具体分析。</p><h3 id="review的粒度"><a href="#review的粒度" class="headerlink" title="review的粒度"></a>review的粒度</h3><p>个人认为,一些简单的改动,是不需要代码review的。<br>但是团队成员的管理上,需要一些硬性规定,那么可以把代码量超过500行,算作一个临界点。</p><h3 id="review的工具"><a href="#review的工具" class="headerlink" title="review的工具"></a>review的工具</h3><p>review是一个系统的工程,有参与的人员、有相关规范,一定也有工具。<br>个人实践上,在阿里内部,其实有类似的工具。<br>但是考虑大多数公司可能没有自己确定的工具,那么个人推荐开源工具<a href="http://www.reviewboard.org.cn/">reviewboard</a>。</p><h1 id="什么是一个埋点系统"><a href="#什么是一个埋点系统" class="headerlink" title="什么是一个埋点系统"></a>什么是一个埋点系统</h1><p><strong>埋点系统 = 埋点SDK + 埋点可视化平台 + 埋点接入方式</strong></p><p>埋点SDK,解决的问题,就是埋点的一些通用的功能的实现,比如统计用户的UV、PV等等。它在内部主要通过向埋点服务后台发送相关的数据。它对外暴露一套统一个api,方便我们接入。<br>埋点可视化平台,解决的问题,就是当我这些埋点数据被上报之后,我能看到相关的数据。比如今天的UV、PV是多少,相比昨天的环比增长是多少等等。</p><p>埋点接入方式,这是埋点系统对外开放的能力,一般会需要申请一个appid,这个appid是唯一的,然后每次上报埋点的时候,就知道是哪个项目的数据了。</p><p>更详细一点的:<br>上面的说法比较笼统,比如埋点SDK,定义的更详细一点,本质上它就是一个js文件,这个js文件是由开发商提供的,当然我们也可以做自己的埋点SDK。<br>之所以称之为SDK,还有一层原因,就是SDK基本上是不变的,就和我们开发Vue项目一样,我们需要引入vue.js文件。我们不需要知道vue.js文件怎么实现的,我们只需要知道它提供的api是什么就行。</p><p>SDK内部所做的事情,就是上报数据,所谓的上报数据,无非就是发送一个http请求接口,发送到后台服务器。然后存入数据库中。<br>埋点可视化平台,就是从数据库中,把这些数据读取出来,展示出来而已。更深层次的,无非就是做数据分析,转化率分析之类。</p><h2 id="埋点和监控是两回事"><a href="#埋点和监控是两回事" class="headerlink" title="埋点和监控是两回事"></a>埋点和监控是两回事</h2><p>好多同学把埋点和监控混为一谈,其实在大厂内部,他们完全是不一样的。</p><ul><li><strong>从功能上说:</strong></li></ul><p>监控主要是进行JS错误监控、接口异常监控、页面性能监控、资源加载异常、以及一些自定义的异常。而埋点主要做的事情是数据上报,数据统计,数据分析。</p><ul><li><strong>从使用的角度来说:</strong></li></ul><p>监控能准确的定位到错误,不需要我们在代码中添加额外的东西,只需要引入监控的SDK,就能实现错误的监控。<br>而埋点更多的是业务行为,我们在业务层面,得自己决定,自己想知道什么数据,想要怎么埋点,然后再做什么样的分析,所以我们接入埋点SDK之后,还需要调用埋点的api进行数据上报。</p><p>所以一定要明确,埋点和监控是两回事。</p><h2 id="埋点和监控的实现是有差异的"><a href="#埋点和监控的实现是有差异的" class="headerlink" title="埋点和监控的实现是有差异的"></a>埋点和监控的实现是有差异的</h2><p>这里所说的实现,是<strong>指这个相关的SDK的实现原理</strong>。</p><ul><li><strong>埋点的实现原理是什么?</strong></li></ul><p>埋点的本质是数据上报,那一定离不开这么一些纬度的数据:地理位置、页面路径、浏览器信息、userId、时间戳等等。类似这样的信息,在用户上报的时候,应该都是自动携带的。<br>一些上报的数据,可能会和业务相关,提供的埋点api中,加一条类似业务标识的ID,让相关数据关联起来。</p><ul><li><strong>监控的实现原理是什么?</strong></li></ul><p>其实无非在SDK内部,实现几种异常监控。</p><p>JS错误,可以使用<code>window.onerror</code>的方式监控。</p><p>接口异常,现在的请求一般是<code>fetch或者是ajax请求</code>,我们只需要把他们包装一层,在相应的响应事件中,获取我们的信息,如果返回的不是200的请求,直接上报错误。</p><p>页面性能,主要通过浏览器提供的一个对象来实现,<code>Performance相关的api</code>,它记录了浏览器加载、解析资源的时间占用情况。</p><p>资源加载错误,最简单的方式,可以通过<code>onerror</code>事件来实现。script可以用,img标签都可以用。<br>这些表述,比较笼统,比如js错误里面,关于promise、关于vue的错误,在错误捕获上都需要做补充。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://juejin.cn/post/6860365929927639047">原文地址(codereview)</a><br><a href="http</summary>
<category term="杂文" scheme="https://shinichikudo-fe.github.io/categories/%E6%9D%82%E6%96%87/"/>
<category term="CodeReview" scheme="https://shinichikudo-fe.github.io/tags/CodeReview/"/>
</entry>
<entry>
<title>GraphQL vs Restful Api</title>
<link href="https://shinichikudo-fe.github.io/2020/11/18/GraphQL/GraphQL-vs-Restful-Api/"/>
<id>https://shinichikudo-fe.github.io/2020/11/18/GraphQL/GraphQL-vs-Restful-Api/</id>
<published>2020-11-18T09:07:34.000Z</published>
<updated>2020-11-19T03:50:42.329Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://cloud.tencent.com/developer/news/572892">原文地址</a></p><h2 id="与传统-REST-API-相比,GraphQL-提供了哪些优势?"><a href="#与传统-REST-API-相比,GraphQL-提供了哪些优势?" class="headerlink" title="与传统 REST API 相比,GraphQL 提供了哪些优势?"></a>与传统 REST API 相比,GraphQL 提供了哪些优势?</h2><p>我们将讨论 GraphQL 的设计原则,比较 GraphQL 与 REST 中的相同请求,并深入探讨 GraphQL 相对其他架构的优点。</p><p>为理解 GraphQL 作为 API 架构的好处,我们要讨论 API 在<code>客户端 - 服务器</code>结构中的作用。<strong>API(应用程序编程接口)是一个中间层,它允许服务器从客户端接收结构化数据请求,并针对请求的数据发送结构化的响应</strong>。设计 API 架构的方法有很多种。</p><p>让我们来研究一下促使 GraphQL 适合现代化 Web 应用程序的重要基本设计原则。</p><h2 id="GraphQL-服务器的设计原则"><a href="#GraphQL-服务器的设计原则" class="headerlink" title="GraphQL 服务器的设计原则"></a>GraphQL 服务器的设计原则</h2><p> 1.<strong>查询为分层结构</strong>,使用将查询与响应数据1对1匹配的分层和嵌套字段格式。查询和响应的形状类似于树,可查询每个项的其他嵌套字段。在 Facebook 的新闻推送中,这种结构允许一个查询返回一个帖子列表、每个特定帖子的评论,以及每个评论的点赞。</p><p> 2.<strong>该结构以产品为中心,关注前端希望如何接收数据,并构建交付所需的运行时</strong>。这使得 Facebook 的新闻推送可通过一次请求从后端获取需要的所有数据,使服务器按照 GraphQL 的规范从不同的端点获取数据。</p><p> 3.<strong>它使用特定于应用程序的类型系统,该系统使开发人员能在执行前确保查询使用了有效类型,并且语法正确</strong>。例如,新闻推送的 GraphQL 模式要求字段“<code>user</code>”必须包含一个字符串,而“<code>likes</code>”必须包含一个数字。如果查询试图添加不同类型的输入,GraphQL 将在执行查询前抛出一个错误。</p><p> 4.<strong>GraphQL 查询是在客户端指定的,因此,客户端确切知道它将以何种格式接收数据</strong>。这意味着,如果前端请求的格式是包含用户名、评论和点赞的 post 数据,那么来自 Facebook 新闻推送的请求会构建在一个对象中,而不是像其他架构那样构建多个单独的数据块。</p><p> 5.<strong>使用 GraphQL 的服务器结构必须是内省的,或者可由 GraphQL 自己查询</strong>。于是才有了像 <a href="https://github.com/graphql/graphiql">GraphiQL</a> 或 <a href="https://www.apollographql.com/docs/apollo-server/testing/graphql-playground/">GraphQL playground</a> 这样的强大工具。这两种工具都可以让 Facebook 开发人员准确地看到在他们的服务器上使用了哪些查询和字段。</p><h2 id="传统的-RESTful-架构"><a href="#传统的-RESTful-架构" class="headerlink" title="传统的 RESTful 架构"></a>传统的 RESTful 架构</h2><p>REST 架构的设计范式侧重于分配 HTTP 请求方法(<code>GET、POST、PUT、PATCH、DELETE</code>)和 URL 端点之间的关系。</p><p><img src="https://ask.qcloudimg.com/http-save/yehe-7350649/gmtfcrky41.png?imageView2/2/w/1620" alt="restful"></p><p><strong>在 REST 架构中,方法和端点的每个组合得到不同的封装功能</strong>。如果客户端需要的数据特定端点 / 方法不提供,则可能需要额外请求。从 REST 请求返回的数据格式依赖于端点—不能保证这些数据会按照前端需要的方式进行格式化。为使用来自响应的数据(格式与缺省情况下从端点返回的格式不同),必须在客户端编写数据解析和数据操作。</p><p>接下来,让我们看看 GraphQL 规范与 REST 的不同之处,以及它的优点。这些优点使这个新架构成为特别适合解决客户端和服务器之间数据交付问题的解决方案。</p><h2 id="GraphQL-架构"><a href="#GraphQL-架构" class="headerlink" title="GraphQL 架构"></a>GraphQL 架构</h2><p>与 RESTful API 一样,GraphQL API 设计用于处理 HTTP 请求并对这些请求提供响应。无论如何,这就是相似之处。**REST API 构建在请求方法和端点之间的连接上,而 GraphQL API 被设计为只通过一个端点,始终使用 POST 请求进行查询,其 URL 通常是 <code>yourdomain.com/graphql</code>**。</p><p><img src="https://ask.qcloudimg.com/http-save/yehe-7350649/yvuf86kzg2.jpeg?imageView2/2/w/1620" alt="graphql"></p><p>请求到达 GraphQL 端点后,客户端请求的载荷完全在请求体中处理。这个请求体必须遵循 GraphQL 规范,API 必须<strong>有适当的服务器端逻辑来处理这些请求并提供适当的响应</strong>。</p><p>这提供了比 RESTful API 更流畅的客户端体验,后者可能要求客户端针对多个数据块发出多个请求,并在数据返回后进行操作。</p><p>为了阐明 GraphQL 是如何实现这一点的,让我们分解一下 GraphQL 服务器的结构。</p><h2 id="GraphQL-服务器"><a href="#GraphQL-服务器" class="headerlink" title="GraphQL 服务器"></a>GraphQL 服务器</h2><p>启用 GraphQL 逻辑的服务器端逻辑由定义了服务器功能的 <code>Documents</code> 组成。这些 <code>Documents</code> 包含可执行文件和类型系统定义。顾名思义,类型系统定义为每个数据字段定义可接受的类型和格式输入及结果。</p><p>可执行文件包含要处理的可能的操作列表,其中包括操作类型(查询、修改或订阅)、操作名称、要查询或写入的字段和一个选择集,该选择集准确定义了将从操作返回的数据。选择集是 GraphQL 的最大价值所在——它们允许客户端查询特定的数据集并接收包含所请求信息的响应:不多不少。</p><p>有关 GraphQL 规范的结构和语法的更多信息,请参阅 <a href="http://spec.graphql.org/June2018/#sec-Overview">GraphQL 的文档</a>。</p><p>接下来,我们将看下 GraphQL 中查询的结构。</p><h2 id="GraphQL-查询解析"><a href="#GraphQL-查询解析" class="headerlink" title="GraphQL 查询解析"></a>GraphQL 查询解析</h2><p>下面是一个结构化的 GraphQL 查询,用于获取特定书籍的数据,包括作者的姓和名。</p><pre><code class="js">GET /graphql?query={ books(id:12) { authors { firstName, lastName } title, yearPublished, length } { Query { // operation type books (id:12) { // operation endpoint authors { // requested fields firstName lastName } title yearPublished } }}</code></pre><p>这一切都可以通过一个查询由 GraphQL 服务器逻辑解析和处理完成。当把它与 REST 架构中相同结构的请求进行比较时,GraphQL 的优势就开始显现出来了。</p><p>让我们看看下面的 REST 请求结构,然后重点讨论其中的一些差异!</p><h2 id="REST-请求解析"><a href="#REST-请求解析" class="headerlink" title="REST 请求解析"></a>REST 请求解析</h2><p>要向 REST API 发出相同的请求,客户端首先需要向能够返回图书数据的端点发送一个请求,并将图书 id 作为参数传入:</p><pre><code class="js">GET /books/12</code></pre><p>这个请求可能会返回一个包含特定图书所有数据的对象,例如:</p><pre><code class="js">{ “title” : “The Hitchhiker's Guide to the Galaxy”, “authorID”: 42, “yearPublished” : 1978, “length”: 208, “genre”: “Science Fiction”}</code></pre><p>在我们的例子里,与相同的 GraphQL 查询相比,该响应有两个缺点:</p><ul><li>1.REST 响应包含类似 genre 这样的额外数据,返回的信息超出了我们的需求。</li><li>2.REST 需要再发送一个请求来获得我们实际上正在查找的数据:这个特定作者的所有书籍。</li></ul><p>为了获得这些数据,我们需要使用我们的 <code>authorID</code> 发出一个额外的请求:</p><pre><code class="js">GET /authors/42</code></pre><p>这个请求的响应应该包含我们正在查找的所有数据:</p><pre><code class="js">{ “firstName” : “Douglas”, “lastName”: “Adams”}</code></pre><p>现在我们已经有了需要的所有书籍和作者数据,响应解析由客户端完成。现在,前端应用程序必须将来自不同端点的数据组合在一起,用于实现期望的功能。总的来说,与 REST API 相比,GraphQL 提供的性能优势可以为前端开发人员带来回报。使用 GraphQL 规范创建服务器可能需要更多的设置以及编写预测性的服务器端逻辑来解析和处理请求。</p><p>虽然 GraphQL 的设置成本可能比传统的 REST 架构要高,但是,更易于维护的代码、健壮的开发工具和精简的客户端查询所带来的好处通常会超过成本。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>相同: 都拥有资源这个概念,而且都可以指定资源的身份<br>相同: 都能通过 HTTP GET 和一个 URL 来获取信息<br>相同: 请求的返回值都是 JSON 数据<br>不同: 在 REST 中,你所访问的终端就是所需对象的身份,在 GraphQL 中,对象的身份和获取的方式是独立存在的<br>不同: 在 REST 中,资源的形式和大小是由服务器所决定的。在 GraphQL 中,服务器声明哪些资源可以获得,而客户端会对其所需资源作出请求。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://cloud.tencent.com/developer/news/572892">原文地址</a></p>
<h2 id="与传统-REST-AP</summary>
<category term="GraphQL" scheme="https://shinichikudo-fe.github.io/categories/GraphQL/"/>
<category term="GraphQL" scheme="https://shinichikudo-fe.github.io/tags/GraphQL/"/>
</entry>
<entry>
<title>GraphQL 基础实践</title>
<link href="https://shinichikudo-fe.github.io/2020/11/11/GraphQL/GraphQL%20%E5%9F%BA%E7%A1%80%E5%AE%9E%E8%B7%B5/"/>
<id>https://shinichikudo-fe.github.io/2020/11/11/GraphQL/GraphQL%20%E5%9F%BA%E7%A1%80%E5%AE%9E%E8%B7%B5/</id>
<published>2020-11-11T05:18:03.000Z</published>
<updated>2020-11-11T08:07:34.956Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><p><a href="https://juejin.im/post/6844903641996869645#heading-19">原文地址</a></p><p><a href="https://ppt.baomitu.com/d/4248c64a#/1">PPT讲解地址</a></p><h1 id="什么是GraphQL"><a href="#什么是GraphQL" class="headerlink" title="什么是GraphQL"></a>什么是GraphQL</h1><p><code>GraphQL</code> 是一款由 Facebook 主导开发的数据查询和操作语言, 写过 SQL 查询的同学可以把它想象成是 SQL 查询语言,但 <code>GraphQL</code> 是给客户端查询数据用的。虽然这让你听起来觉得像是一款数据库软件,但实际上 <code>GraphQL</code> 并不是数据库软件。<strong>你可以将 <code>GraphQL</code> 理解成一个中间件,是连接客户端和数据库之间的一座桥梁,客户端给它一个描述,然后从数据库中组合出符合这段描述的数据返回</strong>。这也意味着 <code>GraphQL</code> 并不关心数据存在什么数据库上。</p><p>同时 GraphQL 也是一套标准,在这个标准下不同平台不同语言有相应的实现。GraphQL 中还设计了一套类型系统,在这个类型系统的约束下,可以获得与 <code>TypeScript</code> 相近的相对安全的开发体验。</p><h2 id="GraphQL-解决了什么问题"><a href="#GraphQL-解决了什么问题" class="headerlink" title="GraphQL 解决了什么问题"></a>GraphQL 解决了什么问题</h2><p>我们先来回顾一下我们已经非常熟悉的 RESTful API 设计。简单的说 RESTful API 主要是使用 URL 的方式表达和定位资源,用 HTTP 动词来描述对这个资源的操作。</p><p>我们以 IMDB 电影信息详情页为例子,看看我们得需要什么样的 API 才能满足 RESTful API 设计的要求。先来看看主页面上都需要什么信息。</p><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdzuD8b0MhgbohicKNzDDHlXNjCdTl00r1vryK0nicEnnlHWN5o1ibteyjRQ/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="imdb demo"></p><p>可以看到页面上由<em>电影基本信息,演员和评分/评论信息组成</em>,按照设计要求,我们需要将这三种资源放在不同 API 之下。首先是电影基本信息,我们有 API <code>/movie/:id</code>,给定一个电影ID返回基本信息数据。</p><p>假装 GET 一下获得一个 JSON 格式的数据:</p><pre><code class="js">{ name: “Manchester by the Sea”, ratings: “PG-13”, score: 8.2, release: “2016”, actors:[“https://api/movie/1/actor/1/”], reviews:[“https://api/movie/1/reviews”]}</code></pre><p>这里面包含了我们所需的电影名、分级等信息,以及一种叫做 <code>HyperMedia</code> 的数据,通常是一个 URL,指明了能够获取这个资源的 API 端点地址。如果我们跟着 <code>HyperMedia</code> 指向的连接请求下去,我们就能得到我们页面上所需的所有信息。</p><p><code>GET /api/movue/1/actor/1</code></p><pre><code class="js">{ name: “Ben Affleck”, dob: “1971-01-26”, desc: “blablabla”, movies:[“https://api/movie/1”]}</code></pre><p><code>GET /api/movie/1/reviews</code></p><pre><code class="js">[ { content: “Its’s as good as…”, score: 9 }]</code></pre><p>最后根据需要,我们要将所有包含需要信息的 API 端点都请求一遍,<strong>对于移动端来说,发起一个 HTTP 请求还是比较消耗资源的</strong>,特别是在一些网络连接质量不佳的情况下,一下发出多个请求反而会导致不好的体验。</p><p>而且在这样的 API 设计之中,特定资源分布在特定的 API 端点之中,对于后端来说写起来是挺方便的,但对于Web端或者客户端来说并不一定。例如在 Android 或 iOS 客户端上,发版升级了一个很爆炸的功能,同一个API上可能为了支持这个功能而多吐一些数据。但是对于未升级的客户端来说,这些新数据是没有意义的,也造成了一定的资源浪费。如果单单将所有资源整合到一个 API 之中,还有可能会因为整合了无关的数据而导致数据量的增加。</p><p>而 <code>GraphQL</code> 就是为了解决这些问题而来的,<strong>向服务端发送一次描述信息,告知客户端所需的所有数据,数据的控制甚至可以精细到字段</strong>,达到一次请求获取所有所需数据的目的。</p><h2 id="GraphQL-Hello-World"><a href="#GraphQL-Hello-World" class="headerlink" title="GraphQL Hello World"></a>GraphQL Hello World</h2><h3 id="GraphQL-请求体"><a href="#GraphQL-请求体" class="headerlink" title="GraphQL 请求体"></a>GraphQL 请求体</h3><p>我们先来看一下一个 GraphQL 请求长什么样:</p><pre><code class="js">query myQry ($name: String!) { movie(name: “Manchester”) { name desc ratings }}</code></pre><p>那么,上面的这个请求描述称为一个 <code>GraphQL</code> 请求体,请求体即用来描述你要从服务器上取什么数据用的。一般请求体由几个部分组成,从里到外了解一下。</p><p>首先是字段,字段请求的是一个数据单元。同时在 <code>GraphQL</code> 中,标量字段是粒度最细的一个数据单元了,同时作为返回 JSON 响应数据中的最后一个字段。也就是说,如果是一个 Object,还必须选择至少其中的一个字段。</p><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdzViawDHFfDRibXSlV3o9tLpPTWK9OU42F887H8AIREaAbnj0DUibsQ6dSA/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="字段"></p><p>把我们所需要的字段合在一起,我们把它称之为某某的选择集。上面的 <code>name、desc、ratings</code> 合在一起则称之为 <code>movie</code> <code>的选择集,同理,movie</code> 是 <code>myQry</code> 的选择集。需要注意的是,在标量上不能使用选择集这种操作,因为它已经是最后一层了。</p><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdzhKXezDO26xBpnuyiaX1LP2ich5JvESXXBXJqibg5jc2ckQgjIv7luy8KQ/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="选择集"></p><p>在 <code>movie</code> 的旁边,<code>name: "Manchester"</code>,这个代表着传入 <code>movie</code> 的参数,参数名为 <code>name</code> 值为<code>Manchester</code>,利用这些参数向服务器表达你所需的数据需要符合什么条件。</p><p>最后我们来到请求体的最外层:</p><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdz4daRBcWjtx0slTOtNUtkwvyZFEafEqvzyic2CvwGp8qKvJcZ5Bws7Jg/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="最外层"></p><ul><li><p><code>操作类型</code>:指定本请求体要对数据做什么操作,类似与 <code>REST</code> 中的 <code>GET POST</code>。<code>GraphQL</code> 中基本操作类型有 <code>query 表示查询,mutation 表示对数据进行操作</code>,例如增删改操作,<code>subscription 订阅操作</code>。</p></li><li><p><code>操作名称</code>:操作名称是个可选的参数,操作名称对整个请求并不产生影响,只是赋予请求体一个名字,可以作为调试的依据。</p></li><li><p><code>变量定义</code>:在 <code>GraphQL</code> 中,声明一个变量使用<code>$</code>符号开头,冒号后面紧跟着变量的传入类型。如果要使用变量,直接引用即可,例如上面的 <code>movie</code> 就可以改写成 <code>movie(name: $name)</code>。</p></li></ul><p>如果上述三者都没有提供,那么这个请求体默认会被视为一个 query 操作。</p><h3 id="请求的结果"><a href="#请求的结果" class="headerlink" title="请求的结果"></a>请求的结果</h3><p>如果我们执行上面的请求体,我们将会得到如下的数据:</p><pre><code class="js">{ "data": { "movie": { "name": "Manchester By the Sea", "desc": "A depressed uncle is asked to take care of his teenage nephew after the boy's father dies. ", "ratings": "R" } }}</code></pre><p>仔细对比结果和请求体的结构,你会发现,与请求体的结构是完全一致的。也就是说,<strong>请求体的结构也确定了最终返回数据的结构</strong>。</p><h3 id="GraphQL-Server"><a href="#GraphQL-Server" class="headerlink" title="GraphQL Server"></a>GraphQL Server</h3><p>在前面的 <code>REST</code> 举例中,我们请求多个资源有多个 API 端点。在 GraphQL 中,只有一个 API 端点,同样也接受 <code>GET</code> 和 <code>POST</code> 动词,如要操作 <code>mutation</code> 则使用 <code>POST</code> 请求。</p><p>前面还提到 <code>GraphQL</code> 是一套标准,怎么用呢,我们可以借助一些库去解析。例如 Facebook 官方的 <code>GraphQL.js</code>。以及 Meteor 团队开发的 <code>Apollo</code>,同时开发了客户端和服务端,同时也支持流行的 Vue 和 React 框架。调试方面,可以使用 <code>Graphiql</code> 进行调试,得益于 <code>GraphQL</code> 的类型系统和 <code>Schema</code>,我们还可以在 <code>Graphiql</code> 调试中使用自动完成功能。</p><h2 id="Schema"><a href="#Schema" class="headerlink" title="Schema"></a>Schema</h2><p>前面我们提到,<code>GraphQL</code> 拥有一个类型系统,那么每个字段的类型是怎么约定的呢?答案就在本小节中。在 <code>GraphQL</code> 中,<strong>类型的定义以及查询本身都是通过 <code>Schema</code> 去定义的</strong>。<code>GraphQL</code> 的 <code>Schema</code> 语言全称叫 <code>Schema Definition Language</code>。<code>Schema</code> 本身并不代表你数据库中真实的数据结构,它的定义决定了<strong>这整个端点能干些什么事情,确定了我们能向端点要什么,操作什么</strong>。再次回顾一下前面的请求体,请求体决定了返回数据的结构,而 <code>Schema</code> 的定义决定了端点的能力。</p><p>接下来我们就通过一个一个的例子了解一下 <code>Schema</code>。</p><h3 id="类型系统、标量类型、非空类型、参数"><a href="#类型系统、标量类型、非空类型、参数" class="headerlink" title="类型系统、标量类型、非空类型、参数"></a>类型系统、标量类型、非空类型、参数</h3><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdzibIm98cIybzWGqaEHhRI4mCY0icrkn9DnwnK2MKruYib1LCWnib0jjJD1A/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="schema"></p><p>先看右边的 <code>Schema</code>:<code>type</code> 是 GraphQL <code>Schema</code> 中最基本的一个概念,表示一个 GraphQL 对象类型,可以简单地将其理解为 JavaScript 中的一个对象,在 JavaScript 中一个对象可以包含各种 key,在 GraphQL 中,<code>type</code> 里面同样可以包含各种字段(field),而且字段类型不仅仅可以是标量类型,还可以是 <code>Schema</code> 中定义的其他 <code>type</code>。例如上面的 <code>Schema</code> 中, Query 下的 movie 字段的类型就可以是 Movie。</p><p>在 GraphQL 中,有如下几种标量类型:<code>Int, Float, String, Boolean, ID</code> ,分别表示<code>整型、浮点型、字符串、布尔型以及一个ID类型</code>。ID类型<strong>代表着一个独一无二的标识,ID 类型最终会被转化成String类型,但它必须是独一无二的</strong>,例如 mongodb 中的 _id 字段就可以设置为ID类型。同时这些标量类型可以理解为 JavaScript 中的原始类型,上面的标量类型同样可以对应 JavaScript 中的 <code>Number, Number, String, Boolean, Symbol</code> 。</p><p>通过上面的类型定义,可以看到 <code>GraphQL</code> 中的类型系统起到了很重要的角色。在本例中,Schema 定义了 name 为 String类型,那么你就不能传 Int类型进去,此时会抛出类型不符的错误。同样的,如果传出的 <code>ratings</code> 数据类型不为 <code>String</code>,也同样会抛出类型不符的错误。</p><h3 id="列表(List)、枚举类型(Enum)"><a href="#列表(List)、枚举类型(Enum)" class="headerlink" title="列表(List)、枚举类型(Enum)"></a>列表(List)、枚举类型(Enum)</h3><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdzVM5h5I7ibtMbRh9HMGqVAeLfqHUSbArCBYaHUWdET4QfeONw0kO0aeg/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1"></p><p>如果我们的某个字段返回不止一个标量类型的数据,而是一组,则需要使用List类型声明,在该标量类型两边使用中括号<code>[]</code>包围即可,与 JavaScript 中数组的写法相同,而且返回的数据也将会是数组类型。</p><p>需要注意的是<code>[Movie]!</code>与 <code>[Movie!]</code>两种写法的含义是不同的:<strong>前者表示 <code>movies</code>字段始终返回不可为空但Movie元素可以为空</strong>。<strong>后者表示<code>movies</code>中返回的 Movie 元素不能为空,但 <code>movies</code>字段的返回是可以为空的</strong>。</p><p>你可能在请求体中注意到,<code>genre</code> 参数的值没有被双引号括起来,也不是任何内置类型。看到 <code>Schema</code> 定义,COMEDY是枚举类型MovieTypes中的枚举成员。枚举类型用于声明一组取值常量列表,如果声明了某个参数为某个枚举类型,那么该参数只能传入该枚举类型内限定的常量名。</p><h3 id="传入复杂结构的参数(Input)"><a href="#传入复杂结构的参数(Input)" class="headerlink" title="传入复杂结构的参数(Input)"></a>传入复杂结构的参数(Input)</h3><p>前面的例子中,传入的参数均为标量类型,那么如果我们想传入一个拥有复杂结构的数据该怎么定义呢。答案是使用<strong>关键字input</strong>。其使用方法和type完全一致。</p><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdz7tz9DrrcqantibfXNo5Z2NZ3ymRE51y16Teeawibf5kjuE81HiaqLDibeQ/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="input"></p><p>根据本例中的 <code>Schema</code> 定义,我们在查询 search时data的参数必须为</p><pre><code class="js">{ term: "Deepwater Horizon" }</code></pre><h3 id="别名(Alias)"><a href="#别名(Alias)" class="headerlink" title="别名(Alias)"></a>别名(Alias)</h3><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdz87vwwe8S2CXvu20MPo6UzrDhY1gW3A87xRbicPW7f5DDibLvecSAyK1A/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="alias"></p><p>想象这么一个页面,我要列出两个电影的信息做对比,为了发挥 <code>GraphQL</code> 的优势,我要同时查询这两部电影的信息,在请求体中请求 <code>movie</code> 数据。前面我们说到,请求体决定了返回数据的结构。在数据返回前查出两个 key 为 <code>movie</code> 的数据,合并之后由于 key 重复而只能拿到一条数据。那么在这种情况下我们需要使用别名功能。</p><p>别名即为返回字段使用另一个名字,使用方法也很简单,只需要在请求体的字段前面使用别名<code>:</code>的形式即可,返回的数据将会自动替换为该名称。</p><h3 id="片段(Fragment)、片段解构(Fragment-Spread)"><a href="#片段(Fragment)、片段解构(Fragment-Spread)" class="headerlink" title="片段(Fragment)、片段解构(Fragment Spread)"></a>片段(Fragment)、片段解构(Fragment Spread)</h3><p><img src="https://mmbiz.qpic.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdz4duFiaoiaB7n6rSXibEqibDwFAaKibGUc2VUm4JkYVCSmqDibdNia4KaZweWA/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="片段"></p><p>在上面的例子中,我们需要对比两部电影的数据。如果换作是硬件对比网站,需要查询的硬件数量往往不止两个。此时编写冗余的选择集显得非常的费劲、臃肿以及难维护。为了解决这个问题,我们可以使用片段功能。**<code>GraphQL</code> 允许定义一段公用的选择集,叫片段**。定义片段使用 <code>fragment name on Type</code> 的语法,其中 name为自定义的片段名称,Type为片段来自的类型。</p><p>本例中的请求体的选择集公共部分提取成片段之后为</p><pre><code class="js">fragment movieInfo on Movie { name desc}</code></pre><p>在正式使用片段之前,还需要向各位介绍<code>片段解构功能</code>。类似于 JavaScript 的结构。<strong>GraphQL 的片段结构符号将片段内的字段“结构”到选择集中</strong>。</p><p><img src="https://mmbiz.qlogo.cn/mmbiz/MpGQUHiaib4ib5EjuicgHA4YxH7DOsVHGbdzvOpicOCfiajJPfWgs5LFr4Ujym34VqhZwicMITEOIGiamdNicr1DvbQhNcg/640?wx_fmt=other&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1&retryload=2" alt="片段解构"></p><h3 id="接口(Interface)"><a href="#接口(Interface)" class="headerlink" title="接口(Interface)"></a>接口(Interface)</h3><p>与其他大多数语言一样,<code>GraphQL</code> 也提供了定义接口的功能。接口指的是 <code>GraphQL</code> 实体类型本身提供字段的集合,定义一组与外部沟通的方式。使用了 <code>implements</code>的类型必须包含接口中定义的字段。</p><pre><code class="js">interface Basic { name: String! year: Number!}type Song implements Basic { name: String! year: Number! artist: [String]!}type Video implements Basic { name: String! year: Number! performers: [String]!}Query { search(term: String!): [Basic]!}</code></pre><p>在本例中,定义了一个<code>Basic</code>接口,<code>Song</code>以及<code>Video</code>类型都要实现该接口的字段。然后在search查询中返回该接口。</p><p><code>searchMedia</code>查询返回一组<code>Basic</code>接口。由于该接口中的字段是所有实现了该接口的类型所共有的,在请求体上可以直接使用。而对于特定类型上的其他非共有字段,例如<code>Video</code>中的performers,直接选取是会有问题的,因为<code>searchMedia</code>在返回的数据中类型可能是所有实现了该接口的类型,而在 <code>Song</code>类型中就没有performers字段。此时我们可以借助内联片段的帮助(下面介绍)</p><h3 id="联合类型(Union)"><a href="#联合类型(Union)" class="headerlink" title="联合类型(Union)"></a>联合类型(Union)</h3><p>联合类型与接口概念差不多相同,不同之处在于联合类型下的类型之间没有定义公共的字段。在 <code>Union</code> 类型中必须使用内联片段的方式查询,原因与上面的接口类型一致。</p><pre><code class="js">union SearchResult = Song | VideoQuery { search(term: String!): [SearchResult]!}</code></pre><h3 id="内联片段(Inline-Fragment)"><a href="#内联片段(Inline-Fragment)" class="headerlink" title="内联片段(Inline Fragment)"></a>内联片段(Inline Fragment)</h3><p>对接口或联合类型进行查询时,由于返回类型的不同导致选取的字段可能不同,此时需要通过内联片段的方式决定在特定类型下使用特定的选择集。内联选择集的概念和用法与普通片段基本相同,不同的是<strong>内联片段直接声明在选择集内,并且不需要fragment声明</strong>。</p><p>查询接口的例子:</p><pre><code class="js">query { searchMedia(term: "AJR") { name year ...on Song { artist } ...on Video { performers } }}</code></pre><p>首选我们需要该接口上的两个公共字段,并且结果为Song类型时选取artist字段,结果为Video类型时选取performers字段</p><h3 id="GraphQL-内置指令"><a href="#GraphQL-内置指令" class="headerlink" title="GraphQL 内置指令"></a>GraphQL 内置指令</h3><p>GraphQL 中内置了两款逻辑指令,指令跟在字段名后使用。</p><h4 id="include"><a href="#include" class="headerlink" title="@include"></a>@include</h4><p>当条件成立时,查询此字段</p><pre><code class="js">query { search { actors @include(if: $queryActor) { name } }}</code></pre><h4 id="skip"><a href="#skip" class="headerlink" title="@skip"></a>@skip</h4><p>当条件成立时,不查询此字段</p><pre><code class="js">query { search { comments @skip(if: $noComments) { from } }}</code></pre><h2 id="Resolvers"><a href="#Resolvers" class="headerlink" title="Resolvers"></a>Resolvers</h2><p>前面我们已经了解了请求体以及 Schema,那么我们的数据到底怎么来呢?答案是来自 <code>Resolver</code> 函数。</p><p><code>Resolver</code> 的概念非常简单。<code>Resolver</code> 对应着 Schema 上的字段,当请求体查询某个字段时,对应的 <code>Resolver</code> 函数会被执行,由 <code>Resolver</code> 函数负责到数据库中取得数据并返回,最终将请求体中指定的字段返回。</p><pre><code class="js">type Movie { name genre}type Query { movie: Movie!}</code></pre><p>当请求体查询movie时,同名的 <code>Resolver</code> 必须返回Movie类型的数据。当然你还可以单独为name字段使用独立的 <code>Resolver</code> 进行解析。后面的代码例子中将会清楚地了解 <code>Resolver</code></p><h2 id="GraphQL-的优缺点"><a href="#GraphQL-的优缺点" class="headerlink" title="GraphQL 的优缺点"></a>GraphQL 的优缺点</h2><blockquote><p>优点</p></blockquote><ul><li>所见即所得:所写请求体即为最终数据结构</li><li>减少网络请求:复杂数据的获取也可以一次请求完成</li><li>Schema 即文档:定义的 Schema 也规定了请求的规则</li><li>类型检查:严格的类型检查能够消除一定的认为失误</li></ul><blockquote><p>缺点</p></blockquote><ul><li>增加了服务端实现的复杂度:一些业务可能无法迁移使用 GraphQL,虽然可以使用<code>中间件的方式</code>将原业务的请求进行代理,这无疑也将增加复杂度和资源的消耗</li></ul>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<p><a href="https://juejin.im/post/6844903641996869645#heading-19">原文地址</a></p>
<p><a href="h</summary>
<category term="GraphQL " scheme="https://shinichikudo-fe.github.io/categories/GraphQL/"/>
<category term="GraphQL " scheme="https://shinichikudo-fe.github.io/tags/GraphQL/"/>
</entry>
<entry>
<title>Typescript基础及问题汇总</title>
<link href="https://shinichikudo-fe.github.io/2020/11/02/Ts/Typescript%E5%9F%BA%E7%A1%80%E5%8F%8A%E9%97%AE%E9%A2%98%E6%B1%87%E6%80%BB/"/>
<id>https://shinichikudo-fe.github.io/2020/11/02/Ts/Typescript%E5%9F%BA%E7%A1%80%E5%8F%8A%E9%97%AE%E9%A2%98%E6%B1%87%E6%80%BB/</id>
<published>2020-11-02T02:18:00.000Z</published>
<updated>2020-11-02T11:33:24.069Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><h2 id="是否可以将多个js文件合并为1个ts文件?如果是怎么做?"><a href="#是否可以将多个js文件合并为1个ts文件?如果是怎么做?" class="headerlink" title="是否可以将多个js文件合并为1个ts文件?如果是怎么做?"></a>是否可以将多个js文件合并为1个ts文件?如果是怎么做?</h2><p>可以合并为一个文件,为此我们需要添加–outFile[OutputJSFileName]编译选项</p><pre><code class="js">$tsc --outFile common.js file1.ts file2.js file3.js</code></pre><p>上述命令将三个文件合并到common.js文件中</p><h2 id="ts的基础类型都有哪些?与js有哪些区别?"><a href="#ts的基础类型都有哪些?与js有哪些区别?" class="headerlink" title="ts的基础类型都有哪些?与js有哪些区别?"></a>ts的基础类型都有哪些?与js有哪些区别?</h2><p>ts必须指定数据类型,可以分为3种:</p><ul><li>1.js有的类型</li></ul><p>blooean类型,number类型,string类型,null类型,undefined,数组类型</p><ul><li>2.ts特有的类型</li></ul><p>tuple类型(元组类型),emnu类型(枚举类型),any类型(任意类型)</p><ul><li>3.特别的类型</li></ul><p>void类型(没有任何类型)表示定义方法没有返回值</p><p>never类型:是undefind,null的子类型,表示从不会出现的值,这意味着声明never变量只能被never类型所赋值</p><h3 id="Demo"><a href="#Demo" class="headerlink" title="Demo"></a>Demo</h3><p>1.定义数组的方式</p><pre><code class="ts">let arr1:number[] = [1,2,3,4,5]let arr2:Array[number] = [1,2,3,4,5]let arr3:[number,string] = [1,2,3,4,5]</code></pre><p>2.定义emnu枚举类型方法</p><pre><code class="ts">emnu flag{success=1,error=2}let s:flag = flag.successconsole.log(s)</code></pre><p>3.定义any任意类型方法</p><pre><code class="ts">let num1:any:truenum1 = true</code></pre><p>4.undefined类型</p><pre><code class="ts">let num2 = number | undefinedconsole.log(num2)</code></pre><p>5.vold类型,函数没有返回值</p><pre><code class="ts">function run():void { console.log('run')}</code></pre><p>6.never类型定义方法</p><pre><code class="ts">let c:never;c = (()=>{ throw new Error('error')})</code></pre><h2 id="ts为什么会流行?与ECMA新规范的关系?"><a href="#ts为什么会流行?与ECMA新规范的关系?" class="headerlink" title="ts为什么会流行?与ECMA新规范的关系?"></a>ts为什么会流行?与ECMA新规范的关系?</h2><ul><li>TypeScript快速、简单,最重要的是,容易学习。</li><li>TypeScript支持面向对象的编程特性,比如类、接口、继承、泛型等等。</li><li>TypeScript在编译时提供了错误检查功能。它将编译代码,如果发现任何错误,它将在运行脚本之前突出显示这些错误。</li><li>TypeScript支持所有JavaScript库,因为它是JavaScript的超集。</li><li>TypeScript通过使用继承来支持可重用性。</li><li>TypeScript使应用程序开发尽可能的快速和简单,并且TypeScript的工具支持为我们提供了自动完成、类型检查和源文档。</li><li>TypeScript支持最新的JavaScript特性,包括ECMAScript 2015。</li><li>TypeScript提供了ES6的所有优点和更高的生产力。</li><li>TypeScript支持静态类型、强类型、模块、可选参数等。</li></ul><h2 id="tslint都能配置哪些功能?对开发流程有何影响?"><a href="#tslint都能配置哪些功能?对开发流程有何影响?" class="headerlink" title="tslint都能配置哪些功能?对开发流程有何影响?"></a>tslint都能配置哪些功能?对开发流程有何影响?</h2><h2 id="如何使用ts实现类型约束,枚举等特性?"><a href="#如何使用ts实现类型约束,枚举等特性?" class="headerlink" title="如何使用ts实现类型约束,枚举等特性?"></a>如何使用ts实现类型约束,枚举等特性?</h2><h2 id="如何理解接口,泛型"><a href="#如何理解接口,泛型" class="headerlink" title="如何理解接口,泛型?"></a>如何理解接口,泛型?</h2><h2 id="和JS的区别"><a href="#和JS的区别" class="headerlink" title="和JS的区别"></a>和JS的区别</h2><p><img src="https://img-blog.csdnimg.cn/20201019091205642.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzY5ODMyOA==,size_16,color_FFFFFF,t_70#pic_center" alt="区别"></p><h2 id="TS的接口"><a href="#TS的接口" class="headerlink" title="TS的接口"></a>TS的接口</h2><p>接口是在我们的应用程序中充当契约的结构。它定义了要遵循的类的语法,这意味着实现接口的类必须实现它的所有成员。它不能被实例化,但是可以被实现它的类对象引用。无论对象是否具有特定的结构,TypeScript编译器都使用接口进行类型检查</p><pre><code class="js">interface name{ // 字段声明 // 方法声明}</code></pre><p>接口只是声明方法和字段,它不能用来建造任何东西。不需要将接口转换为<code>JavaScript</code>来执行,它们对运行时<code>JavaScript</code>没有任何影响。因此,它们的唯一目的是在开发阶段提供帮助。</p><h2 id="你如何理解Typescript中的类?列出类的一些特性。"><a href="#你如何理解Typescript中的类?列出类的一些特性。" class="headerlink" title="你如何理解Typescript中的类?列出类的一些特性。"></a>你如何理解Typescript中的类?列出类的一些特性。</h2><p><code>TypeScript</code>是一种面向对象的<code>JavaScript</code>语言,支持OOP编程特性,比如类、接口等。与Java一样,类是用于创建可重用组件的基本实体。它是一组具有公共属性的对象。类是创建对象的模板或蓝图。它是一个逻辑实体。“class”关键字用于在<code>Typescript</code>中声明一个类。</p><pre><code class="js">class Student { studCode: number; studName: string; constructor(code: number, name: string) { this.studName = name; this.studCode = code; } getGrade() : string { return "A+" ; } } </code></pre><p>类的特征是:</p><p>继承,封装,多态性,抽象</p><ul><li>类(Class):定义了一件事物的抽象特点,包含它的属性和方法</li><li>对象(Object):类的实例,通过 new 生成</li><li>面向对象(OOP)的三大特性:封装、继承、多态</li><li>封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据</li><li>继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性</li><li>多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat还是 Dog,就可以直接调用 eat方法,程序会自动判断出来应该如何执行 eat</li><li>存取器(getter & setter):用以改变属性的读取和赋值行为</li><li>修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。<br>比如 public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的</li><li>private 修饰的属性或方法是私有的,不能在声明它的类的外部访问</li><li>protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的</li><li>抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现</li><li>接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口</li></ul><h2 id="继承"><a href="#继承" class="headerlink" title="继承"></a>继承</h2><p>继承可以通过使用<code>extend</code>关键字来实现。</p><pre><code class="js">class Shape { Area:number constructor(area:number) { this.Area = area } } class Circle extends Shape { display():void { console.log("圆的面积: "+this.Area) } } var obj = new Circle(320); obj.display() </code></pre><h2 id="模块"><a href="#模块" class="headerlink" title="模块"></a>模块</h2><p>在模块中声明的变量、函数、类和接口不能在模块外部直接访问。</p><pre><code class="js">//可以使用export关键字创建模块,也可以在其他模块中使用import关键字。module module_name{ class xyz{ export sum(x, y){ return x+y; } } } </code></pre><p><img src="https://img-blog.csdnimg.cn/20201019093902395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzY5ODMyOA==,size_16,color_FFFFFF,t_70#pic_center" alt="模块"></p><p>“内部模块”现在称做“<code>命名空间</code>”。<br>“外部模块”现在则简称为“模块”模块在其自身的作用域里执行,而不是在全局作用域里;</p><h3 id="方法一:"><a href="#方法一:" class="headerlink" title="方法一:"></a>方法一:</h3><pre><code class="js">/** * 新建一个db.ts 将数据库方法封装并且暴露出来 * 暴露一个获取数据的方法 */export function getData():any[]{ return [ { name:'123', ahe:20 }, { name:'123425', age:30 } ]}export function saveData():boolean{ console.log('保存数据成功!') return true;}/** * 在index.ts文件中引入 * 在这里引入我暴露的函数 */import {getData} from './modules/db'console.log(getData());saveData();</code></pre><h3 id="方法二"><a href="#方法二" class="headerlink" title="方法二"></a>方法二</h3><pre><code class="js">/** * 暴露一个获取数据的方法 */function getData():any[]{ return [ { name:'123', ahe:20 }, { name:'123425', age:30 } ]}function saveData():boolean{ console.log('保存数据成功!') return true;}export {getData, saveData}/** * 在index.ts文件中引入 * 在这里引入我暴露的函数 */import {getData} from './modules/db'console.log(getData());saveData();</code></pre><h3 id="命名空间:"><a href="#命名空间:" class="headerlink" title="命名空间:"></a>命名空间:</h3><p>它封装了共享某些关系的特性和对象。名称空间也称为内部模块。名称空间还可以包括接口、类、函数和变量,以支持一组相关功能。<br><strong>注意</strong>: 名称空间可以在多个文件中定义,并允许将每个文件都定义在一个地方。它使代码更容易维护。</p><pre><code class="ts">namespace <namespace_name> { export interface I1 { } export class c1{ } } </code></pre><h2 id="TypeScript是如何在函数中支持可选参数的?"><a href="#TypeScript是如何在函数中支持可选参数的?" class="headerlink" title="TypeScript是如何在函数中支持可选参数的?"></a>TypeScript是如何在函数中支持可选参数的?</h2><p>我们可以通过使用问号符号(‘?’)来使用可选参数。这意味着可以或不可以接收值的参数可以附加一个”?”可选的。</p><pre><code class="js">function Demo(arg1: number, arg2? :number) { }</code></pre><p>因此,<code>arg1</code>总是必需的,而<code>arg2</code>是一个可选参数要放后面。<br>注意: 可选参数必须遵循要求的参数。如果我们想让<code>arg1</code>成为可选的,而不是arg2,那么我们需要改变顺序,<code>arg1</code>必须放在<code>arg2</code>之后。</p><pre><code class="js">function Demo(arg2: number, arg1? :number) { } </code></pre><h2 id="什么是TypeScript-Declare关键字"><a href="#什么是TypeScript-Declare关键字" class="headerlink" title="什么是TypeScript Declare关键字?"></a>什么是TypeScript Declare关键字?</h2><p>我们知道所有的<code>JavaScript</code>库/框架都没有<code>TypeScript</code>声明文件,但是我们希望在<code>TypeScript</code>文件中使用它们时不会出现编译错误。为此,我们使用<code>declare</code>关键字。在我们希望定义可能存在于其他地方的变量的环境声明和方法中,可以使用<code>declare</code>关键字。</p><p>例如,假设我们有一个名为myLibrary的库,它没有<code>TypeScript</code>声明文件,在全局命名空间中有一个名为myLibrary的命名空间。如果我们想在TypeScript代码中使用这个库,我们可以使用以下代码:</p><pre><code class="ts">declare var myLibrary;</code></pre><h2 id="解释TypeScript中的泛型?"><a href="#解释TypeScript中的泛型?" class="headerlink" title="解释TypeScript中的泛型?"></a>解释TypeScript中的泛型?</h2><p><code>TypeScript</code>泛型是一个提供创建可重用组件方法的工具。它能够创建可以处理多种数据类型而不是单一数据类型的组件。泛型在不影响性能或生产率的情况下提供类型安全性。泛型允许我们创建泛型类、泛型函数、泛型方法和泛型接口。</p><p>在泛型中,类型参数写<code>在开(<)和闭(>)</code>括号之间,这使得它是强类型集合。泛型使用一种特殊类型的类型变量,它表示类型。泛型集合只包含类似类型的对象。</p><pre><code class="ts">function identity<T>(arg: T): T { return arg; } let output1 = identity<string>("myString"); let output2 = identity<number>( 100 ); console.log(output1); console.log(output2); </code></pre><h2 id="TS的“接口”和“type”语句有什么区别?"><a href="#TS的“接口”和“type”语句有什么区别?" class="headerlink" title="TS的“接口”和“type”语句有什么区别?"></a>TS的“接口”和“type”语句有什么区别?</h2><pre><code class="ts">interface X { a: number b: string } type X = { a: number b: string }; </code></pre><p><img src="https://img-blog.csdnimg.cn/20201019100145600.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzY5ODMyOA==,size_16,color_FFFFFF,t_70#pic_center" alt="type与interface"></p><h2 id="什么是TypeScript中的类型断言?"><a href="#什么是TypeScript中的类型断言?" class="headerlink" title="什么是TypeScript中的类型断言?"></a>什么是TypeScript中的类型断言?</h2><p>类型断言的工作方式类似于其他语言中的类型转换,但是它不像其他语言一样执行C#和Java那样的类型检查或数据重组。类型转换附带运行时支持,而类型断言对运行时没有影响。但是,类型断言仅由编译器使用,并向编译器提供有关我们希望如何分析代码的提示。</p><pre><code class="ts">let empCode: any = 111; let employeeCode = <number> code; console.log(typeof(employeeCode)); // : number </code></pre><p>例将 something 断言成 string</p><pre><code class="ts">function getLength(something: string | number): number { if ((<string>something).length) { return (<string>something).length; } else { return something.toString().length; }}</code></pre><h2 id="TypeScript的as语法是什么?"><a href="#TypeScript的as语法是什么?" class="headerlink" title="TypeScript的as语法是什么?"></a>TypeScript的as语法是什么?</h2><p>as是TypeScript中类型断言的附加语法,引入as-语法的原因是<strong>原始语法()与JSX冲突</strong>。</p><pre><code class="js">let empCode: any = 111; let employeeCode = code as number; </code></pre><p>当使用带有JSX的TypeScript时,只允许as风格的断言。</p><h2 id="什么是JSX?我们可以在TypeScript中使用JSX吗?"><a href="#什么是JSX?我们可以在TypeScript中使用JSX吗?" class="headerlink" title="什么是JSX?我们可以在TypeScript中使用JSX吗?"></a>什么是JSX?我们可以在TypeScript中使用JSX吗?</h2><p>JSX只不过是带有不同扩展名的Javascript。Facebook提出了这个新的扩展,以便与JavaScript中类似xml的HTML实现区分开来。</p><p>JSX是一种可嵌入的类似xml的语法。它将被转换成有效的JavaScript。JSX随着React框架而流行起来。TypeScript支持嵌入、类型检查和直接将JSX编译成JavaScript。</p><p>要使用JSX,我们必须做两件事。</p><p><code>使用.tsx扩展名命名文件</code><br><code>启用jsx选项</code></p><h2 id="什么是Rest参数?"><a href="#什么是Rest参数?" class="headerlink" title="什么是Rest参数?"></a>什么是Rest参数?</h2><p>rest参数用于向函数传递零个或多个值。它是通过在参数前加上三个点字符(‘…’)来声明的。它允许函数在不使用arguments对象的情况下拥有可变数量的参数。当我们有不确定数量的参数时,这是非常有用的。</p><p>rest参数要遵循的规则:</p><ol><li>一个函数中只允许有一个rest参数。</li><li>它必须是数组类型。</li><li>它必须是参数列表中的最后一个参数。</li></ol><pre><code class="js">function sum(a: number, ...b: number[]): number { let result = a; for (var i = 0; i < b.length; i++) { result += b[i]; } console.log(result); } let result1 = sum(3, 5); let result2 = sum(3, 5, 7, 9); </code></pre><h2 id="解释TypeScript的Enum枚举类型?"><a href="#解释TypeScript的Enum枚举类型?" class="headerlink" title="解释TypeScript的Enum枚举类型?"></a>解释TypeScript的Enum枚举类型?</h2><p>枚举或枚举是一种数据类型,允许我们定义一组命名常量。使用枚举可以更容易地记录意图,或者创建一组不同的案例。它是相关值的集合,可以是数值或字符串值。</p><pre><code class="js">enum Gender { Male, Female Other } console.log(Gender.Female); // : 1 // 我们还可以通过enum值的number值来访问它console.log(Gender[1]); // : Female</code></pre><h2 id="解释相对模块和非相对模块的导入"><a href="#解释相对模块和非相对模块的导入" class="headerlink" title="解释相对模块和非相对模块的导入"></a>解释相对模块和非相对模块的导入</h2><ul><li>非相对</li><li>非相对导入可以相对于baseUrl解析,也可以通过路径映射解析。换句话说,我们在导入任何外部依赖项时使用非相对路径。 例子:</li></ul><pre><code class="js"> import * as $ from “jquery”; import { Component } from “@angular/core”;</code></pre><ul><li>相对</li><li>相对导入可以用于我们自己的模块,这些模块保证在运行时维护它们的相对位置。相对导入以/、./或…/开头。 例子:</li></ul><pre><code class="js"> import Entry from “./components/Entry”; import {DefaultHeaders} from “../constants/http”;</code></pre><h2 id="什么是匿名函数?"><a href="#什么是匿名函数?" class="headerlink" title="什么是匿名函数?"></a>什么是匿名函数?</h2><p>匿名函数是声明时没有任何命名标识符的函数。这些函数是在运行时动态声明的。与标准函数一样,匿名函数可以接受输入和返回输出。匿名函数在初始创建之后通常是不可访问的。</p><pre><code class="js">let myAdd = function(x: number, y: number): number { return x + y; }; console.log(myAdd()) </code></pre><h2 id="什么是声明合并?"><a href="#什么是声明合并?" class="headerlink" title="什么是声明合并?"></a>什么是声明合并?</h2><p><code>声明合并</code>是编译器随后合并两个或多个独立声明的过程。将具有相同名称的声明声明为单个定义。这个合并的定义具有两个原始声明的特性。</p><p>最简单也是最常见的声明合并类型是接口合并。在最基本的层次上,merge将两个声明的成员机械地连接到一个具有相同名称的接口中</p><pre><code class="js">interface Cloner { clone(animal: Animal): Animal; } interface Cloner { clone(animal: Sheep): Sheep; } interface Cloner { clone(animal: Dog): Dog; clone(animal: Cat): Cat; } // 这三个接口将合并为一个单独的声明interface Cloner { clone(animal: Dog): Dog; clone(animal: Cat): Cat; clone(animal: Sheep): Sheep; clone(animal: Animal): Animal; } </code></pre><p>注: 在TypeScript中不是所有的合并都允许。目前,类不能与其他类或变量合并。</p><h2 id="TypeScript中的方法重写是什么"><a href="#TypeScript中的方法重写是什么" class="headerlink" title="TypeScript中的方法重写是什么?"></a>TypeScript中的方法重写是什么?</h2><p>如果子类(子类)具有与父类中声明的相同的方法,则称为方法覆盖。换句话说,在派生类或子类中重新定义基类方法。</p><p>方法重写的规则:</p><ol><li>该方法必须具有与父类相同的名称</li><li>该方法必须具有与父类相同的参数。</li><li>必须有一个IS-A关系(继承)。</li></ol><pre><code class="js">class NewPrinter extends Printer { doPrint(): any { super.doPrint(); console.log("Called Child class."); } doInkJetPrint(): any { console.log("Called doInkJetPrint()."); } } let printer: new () => NewPrinter; printer.doPrint(); printer.doInkJetPrint(); </code></pre><h2 id="Lambda-箭头函数是什么?"><a href="#Lambda-箭头函数是什么?" class="headerlink" title="Lambda/箭头函数是什么?"></a>Lambda/箭头函数是什么?</h2><p>ES6版本的<code>TypeScript</code>提供了定义匿名函数的简写语法,也就是用于函数表达式。<br>这些箭头函数也称为<code>Lambda</code>函数。<code>lambda</code>函数是没有名称的函数,箭头函数省略了<code>function</code>关键字。</p><pre><code class="js">let sum = (a: number, b: number): number => { return a + b; } console.log(sum(20, 30)); //returns 50 </code></pre><p>在上面,<code>?=>?</code>是一个lambda操作符,(a + b)是函数的主体,(a: number, b: number)是内联参数。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<h2 id="是否可以将多个js文件合并为1个ts文件?如果是怎么做?"><a href="#是否可以将多个js文件合并为1个ts文件?如果是怎么做?" class="headerli</summary>
<category term="Ts" scheme="https://shinichikudo-fe.github.io/categories/Ts/"/>
<category term="Ts" scheme="https://shinichikudo-fe.github.io/tags/Ts/"/>
</entry>
<entry>
<title>学习Javascript应懂得33个概念之DOM树和渲染过程</title>
<link href="https://shinichikudo-fe.github.io/2020/09/30/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BDOM%E6%A0%91%E5%92%8C%E6%B8%B2%E6%9F%93%E8%BF%87%E7%A8%8B/"/>
<id>https://shinichikudo-fe.github.io/2020/09/30/Js/33%E4%B8%AAJS%E6%A6%82%E5%BF%B5/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5%E4%B9%8BDOM%E6%A0%91%E5%92%8C%E6%B8%B2%E6%9F%93%E8%BF%87%E7%A8%8B/</id>
<published>2020-09-30T01:15:38.000Z</published>
<updated>2020-09-30T02:18:26.765Z</updated>
<content type="html"><![CDATA[<meta name="referrer" content="no-referrer"/><h2 id="DOM操作"><a href="#DOM操作" class="headerlink" title="DOM操作"></a>DOM操作</h2><p>平时中因为有各个框架的API使用,使我们对DOM的操作,慢慢变得淡忘,但是我们要知道的是各个框架也是操作dom的,只是将方法暴露出来,方便我们使用。通过学习DOM操作,加深对Javascript的理解</p><p>DOM操作并没有你想象的那么难。</p><p><img src="https://user-gold-cdn.xitu.io/2016/11/29/8d195c29c5f16f86e390d4c6f26084da?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="dom操作"></p><h3 id="创建元素"><a href="#创建元素" class="headerlink" title="创建元素"></a>创建元素</h3><p>让我们开始创建HTML元素,为了创建元素,你需要用<code>document.createElement(tagName) </code>方法:</p><pre><code class="js">const h1 = document.createElement('h1')//</code></pre><h3 id="修改文本内容"><a href="#修改文本内容" class="headerlink" title="修改文本内容"></a>修改文本内容</h3><pre><code class="js">h1.textContent = 'hello world'</code></pre><h3 id="Attributes"><a href="#Attributes" class="headerlink" title="Attributes"></a>Attributes</h3><pre><code class="js">h1.setAttributes('class','hello')</code></pre><p>为了管理类,用<code>element.className</code>属性</p><pre><code class="js">h1.className = 'world'</code></pre><p>然而,最好的办法还是用<code>classList</code></p><pre><code class="js">h1.classList.add('hello')h1.classList.remove('hello')</code></pre><p>要设置元素的ID,你可以用attribute或者直接设置id属性</p><pre><code class="js">h1.setAttribute('id', 'hello-world')h1.id = 'hello-world'</code></pre><blockquote><p>如果你不确定是用attributes还是properties,那就用attributes,除了表单元素的状态,像value和checked</p></blockquote><p>除了下面这些,你不能用<code>element.setAttribute(someBoolean, false)</code>来设置bool值:</p><pre><code class="js">input.checked = true// input.checked = false// input.setAttribute(‘checked’, ‘’)// input.removeAttribute('checked')// </code></pre><h3 id="添加元素"><a href="#添加元素" class="headerlink" title="添加元素"></a>添加元素</h3><pre><code class="js">document.body.appendChild(h1)</code></pre><h3 id="删除元素"><a href="#删除元素" class="headerlink" title="删除元素"></a>删除元素</h3><pre><code class="js">document.body.removeChild(h1)</code></pre><h3 id="查找元素"><a href="#查找元素" class="headerlink" title="查找元素"></a>查找元素</h3><pre><code class="js">document.getElementById(id)element.childNodes[i]element.firstChild === element.childNodes[0]element.lastChild === element.childNodes[element.childNodes.length - 1]element.getElementsByTagName(tagName)element.getElementsByClassName(className)element.querySelector(query)element.querySelectorAll(query)</code></pre><p><strong>注意</strong>:<code>getElementsByTagName, getElementsByClassName 和 querySelectorAll</code>返回的不是数组,而是一个<code>NodeList</code>,所以不能用ES5的数组迭代器迭代,这里有一些关于<a href="https://developer.mozilla.org/en-US/docs/Web/API/NodeList#Workarounds">NodeList的介绍</a></p><h3 id="元素之间插入元素"><a href="#元素之间插入元素" class="headerlink" title="元素之间插入元素"></a>元素之间插入元素</h3><pre><code class="js"> document.body.insertBefore(h1, document.body.firstChild)</code></pre><h3 id="创建很多元素"><a href="#创建很多元素" class="headerlink" title="创建很多元素"></a>创建很多元素</h3><pre><code class="js">const data = [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ]const table = document.createElement('table')data.forEach(row => { const tr = document.createElement('tr') row.forEach(cell => { const td = document.createElement('td') td.textContent = cell tr.appendChild(td) }) table.appendChild(tr)})document.body.appendChild(table)</code></pre><h3 id="更新一系列元素"><a href="#更新一系列元素" class="headerlink" title="更新一系列元素"></a>更新一系列元素</h3><pre><code class="js">const table = document.createElement('table')document.body.appendChild(table)updateTable(table, [ [ 1, 2 ], [ 3, 4, 5 ], [ 6, 7, 8, 9 ] ])setTimeout(() => { updateTable(table, [ [ 1, 2, 3, 4 ], [ 5, 6, 7 ], [ 8, 9 ] ]) }, 1000)function updateTable (table, data) { const rowLookup = table._lookup || (table._lookup = []) setChildren(table, updateRows(rowLookup, data))}function updateRows (rowLookup, rows) { return rows.map((row, y) => { const tr = rowLookup[y] || (rowLookup[y] = document.createElement('tr')) const cellLookup = tr._lookup || (tr._lookup = []) setChildren(tr, updateCells(cellLookup, row)) return tr })}function updateCells (cellLookup, cells) { return cells.map((cell, x) => { const td = cellLookup[x] || (cellLookup[x] = document.createElement('td')) td.textContent = cell return td })}function setChildren (parent, children) { let traverse = parent.firstChild for (let i = 0; i < children.length; i++) { const child = children[i] if (child == null) { return } if (child === traverse) { traverse = traverse.nextSibling } else if (traverse) { parent.insertBefore(child, traverse) } else { parent.appendChild(child) } } while (traverse) { const next = traverse.nextSibling parent.removeChild(traverse) traverse = next }}</code></pre><p>这里发生了两件事:</p><ul><li>1.这里有一个隐藏的元素<code>element._lookup = []</code>,用来查找子元素(可能是一个有id的元素),用这个方法我们可以重复利用已经存在的dom,更新他们</li><li>2.<code>setChildren(parent, children)</code>方法里有包含子元素的列表</li></ul><p>你还可以用<code>setChildren</code>方法来<code>mount/unmount</code>子元素</p><pre><code class="js">setChildren(login, [ email, !forgot && pass])</code></pre><h2 id="DOM树的渲染"><a href="#DOM树的渲染" class="headerlink" title="DOM树的渲染"></a>DOM树的渲染</h2><h3 id="CSS-不会阻塞-DOM-的解析"><a href="#CSS-不会阻塞-DOM-的解析" class="headerlink" title="CSS 不会阻塞 DOM 的解析"></a>CSS 不会阻塞 DOM 的解析</h3><p>首先在头部插入<code><script defer src="/js/logDiv.js"></script></code>,JS文件的内容是:</p><pre><code class="js">const div = document.querySelector('div');console.log(div);</code></pre><p><code>defer</code>属性相信大家也很熟悉了,MDN对此的描述是用来通知浏览器该脚本将在文档完成解析后,触发 <code>DOMContentLoaded</code> 事件前执行。设置这个属性,能保证DOM解析后马上打印出div。</p><p>浏览器是解析DOM生成<code>DOM Tree</code>,结合CSS生成的<code>CSS Tree</code>,最终组成<code>render tree</code>,再渲染页面。由此可见,在此过程中CSS完全无法影响<code>DOM Tree</code>,因而无需阻塞DOM解析。然而,<code>DOM Tree</code>和<code>CSS Tree</code>会组合成<code>render tree</code>,那CSS会不会页面阻塞渲染呢?</p><h3 id="CSS-阻塞页面渲染"><a href="#CSS-阻塞页面渲染" class="headerlink" title="CSS 阻塞页面渲染"></a>CSS 阻塞页面渲染</h3><p>如果脚本的内容是获取元素的样式,宽高等CSS控制的属性,浏览器是需要计算的,也就是依赖于CSS。浏览器也无法感知脚本内容到底是什么,为避免样式获取,因而只好等前面所有的样式下载完后,再执行JS。因而造成了之前例子的情况。</p><h3 id="JS-阻塞-DOM-解析"><a href="#JS-阻塞-DOM-解析" class="headerlink" title="JS 阻塞 DOM 解析"></a>JS 阻塞 DOM 解析</h3><p>浏览器遇到<code> <script></code> 标签时,会触发页面渲染</p><p>每次碰到<code><script></code>标签时,浏览器都会渲染一次页面。这是基于同样的理由,浏览器不知道脚本的内容,因而碰到脚本时,只好先渲染页面,确保脚本能获取到最新的DOM元素信息,尽管脚本可能不需要这些信息。</p>]]></content>
<summary type="html"><meta name="referrer" content="no-referrer"/>
<h2 id="DOM操作"><a href="#DOM操作" class="headerlink" title="DOM操作"></a>DOM操作</h2><p>平时中因为有各个框架的</summary>
<category term="Js" scheme="https://shinichikudo-fe.github.io/categories/Js/"/>
<category term="学习Javascript应懂得33个概念" scheme="https://shinichikudo-fe.github.io/tags/%E5%AD%A6%E4%B9%A0Javascript%E5%BA%94%E6%87%82%E5%BE%9733%E4%B8%AA%E6%A6%82%E5%BF%B5/"/>
</entry>
</feed>