-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
2485 lines (1081 loc) · 284 KB
/
index.html
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
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html class="theme-next pisces use-motion" lang="zh-Hans">
<head><meta name="generator" content="Hexo 3.9.0">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="theme-color" content="#222">
<meta http-equiv="Cache-Control" content="no-transform">
<meta http-equiv="Cache-Control" content="no-siteapp">
<link href="/lib/fancybox/source/jquery.fancybox.css?v=2.1.5" rel="stylesheet" type="text/css">
<link href="/lib/font-awesome/css/font-awesome.min.css?v=4.6.2" rel="stylesheet" type="text/css">
<link href="/css/main.css?v=5.1.4" rel="stylesheet" type="text/css">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png?v=5.1.4">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png?v=5.1.4">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png?v=5.1.4">
<link rel="mask-icon" href="/images/logo.svg?v=5.1.4" color="#222">
<meta name="keywords" content="Hexo, NexT">
<link rel="alternate" href="/atom.xml" title="Chasony's Blog" type="application/atom+xml">
<meta name="description" content="毕业时给你的“一张纸”对你来说没什么意义,这些年的成长才是你唯一能带走的东西">
<meta name="keywords" content="ChenShuoYu">
<meta property="og:type" content="website">
<meta property="og:title" content="Chasony's Blog">
<meta property="og:url" content="http://yoursite.com/index.html">
<meta property="og:site_name" content="Chasony's Blog">
<meta property="og:description" content="毕业时给你的“一张纸”对你来说没什么意义,这些年的成长才是你唯一能带走的东西">
<meta property="og:locale" content="zh-Hans">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Chasony's Blog">
<meta name="twitter:description" content="毕业时给你的“一张纸”对你来说没什么意义,这些年的成长才是你唯一能带走的东西">
<script type="text/javascript" id="hexo.configurations">
var NexT = window.NexT || {};
var CONFIG = {
root: '/',
scheme: 'Pisces',
version: '5.1.4',
sidebar: {"position":"left","display":"post","offset":12,"b2t":false,"scrollpercent":false,"onmobile":false},
fancybox: true,
tabs: true,
motion: {"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},
duoshuo: {
userId: '0',
author: '博主'
},
algolia: {
applicationID: '',
apiKey: '',
indexName: '',
hits: {"per_page":10},
labels: {"input_placeholder":"Search for Posts","hits_empty":"We didn't find any results for the search: ${query}","hits_stats":"${hits} results found in ${time} ms"}
}
};
</script>
<link rel="canonical" href="http://yoursite.com/">
<title>Chasony's Blog</title>
</head>
<body itemscope itemtype="http://schema.org/WebPage" lang="zh-Hans">
<div class="container sidebar-position-left
page-home">
<div class="headband"></div>
<header id="header" class="header" itemscope itemtype="http://schema.org/WPHeader">
<div class="header-inner"><div class="site-brand-wrapper">
<div class="site-meta ">
<div class="custom-logo-site-title">
<a href="/" class="brand" rel="start">
<span class="logo-line-before"><i></i></span>
<span class="site-title">Chasony's Blog</span>
<span class="logo-line-after"><i></i></span>
</a>
</div>
<p class="site-subtitle">每个人都是一部终生学习的机器</p>
</div>
<div class="site-nav-toggle">
<button>
<span class="btn-bar"></span>
<span class="btn-bar"></span>
<span class="btn-bar"></span>
</button>
</div>
</div>
<nav class="site-nav">
<ul id="menu" class="menu">
<li class="menu-item menu-item-home">
<a href="/" rel="section">
<i class="menu-item-icon fa fa-fw fa-home"></i> <br>
首页
</a>
</li>
<li class="menu-item menu-item-react">
<a href="/categories/reactjs" rel="section">
<i class="menu-item-icon fa fa-fw fa-reactjs"></i> <br>
react
</a>
</li>
<li class="menu-item menu-item-vue">
<a href="/categories/vuejs" rel="section">
<i class="menu-item-icon fa fa-fw fa-vuejs"></i> <br>
vue
</a>
</li>
<li class="menu-item menu-item-javascript">
<a href="/categories/js" rel="section">
<i class="menu-item-icon fa fa-fw fa-js"></i> <br>
javascript
</a>
</li>
</ul>
</nav>
</div>
</header>
<main id="main" class="main">
<div class="main-inner">
<div class="content-wrap">
<div id="content" class="content">
<section id="posts" class="posts-expand">
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2020/04/06/Array的变化侦测/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="ChenShuoYu">
<meta itemprop="description" content>
<meta itemprop="image" content="/images/avatar.gif">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Chasony's Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2020/04/06/Array的变化侦测/" itemprop="url">Array的变化侦测</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2020-04-06T10:04:51+08:00">
2020-04-06
</time>
</span>
<span class="post-category">
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/vuejs/" itemprop="url" rel="index">
<span itemprop="name">vuejs</span>
</a>
</span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<h2 id="1-前言"><a href="#1-前言" class="headerlink" title="1. 前言"></a>1. 前言</h2><p>为什么Object数据和Array型数据会有两种不同的变化侦测方式?</p>
<p>这是因为对于Object数据我们使用的是JS提供的对象原型上的方法Object.defineProperty,而这个方法是对象原型上的,所以Array无法使用这个方法,所以我们需要对Array型数据设计一套另外的变化侦测机制。</p>
<p>在这里,有的同学就有疑问了,我用Object.defineProperty同样可以监测到Array型数据的变化呀,例如如下代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> obj = { <span class="attr">arr</span>: [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>] }</span><br><span class="line"><span class="built_in">Object</span>.defineProperty(obj.arr, <span class="string">'0'</span>, {</span><br><span class="line"> <span class="keyword">set</span>: function (newValue) {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'数据被修改了'</span>)</span><br><span class="line"> value = newValue</span><br><span class="line"> },</span><br><span class="line"> <span class="keyword">get</span>: function () {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'数据被读取了'</span>)</span><br><span class="line"> } </span><br><span class="line">})</span><br><span class="line"></span><br><span class="line">obj.arr[<span class="number">0</span>] <span class="comment">// 数据被读取了</span></span><br><span class="line">obj.arr[<span class="number">0</span>] = <span class="number">5</span> <span class="comment">// 数据被修改了</span></span><br></pre></td></tr></table></figure>
<p>为什么还要对Array型数据设计一套另外的变化侦测机制呢?</p>
<p>这个问题问的好。对,这个例子没有错,Array本质上也是Object,我们甚至可以把一个Array看作是如下样子的Object(这样显然是不准确的,但是便于我们理解):</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> arr = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line"><span class="comment">// =></span></span><br><span class="line"><span class="keyword">let</span> arrObj = {</span><br><span class="line"> <span class="string">"0"</span>:<span class="number">1</span>,</span><br><span class="line"> <span class="string">"1"</span>:<span class="number">2</span>,</span><br><span class="line"> <span class="string">"2"</span>:<span class="number">3</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以看到,如果把arr看成arrObj,那么我们就可以使用Object.defineProperty来监测arr的变化。另外我们还知道,Object.defineProperty监测Object型数据时是给Object型数据的每个key/value添加上了getter和setter,这样,对于Object型数据我们在通过key值取值或设置值时就可以被监测到。</p>
<p>同理,我们仔细观察,数组arr的索引值恰好就是arrObj的key值,所以我们通过数组的索引值来操作数组时是可以用Object.defineProperty监测到的。<strong>但是,数组并不是只能由索引值来操作数组,更常用的操作数组的方法是使用数组原型上的一些方法如push,shift等来操作数组,当使用这些数组原型方法来操作数组时,Object.defineProperty就监测不到了,所以Vue对Array型数据单独设计了数据监测方式</strong>。</p>
<p>万变不离其宗,虽然对Array型数据设计了新的变化侦测机制,但是其根本思路还是不变的。那就是:还是在获取数据时收集依赖,数据变化时通知依赖更新。</p>
<p>下面我们就通过源码来看看Vue对Array型数据到底是如何进行变化侦测的。</p>
<h2 id="2-在哪里收集依赖"><a href="#2-在哪里收集依赖" class="headerlink" title="2. 在哪里收集依赖"></a>2. 在哪里收集依赖</h2><p>首先还是老规矩,我们得先把用到Array型数据的地方作为依赖收集起来,那么第一问题就是该在哪里收集呢?</p>
<p>其实Array型数据的依赖收集方式和Object数据的依赖收集方式相同,都是在getter中收集。那么问题就来了,不是说Array无法使用Object.defineProperty方法吗?无法使用怎么还在getter中收集依赖呢?</p>
<p>其实不然,我们回想一下平常在开发的时候,在组件的data中是不是都这么写的:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">data(){</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> arr:[<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>想想看,arr这个数据始终都存在于一个object数据对象中,而且我们也说了,谁用到了数据谁就是依赖,那么要用到arr这个数据,是不是得先从object数据对象中获取一下arr数据,而从object数据对象中获取arr数据自然就会触发arr的getter,所以我们就可以在getter中收集依赖。</p>
<p>总结一句话就是:<strong>Array型数据还是在getter中收集依赖</strong>。</p>
<h2 id="3-使Array型数据可观测"><a href="#3-使Array型数据可观测" class="headerlink" title="3. 使Array型数据可观测"></a>3. 使Array型数据可观测</h2><p>上一章节中我们知道了Array型数据还是在getter中收集依赖,换句话说就是我们已经知道了Array型数据何时被读取了。</p>
<p>回想上一篇文章中介绍Object数据变化侦测的时候,我们先让Object数据变的可观测,即我们能够知道数据什么时候被读取了、什么时候发生变化了。同理,对于Array型数据我们也得让它变的可观测,目前我们已经完成了一半可观测,即我们只知道了Array型数据何时被读取了,而何时发生变化我们无法知道,那么接下来我们就来解决这一问题:当Array型数据发生变化时我们如何得知?</p>
<h4 id="3-1-思路分析"><a href="#3-1-思路分析" class="headerlink" title="3.1 思路分析"></a>3.1 思路分析</h4><p>Object的变化时通过setter来追踪的,只有某个数据发生了变化,就一定会触发这个数据上的setter。但是Array型数据没有setter,怎么办?</p>
<p>我们试想一下,要想让Array型数据发生变化,那必然是操作了Array,而JS中提供的操作数组的方法就那么几种,我们可以把这些方法都重写一遍,在不改变原有功能的前提下,我们为其新增一些其他功能,例如下面这个例子:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> arr = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line">arr.push(<span class="number">4</span>)</span><br><span class="line"><span class="built_in">Array</span>.prototype.newPush = <span class="function"><span class="keyword">function</span>(<span class="params">val</span>)</span>{</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'arr被修改了'</span>)</span><br><span class="line"> <span class="keyword">this</span>.push(val)</span><br><span class="line">}</span><br><span class="line">arr.newPush(<span class="number">4</span>)</span><br></pre></td></tr></table></figure>
<p>在上面这个例子中,我们针对数组的原生push方法定义个一个新的newPush方法,这个newPush方法内部调用了原生push方法,这样就保证了新的newPush方法跟原生push方法具有相同的功能,而且我们还可以在新的newPush方法内部干一些别的事情,比如通知变化。</p>
<p>是不是很巧妙?Vue内部就是这么干的。</p>
<h4 id="3-2-数组方法拦截器"><a href="#3-2-数组方法拦截器" class="headerlink" title="3.2 数组方法拦截器"></a>3.2 数组方法拦截器</h4><p>基于上一小节的思想,在Vue中创建了一个数组方法拦截器,它拦截在数组实例与Array.prototype之间,在拦截器内重写了操作数组的一些方法,当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype上的原生方法。如下图所示:<br><img src="https://img-blog.csdnimg.cn/20200330174433621.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NoYXNvbnk=,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>经过整理,Array原型中可以改变数组自身内容的方法有7个,分别是:push,pop,shift,unshift,splice,sort,reverse。那么源码中的拦截器代码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源码位置:/src/core/observer/array.js</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> arrayProto = <span class="built_in">Array</span>.prototype</span><br><span class="line"><span class="comment">// 创建一个对象作为拦截器</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> arrayMethods = <span class="built_in">Object</span>.create(arrayProto)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 改变数组自身内容的7个方法</span></span><br><span class="line"><span class="keyword">const</span> methodsToPatch = [</span><br><span class="line"> <span class="string">'push'</span>,</span><br><span class="line"> <span class="string">'pop'</span>,</span><br><span class="line"> <span class="string">'shift'</span>,</span><br><span class="line"> <span class="string">'unshift'</span>,</span><br><span class="line"> <span class="string">'splice'</span>,</span><br><span class="line"> <span class="string">'sort'</span>,</span><br><span class="line"> <span class="string">'reverse'</span></span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Intercept mutating methods and emit events</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">methodsToPatch.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">method</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> original = arrayProto[method] <span class="comment">// 缓存原生方法</span></span><br><span class="line"> <span class="built_in">Object</span>.defineProperty(arrayMethods, method, {</span><br><span class="line"> enumerable: <span class="literal">false</span>,</span><br><span class="line"> configurable: <span class="literal">true</span>,</span><br><span class="line"> writable: <span class="literal">true</span>,</span><br><span class="line"> value:<span class="function"><span class="keyword">function</span> <span class="title">mutator</span>(<span class="params">...args</span>)</span>{</span><br><span class="line"> <span class="keyword">const</span> result = original.apply(<span class="keyword">this</span>, args)</span><br><span class="line"> <span class="keyword">return</span> result</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<p>在上面的代码中,首先创建了继承自Array原型的空对象arrayMethods,接着在arrayMethods上使用object.defineProperty方法将那些可以改变数组自身的7个方法遍历逐个进行封装。最后,当我们使用push方法的时候,其实用的是arrayMethods.push,而arrayMethods.push就是封装的新函数mutator,也就后说,实标上执行的是函数mutator,而mutator函数内部执行了original函数,这个original函数就是Array.prototype上对应的原生方法。 那么,接下来我们就可以在mutator函数中做一些其他的事,比如说发送变化通知。</p>
<h4 id="3-3-使用拦截器"><a href="#3-3-使用拦截器" class="headerlink" title="3.3 使用拦截器"></a>3.3 使用拦截器</h4><p>在上一小节的图中,我们把拦截器做好还不够,还要把它挂载到数组实例与Array.prototype之间,这样拦截器才能够生效。</p>
<p>其实挂载不难,我们只需把数据的<code>__proto__</code>属性设置为拦截器arrayMethods即可,源码实现如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源码位置:/src/core/observer/index.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Observer</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span> (value) {</span><br><span class="line"> <span class="keyword">this</span>.value = value</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">Array</span>.isArray(value)) {</span><br><span class="line"> <span class="keyword">const</span> augment = hasProto</span><br><span class="line"> ? protoAugment</span><br><span class="line"> : copyAugment</span><br><span class="line"> augment(value, arrayMethods, arrayKeys)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">this</span>.walk(value)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 能力检测:判断__proto__是否可用,因为有的浏览器不支持该属性</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> hasProto = <span class="string">'__proto__'</span> <span class="keyword">in</span> {}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> arrayKeys = <span class="built_in">Object</span>.getOwnPropertyNames(arrayMethods)</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Augment an target Object or Array by intercepting</span></span><br><span class="line"><span class="comment"> * the prototype chain using __proto__</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">protoAugment</span> (<span class="params">target, src: Object, keys: any</span>) </span>{</span><br><span class="line"> target.__proto__ = src</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Augment an target Object or Array by defining</span></span><br><span class="line"><span class="comment"> * hidden properties.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="comment">/* istanbul ignore next */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">copyAugment</span> (<span class="params">target: Object, src: Object, keys: Array<string></span>) </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, l = keys.length; i < l; i++) {</span><br><span class="line"> <span class="keyword">const</span> key = keys[i]</span><br><span class="line"> def(target, key, src[key])</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上面代码中首先判断了浏览器是否支持<code>__proto__</code>,如果支持,则调用<code>protoAugment</code>函数把<code>value.__proto__</code> = <code>arrayMethods</code>;如果不支持,则调用<code>copyAugment</code>函数把拦截器中重写的7个方法循环加入到<code>value</code>上。</p>
<p>拦截器生效以后,当数组数据再发生变化时,我们就可以在拦截器中通知变化了,也就是说现在我们就可以知道数组数据何时发生变化了,OK,以上我们就完成了对Array型数据的可观测。</p>
<h2 id="4-再谈依赖收集"><a href="#4-再谈依赖收集" class="headerlink" title="4. 再谈依赖收集"></a>4. 再谈依赖收集</h2><h4 id="4-1-把依赖收集到哪里"><a href="#4-1-把依赖收集到哪里" class="headerlink" title="4.1 把依赖收集到哪里"></a>4.1 把依赖收集到哪里</h4><p>在第二章中我们说了,数组数据的依赖也在getter中收集,而给数组数据添加getter/setter都是在Observer类中完成的,所以我们也应该在Observer类中收集依赖,源码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源码位置:/src/core/observer/index.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Observer</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span> (value) {</span><br><span class="line"> <span class="keyword">this</span>.value = value</span><br><span class="line"> <span class="keyword">this</span>.dep = <span class="keyword">new</span> Dep() <span class="comment">// 实例化一个依赖管理器,用来收集数组依赖</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">Array</span>.isArray(value)) {</span><br><span class="line"> <span class="keyword">const</span> augment = hasProto</span><br><span class="line"> ? protoAugment</span><br><span class="line"> : copyAugment</span><br><span class="line"> augment(value, arrayMethods, arrayKeys)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">this</span>.walk(value)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上面代码中,在Observer类中实例化了一个依赖管理器,用来收集数组依赖。</p>
<h4 id="4-2-如何收集依赖"><a href="#4-2-如何收集依赖" class="headerlink" title="4.2 如何收集依赖"></a>4.2 如何收集依赖</h4><p>在第二章中我们说了,数组的依赖也在getter中收集,那么在getter中到底该如何收集呢?这里有一个需要注意的点,那就是依赖管理器定义在Observer类中,而我们需要在getter中收集依赖,也就是说我们必须在getter中能够访问到Observer类中的依赖管理器,才能把依赖存进去。源码是这么做的:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">defineReactive</span> (<span class="params">obj,key,val</span>) </span>{</span><br><span class="line"> <span class="keyword">let</span> childOb = observe(val)</span><br><span class="line"> <span class="built_in">Object</span>.defineProperty(obj, key, {</span><br><span class="line"> enumerable: <span class="literal">true</span>,</span><br><span class="line"> configurable: <span class="literal">true</span>,</span><br><span class="line"> <span class="keyword">get</span>(){</span><br><span class="line"> <span class="keyword">if</span> (childOb) {</span><br><span class="line"> childOb.dep.depend()</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> val;</span><br><span class="line"> },</span><br><span class="line"> <span class="keyword">set</span>(newVal){</span><br><span class="line"> <span class="keyword">if</span>(val === newVal){</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> val = newVal;</span><br><span class="line"> dep.notify() <span class="comment">// 在setter中通知依赖更新</span></span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Attempt to create an observer instance for a value,</span></span><br><span class="line"><span class="comment"> * returns the new observer if successfully observed,</span></span><br><span class="line"><span class="comment"> * or the existing observer if the value already has one.</span></span><br><span class="line"><span class="comment"> * 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。</span></span><br><span class="line"><span class="comment"> * 如果 Value 已经存在一个Observer实例,则直接返回它</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">observe</span> (<span class="params">value, asRootData</span>)</span>{</span><br><span class="line"> <span class="keyword">if</span> (!isObject(value) || value <span class="keyword">instanceof</span> VNode) {</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">let</span> ob</span><br><span class="line"> <span class="keyword">if</span> (hasOwn(value, <span class="string">'__ob__'</span>) && value.__ob__ <span class="keyword">instanceof</span> Observer) {</span><br><span class="line"> ob = value.__ob__</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> ob = <span class="keyword">new</span> Observer(value)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ob</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在上面代码中,我们首先通过observe函数为被获取的数据arr尝试创建一个Observer实例,在observe函数内部,先判断当前传入的数据上是否有<code>__ob__</code>属性,因为在上篇文章中说了,如果数据有<code>__ob__</code>属性,表示它已经被转化成响应式的了,如果没有则表示该数据还不是响应式的,那么就调用new Observer(value)将其转化成响应式的,并把数据对应的Observer实例返回。</p>
<p>而在defineReactive函数中,首先获取数据对应的Observer实例childOb,然后在getter中调用Observer实例上依赖管理器,从而将依赖收集起来。</p>
<h4 id="4-3-如何通知依赖"><a href="#4-3-如何通知依赖" class="headerlink" title="4.3 如何通知依赖"></a>4.3 如何通知依赖</h4><p>到现在为止,依赖已经收集好了,并且也已经存放好了,那么我们该如何通知依赖呢?</p>
<p>其实不难,在前文说过,我们应该在拦截器里通知依赖,要想通知依赖,首先要能访问到依赖。要访问到依赖也不难,因为我们只要能访问到被转化成响应式的数据<code>value</code>即可,因为<code>vaule</code>上的<code>__ob__</code>就是其对应的<code>Observer</code>类实例,有了<code>Observer</code>类实例我们就能访问到它上面的依赖管理器,然后只需调用依赖管理器的<code>dep.notify()</code>方法,让它去通知依赖更新即可。源码如下:</p>
<p>上面代码中,由于我们的拦截器是挂载到数组数据的原型上的,所以拦截器中的this就是数据value,拿到value上的Observer类实例,从而你就可以调用Observer类实例上面依赖管理器的dep.notify()方法,以达到通知依赖的目的。</p>
<p>OK,以上就基本完成了Array数据的变化侦测。</p>
<h2 id="5-深度侦测"><a href="#5-深度侦测" class="headerlink" title="5. 深度侦测"></a>5. 深度侦测</h2><p>在前文所有讲的<code>Array</code>型数据的变化侦测都仅仅说的是数组自身变化的侦测,比如给数组新增一个元素或删除数组中一个元素,而在<code>Vue</code>中,不论是<code>Object</code>型数据还是<code>Array</code>型数据所实现的数据变化侦测都是深度侦测,所谓深度侦测就是不但要侦测数据自身的变化,还要侦测数据中所有子数据的变化。举个例子:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> arr = [</span><br><span class="line"> {</span><br><span class="line"> name:<span class="string">'NLRX'</span>,</span><br><span class="line"> age:<span class="string">'18'</span></span><br><span class="line"> }</span><br><span class="line">]</span><br></pre></td></tr></table></figure>
<p>数组中包含了一个对象,如果该对象的某个属性发生了变化也应该被侦测到,这就是深度侦测。</p>
<p>这个实现起来比较简单,源码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Observer</span> </span>{</span><br><span class="line"> value: any;</span><br><span class="line"> dep: Dep;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">constructor</span> (value: any) {</span><br><span class="line"> <span class="keyword">this</span>.value = value</span><br><span class="line"> <span class="keyword">this</span>.dep = <span class="keyword">new</span> Dep()</span><br><span class="line"> def(value, <span class="string">'__ob__'</span>, <span class="keyword">this</span>)</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">Array</span>.isArray(value)) {</span><br><span class="line"> <span class="keyword">const</span> augment = hasProto</span><br><span class="line"> ? protoAugment</span><br><span class="line"> : copyAugment</span><br><span class="line"> augment(value, arrayMethods, arrayKeys)</span><br><span class="line"> <span class="keyword">this</span>.observeArray(value) <span class="comment">// 将数组中的所有元素都转化为可被侦测的响应式</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">this</span>.walk(value)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Observe a list of Array items.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> observeArray (items: <span class="built_in">Array</span><any>) {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, l = items.length; i < l; i++) {</span><br><span class="line"> observe(items[i])</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">observe</span> (<span class="params">value, asRootData</span>)</span>{</span><br><span class="line"> <span class="keyword">if</span> (!isObject(value) || value <span class="keyword">instanceof</span> VNode) {</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">let</span> ob</span><br><span class="line"> <span class="keyword">if</span> (hasOwn(value, <span class="string">'__ob__'</span>) && value.__ob__ <span class="keyword">instanceof</span> Observer) {</span><br><span class="line"> ob = value.__ob__</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> ob = <span class="keyword">new</span> Observer(value)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ob</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="6-数组新增元素的侦测"><a href="#6-数组新增元素的侦测" class="headerlink" title="6. 数组新增元素的侦测"></a>6. 数组新增元素的侦测</h2><p>对于数组中已有的元素我们已经可以将其全部转化成可侦测的响应式数据了,但是如果向数组里新增一个元素的话,我们也需要将新增的这个元素转化成可侦测的响应式数据。</p>
<p>这个实现起来也很容易,我们只需拿到新增的这个元素,然后调用<code>observe</code>函数将其转化即可。我们知道,可以向数组内新增元素的方法有3个,分别是:<code>push</code>、<code>unshift</code>、<code>splice</code>。我们只需对这3中方法分别处理,拿到新增的元素,再将其转化即可。源码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">methodsToPatch.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">method</span>) </span>{</span><br><span class="line"> <span class="comment">// cache original method</span></span><br><span class="line"> <span class="keyword">const</span> original = arrayProto[method]</span><br><span class="line"> def(arrayMethods, method, <span class="function"><span class="keyword">function</span> <span class="title">mutator</span> (<span class="params">...args</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> result = original.apply(<span class="keyword">this</span>, args)</span><br><span class="line"> <span class="keyword">const</span> ob = <span class="keyword">this</span>.__ob__</span><br><span class="line"> <span class="keyword">let</span> inserted</span><br><span class="line"> <span class="keyword">switch</span> (method) {</span><br><span class="line"> <span class="keyword">case</span> <span class="string">'push'</span>:</span><br><span class="line"> <span class="keyword">case</span> <span class="string">'unshift'</span>:</span><br><span class="line"> inserted = args <span class="comment">// 如果是push或unshift方法,那么传入参数就是新增的元素</span></span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> <span class="keyword">case</span> <span class="string">'splice'</span>:</span><br><span class="line"> inserted = args.slice(<span class="number">2</span>) <span class="comment">// 如果是splice方法,那么传入参数列表中下标为2的就是新增的元素</span></span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (inserted) ob.observeArray(inserted) <span class="comment">// 调用observe函数将新增的元素转化成响应式</span></span><br><span class="line"> <span class="comment">// notify change</span></span><br><span class="line"> ob.dep.notify()</span><br><span class="line"> <span class="keyword">return</span> result</span><br><span class="line"> })</span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<p>在上面拦截器定义代码中,如果是push或unshift方法,那么传入参数就是新增的元素;如果是splice方法,那么传入参数列表中下标为2的就是新增的元素,拿到新增的元素后,就可以调用observe函数将新增的元素转化成响应式的了。</p>
<h2 id="7-不足之处"><a href="#7-不足之处" class="headerlink" title="7. 不足之处"></a>7. 不足之处</h2><p>前文中我们说过,对于数组变化侦测是通过拦截器实现的,也就是说只要是通过数组原型上的方法对数组进行操作就都可以侦测到,但是别忘了,我们在日常开发中,还可以通过数组的下标来操作数据,如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> arr = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line">arr[<span class="number">0</span>] = <span class="number">5</span>; <span class="comment">// 通过数组下标修改数组中的数据</span></span><br><span class="line">arr.length = <span class="number">0</span> <span class="comment">// 通过修改数组长度清空数组</span></span><br></pre></td></tr></table></figure>
<h2 id="8-总结"><a href="#8-总结" class="headerlink" title="8. 总结"></a>8. 总结</h2><p>在本篇文章中,首先我们分析了对于Array型数据也在getter中进行依赖收集;其次我们发现,当数组数据被访问时我们轻而易举可以知道,但是被修改时我们却很难知道,为了解决这一问题,我们创建了数组方法拦截器,从而成功的将数组数据变的可观测。接着我们对数组的依赖收集及数据变化如何通知依赖进行了深入分析;最后我们发现Vue不但对数组自身进行了变化侦测,还对数组中的每一个元素以及新增的元素都进行了变化侦测,我们也分析了其实现原理。</p>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</div>
</article>
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2020/03/29/Object的变化侦测/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="ChenShuoYu">
<meta itemprop="description" content>
<meta itemprop="image" content="/images/avatar.gif">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Chasony's Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2020/03/29/Object的变化侦测/" itemprop="url">Object的变化侦测</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2020-03-29T11:45:23+08:00">
2020-03-29
</time>
</span>
<span class="post-category">
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/vuejs/" itemprop="url" rel="index">
<span itemprop="name">vuejs</span>
</a>
</span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<h2 id="1-前言"><a href="#1-前言" class="headerlink" title="1. 前言"></a>1. 前言</h2><p>我们知道:数据驱动视图的关键点则在于我们如何知道数据发生了变化,只要知道数据在什么时候变了,那么问题就变得迎刃而解,我们只需在数据变化的时候去通知视图更新即可。</p>
<p>要想知道数据什么时候被读取了或数据什么时候被改写了,其实不难,JS为我们提供了<code>Object.defineProperty</code>方法,通过该方法我们就可以轻松的知道数据在什么时候发生变化。</p>
<h2 id="2-使Object数据变得“可观测”"><a href="#2-使Object数据变得“可观测”" class="headerlink" title="2. 使Object数据变得“可观测”"></a>2. 使Object数据变得“可观测”</h2><p>数据的每次读和写能够被我们看的见,即我们能够知道数据什么时候被读取了或数据什么时候被改写了,我们将其称为数据变的‘可观测’。</p>
<p>要将数据变的‘可观测’,我们就要借助前言中提到的<code>Object.defineProperty</code>方法了,在本文中,我们就使用这个方法使数据变得“可观测”。</p>
<p>首先,我们定义一个数据对象car:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> car = {</span><br><span class="line"> <span class="string">'brand'</span>:<span class="string">'BMW'</span>,</span><br><span class="line"> <span class="string">'price'</span>:<span class="number">3000</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>我们定义了这个<code>car</code>的品牌<code>brand</code>是<code>BMW</code>,价格<code>price</code>是3000。现在我们可以通过<code>car.brand</code>和<code>car.price</code>直接读写这个car对应的属性值。但是,当这个<code>car</code>的属性被读取或修改时,我们并不知情。那么应该如何做才能够让car主动告诉我们,它的属性被修改了呢?</p>
<p>接下来,我们使用<code>Object.defineProperty()</code>改写上面的例子:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> car = {}</span><br><span class="line"><span class="keyword">let</span> val = <span class="number">3000</span></span><br><span class="line"><span class="built_in">Object</span>.defineProperty(car, <span class="string">'price'</span>, {</span><br><span class="line"> enumerable: <span class="literal">true</span>,</span><br><span class="line"> configurable: <span class="literal">true</span>,</span><br><span class="line"> <span class="keyword">get</span>(){</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'price属性被读取了'</span>)</span><br><span class="line"> <span class="keyword">return</span> val</span><br><span class="line"> },</span><br><span class="line"> <span class="keyword">set</span>(newVal){</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'price属性被修改了'</span>)</span><br><span class="line"> val = newVal</span><br><span class="line"> }</span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<p>通过<code>Object.defineProperty()</code>方法给<code>car</code>定义了一个<code>price</code>属性,并把这个属性的读和写分别使用<code>get()</code>和<code>set()</code>进行拦截,每当该属性进行读或写操作的时候就会触发<code>get()</code>和<code>set()</code>。如下图:<br><img src="https://img-blog.csdnimg.cn/20200329112037417.png" alt="在这里插入图片描述"><br>可以看到,<code>car</code>已经可以主动告诉我们它的属性的读写情况了,这也意味着,这个<code>car</code>的数据对象已经是“可观测”的了。</p>
<p>为了把<code>car</code>的所有属性都变得可观测,我们可以编写如下代码:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源码位置:src/core/observer/index.js</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="class"><span class="keyword">class</span> <span class="title">Observer</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span> (value) {</span><br><span class="line"> <span class="keyword">this</span>.value = value</span><br><span class="line"> <span class="comment">// 给value新增一个__ob__属性,值为该value的Observer实例</span></span><br><span class="line"> <span class="comment">// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作</span></span><br><span class="line"> def(value,<span class="string">'__ob__'</span>,<span class="keyword">this</span>)</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">Array</span>.isArray(value)) {</span><br><span class="line"> <span class="comment">// 当value为数组时的逻辑</span></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">this</span>.walk(value)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> walk (obj: <span class="built_in">Object</span>) {</span><br><span class="line"> <span class="keyword">const</span> keys = <span class="built_in">Object</span>.keys(obj)</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i < keys.length; i++) {</span><br><span class="line"> defineReactive(obj, keys[i])</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 使一个对象转化成可观测对象</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param <span class="type">{ Object }</span> </span>obj 对象</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param <span class="type">{ String }</span> </span>key 对象的key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param <span class="type">{ Any }</span> </span>val 对象的某个key的值</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">defineReactive</span> (<span class="params">obj,key,val</span>) </span>{</span><br><span class="line"> <span class="comment">// 如果只传了obj和key,那么val = obj[key]</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">arguments</span>.length === <span class="number">2</span>) {</span><br><span class="line"> val = obj[key]</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">typeof</span> val === <span class="string">'object'</span>){</span><br><span class="line"> <span class="keyword">new</span> Observer(val)</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">Object</span>.defineProperty(obj, key, {</span><br><span class="line"> enumerable: <span class="literal">true</span>,</span><br><span class="line"> configurable: <span class="literal">true</span>,</span><br><span class="line"> <span class="keyword">get</span>(){</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">`<span class="subst">${key}</span>属性被读取了`</span>);</span><br><span class="line"> <span class="keyword">return</span> val;</span><br><span class="line"> },</span><br><span class="line"> <span class="keyword">set</span>(newVal){</span><br><span class="line"> <span class="keyword">if</span>(val === newVal){</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">`<span class="subst">${key}</span>属性被修改了`</span>);</span><br><span class="line"> val = newVal;</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在上面的代码中,我们定义了<code>observer</code>类,它用来将一个正常的<code>object</code>转换成可观测的<code>object</code>。</p>
<p>并且给value新增一个<code>__ob__</code>属性,值为该<code>value</code>的<code>Observer</code>实例。这个操作相当于为<code>value</code>打上标记,表示它已经被转化成响应式了,避免重复操作</p>
<p>然后判断数据的类型,只有<code>object</code>类型的数据才会调用<code>walk</code>将每一个属性转换成<code>getter/setter</code>的形式来侦测变化。 最后,在<code>defineReactive</code>中当传入的属性值还是一个<code>object</code>时使用<code>new observer(val)</code>来递归子属性,这样我们就可以把<code>obj</code>中的所有属性(包括子属性)都转换成<code>getter/seter</code>的形式来侦测变化。 也就是说,只要我们将一个<code>object</code>传到<code>observe</code>r中,那么这个<code>object</code>就会变成可观测的、响应式的<code>object</code>。</p>
<p><code>observer</code>类位于源码的<code>src/core/observer/index.js</code>中。</p>
<p>那么现在,我们就可以这样定义<code>car</code>:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> car = <span class="keyword">new</span> Observer({</span><br><span class="line"> <span class="string">'brand'</span>:<span class="string">'BMW'</span>,</span><br><span class="line"> <span class="string">'price'</span>:<span class="number">3000</span></span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<p>这样,<code>car</code>的两个属性都变得可观测了。</p>
<h2 id="3-依赖收集"><a href="#3-依赖收集" class="headerlink" title="3. 依赖收集"></a>3. 依赖收集</h2><h4 id="3-1-什么是依赖收集"><a href="#3-1-什么是依赖收集" class="headerlink" title="3.1 什么是依赖收集"></a>3.1 什么是依赖收集</h4><p>在上一章中,我们迈出了第一步:让<code>object</code>数据变的可观测。变的可观测以后,我们就能知道数据什么时候发生了变化,那么当数据发生变化时,我们去通知视图更新就好了。那么问题又来了,视图那么大,我们到底该通知谁去变化?总不能一个数据变化了,把整个视图全部更新一遍吧,这样显然是不合理的。此时,你肯定会想到,视图里谁用到了这个数据就更新谁呗。对!你想的没错,就是这样。</p>
<p>视图里谁用到了这个数据就更新谁,我们换个优雅说法:我们把”谁用到了这个数据”称为”谁依赖了这个数据”,我们给每个数据都建一个依赖数组(因为一个数据可能被多处使用),谁依赖了这个数据(即谁用到了这个数据)我们就把谁放入这个依赖数组中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍,告诉他们:”你们依赖的数据变啦,你们该更新啦!”。这个过程就是依赖收集。</p>
<h4 id="3-2-何时收集依赖?何时通知依赖更新?"><a href="#3-2-何时收集依赖?何时通知依赖更新?" class="headerlink" title="3.2 何时收集依赖?何时通知依赖更新?"></a>3.2 何时收集依赖?何时通知依赖更新?</h4><p>明白了什么是依赖收集后,那么我们到底该在何时收集依赖?又该在何时通知依赖更新?</p>
<p>其实这个问题在上一小节中已经回答了,我们说过:谁用到了这个数据,那么当这个数据变化时就通知谁。所谓谁用到了这个数据,其实就是谁获取了这个数据,而可观测的数据被获取时会触发<code>getter</code>属性,那么我们就可以在<code>getter</code>中收集这个依赖。同样,当这个数据变化时会触发setter属性,那么我们就可以在<code>setter</code>中通知依赖更新。</p>
<p>总结一句话就是:<strong>在<code>getter</code>中收集依赖,在<code>setter</code>中通知依赖更新</strong>。</p>
<h4 id="3-3-把依赖收集到哪里"><a href="#3-3-把依赖收集到哪里" class="headerlink" title="3.3 把依赖收集到哪里"></a>3.3 把依赖收集到哪里</h4><p>明白了什么是依赖收集以及何时收集何时通知后,那么我们该把依赖收集到哪里?</p>
<p>在3.1小节中也说了,我们给每个数据都建一个依赖数组,谁依赖了这个数据我们就把谁放入这个依赖数组中。单单用一个数组来存放依赖的话,功能好像有点欠缺并且代码过于耦合。我们应该将依赖数组的功能扩展一下,更好的做法是我们应该为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。OK,到这里,我们的依赖管理器<code>Dep</code>类应运而生,代码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源码位置:src/core/observer/dep.js</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> <span class="title">Dep</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span> () {</span><br><span class="line"> <span class="keyword">this</span>.subs = []</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> addSub (sub) {</span><br><span class="line"> <span class="keyword">this</span>.subs.push(sub)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 删除一个依赖</span></span><br><span class="line"> removeSub (sub) {</span><br><span class="line"> remove(<span class="keyword">this</span>.subs, sub)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 添加一个依赖</span></span><br><span class="line"> depend () {</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">window</span>.target) {</span><br><span class="line"> <span class="keyword">this</span>.addSub(<span class="built_in">window</span>.target)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 通知所有依赖更新</span></span><br><span class="line"> notify () {</span><br><span class="line"> <span class="keyword">const</span> subs = <span class="keyword">this</span>.subs.slice()</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, l = subs.length; i < l; i++) {</span><br><span class="line"> subs[i].update()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Remove an item from an array</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">remove</span> (<span class="params">arr, item</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (arr.length) {</span><br><span class="line"> <span class="keyword">const</span> index = arr.indexOf(item)</span><br><span class="line"> <span class="keyword">if</span> (index > <span class="number">-1</span>) {</span><br><span class="line"> <span class="keyword">return</span> arr.splice(index, <span class="number">1</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在上面的依赖管理器<code>Dep</code>类中,我们先初始化了一个<code>subs</code>数组,用来存放依赖,并且定义了几个实例方法用来对依赖进行添加,删除,通知等操作。</p>
<p>有了依赖管理器后,我们就可以在<code>getter</code>中收集依赖,在<code>setter</code>中通知依赖更新了,代码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">defineReactive</span> (<span class="params">obj,key,val</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">arguments</span>.length === <span class="number">2</span>) {</span><br><span class="line"> val = obj[key]</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">typeof</span> val === <span class="string">'object'</span>){</span><br><span class="line"> <span class="keyword">new</span> Observer(val)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> dep = <span class="keyword">new</span> Dep() <span class="comment">//实例化一个依赖管理器,生成一个依赖管理数组dep</span></span><br><span class="line"> <span class="built_in">Object</span>.defineProperty(obj, key, {</span><br><span class="line"> enumerable: <span class="literal">true</span>,</span><br><span class="line"> configurable: <span class="literal">true</span>,</span><br><span class="line"> <span class="keyword">get</span>(){</span><br><span class="line"> dep.depend() <span class="comment">// 在getter中收集依赖</span></span><br><span class="line"> <span class="keyword">return</span> val;</span><br><span class="line"> },</span><br><span class="line"> <span class="keyword">set</span>(newVal){</span><br><span class="line"> <span class="keyword">if</span>(val === newVal){</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> val = newVal;</span><br><span class="line"> dep.notify() <span class="comment">// 在setter中通知依赖更新</span></span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在上述代码中,我们在<code>getter</code>中调用了<code>dep.depend()</code>方法收集依赖,在<code>setter</code>中调用<code>dep.notify()</code>方法通知所有依赖更新。</p>
<h4 id="4-依赖到底是谁"><a href="#4-依赖到底是谁" class="headerlink" title="4. 依赖到底是谁"></a>4. 依赖到底是谁</h4><p>通过上一章节,我们明白了什么是依赖?何时收集依赖?以及收集的依赖存放到何处?那么我们收集的依赖到底是谁?</p>
<p>虽然我们一直在说”谁用到了这个数据谁就是依赖“,但是这仅仅是在口语层面上,那么反应在代码上该如何来描述这个”谁“呢?</p>
<p>其实在<code>Vue</code>中还实现了一个叫做<code>Watcher</code>的类,而<code>Watcher</code>类的实例就是我们上面所说的那个”谁”。换句话说就是:谁用到了数据,谁就是依赖,我们就为谁创建一个<code>Watcher</code>实例。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的<code>Watch</code>实例,由Watcher实例去通知真正的视图。</p>
<p><code>Watcher</code>类的具体实现如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> <span class="title">Watcher</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span> (vm,expOrFn,cb) {</span><br><span class="line"> <span class="keyword">this</span>.vm = vm;</span><br><span class="line"> <span class="keyword">this</span>.cb = cb;</span><br><span class="line"> <span class="keyword">this</span>.getter = parsePath(expOrFn)</span><br><span class="line"> <span class="keyword">this</span>.value = <span class="keyword">this</span>.get()</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">get</span> () {</span><br><span class="line"> <span class="built_in">window</span>.target = <span class="keyword">this</span>;</span><br><span class="line"> <span class="keyword">const</span> vm = <span class="keyword">this</span>.vm</span><br><span class="line"> <span class="keyword">let</span> value = <span class="keyword">this</span>.getter.call(vm, vm)</span><br><span class="line"> <span class="built_in">window</span>.target = <span class="literal">undefined</span>;</span><br><span class="line"> <span class="keyword">return</span> value</span><br><span class="line"> }</span><br><span class="line"> update () {</span><br><span class="line"> <span class="keyword">const</span> oldValue = <span class="keyword">this</span>.value</span><br><span class="line"> <span class="keyword">this</span>.value = <span class="keyword">this</span>.get()</span><br><span class="line"> <span class="keyword">this</span>.cb.call(<span class="keyword">this</span>.vm, <span class="keyword">this</span>.value, oldValue)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Parse simple path.</span></span><br><span class="line"><span class="comment"> * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来</span></span><br><span class="line"><span class="comment"> * 例如:</span></span><br><span class="line"><span class="comment"> * data = {a:{b:{c:2}}}</span></span><br><span class="line"><span class="comment"> * parsePath('a.b.c')(data) // 2</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">const</span> bailRE = <span class="regexp">/[^\w.$]/</span></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">parsePath</span> (<span class="params">path</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (bailRE.test(path)) {</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> segments = path.split(<span class="string">'.'</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span> (<span class="params">obj</span>) </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i < segments.length; i++) {</span><br><span class="line"> <span class="keyword">if</span> (!obj) <span class="keyword">return</span></span><br><span class="line"> obj = obj[segments[i]]</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> obj</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>谁用到了数据,谁就是依赖,我们就为谁创建一个<code>Watcher</code>实例,在创建<code>Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中,以后这个Watcher</code>实例就代表这个依赖,当数据变化时,我们就通知<code>Watcher</code>实例,由<code>Watcher实例再去通知真正的依赖。</code><br>那么,在创建<code>Watcher</code>实例的过程中它是如何的把自己添加到这个数据对应的依赖管理器中呢?</p>
<p>下面我们分析<code>Watcher</code>类的代码实现逻辑:</p>
<ol>
<li>当实例化<code>Watcher</code>类时,会先执行其构造函数;</li>
<li>在构造函数中调用了<code>this.get()</code>实例方法;</li>
<li>在<code>get()</code>方法中,首先通过<code>window.target = this</code>把实例自身赋给了全局的一个唯一对象<code>window.target</code>上,然后通过<code>let value = this.getter.call(vm, vm)</code>获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的<code>getter</code>,上文我们说过,在<code>getter</code>里会调用<code>dep.depend()</code>收集依赖,而在<code>dep.depend()</code>中取到挂载<code>window.target</code>上的值并将其存入依赖数组中,在<code>get()</code>方法最后将<code>window.target</code>释放掉。</li>
<li>而当数据变化时,会触发数据的<code>setter</code>,在<code>setter</code>中调用了<code>dep.notify()</code>方法,在<code>dep.notify()</code>方法中,遍历所有依赖(即<code>watcher</code>实例),执行依赖的<code>update()</code>方法,也就是<code>Watcher</code>类中的<code>update()</code>实例方法,在<code>update()</code>方法中调用数据变化的更新回调函数,从而更新视图。<br>简单总结一下就是:<code>Watcher</code>先把自己设置到全局唯一的指定位置(<code>window.target</code>),然后读取数据。因为读取了数据,所以会触发这个数据的<code>getter</code>。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的<code>Watcher</code>,并把这个<code>watcher</code>收集到<code>Dep</code>中去。收集好之后,当数据发生变化时,会向<code>Dep</code>中的每个<code>Watche</code>r发送通知。通过这样的方式,<code>Watcher</code>可以主动去订阅任意一个数据的变化。为了便于理解,我们画出了其关系流程图,如下图:<br><img src="https://img-blog.csdnimg.cn/20200329114111307.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NoYXNvbnk=,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>以上,就彻底完成了对<code>Object</code>数据的侦测,依赖收集,依赖的更新等所有操作。<h2 id="5-不足之处"><a href="#5-不足之处" class="headerlink" title="5. 不足之处"></a>5. 不足之处</h2>虽然我们通过<code>Object.defineProperty</code>方法实现了对<code>object</code>数据的可观测,但是这个方法仅仅只能观测到<code>object</code>数据的取值及设置值,当我们向<code>object</code>数据里添加一对新的<code>key/value</code>或删除一对已有的<code>key/value</code>时,它是无法观测到的,导致当我们对<code>object</code>数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。</li>
</ol>
<p>当然,<code>Vue</code>也注意到了这一点,为了解决这一问题,<code>Vue</code>增加了两个全局API:<code>Vue.set</code>和<code>Vue.delete</code>,这两个API的实现原理将会在后面学习全局API的时候说到。</p>
<h2 id="6-总结"><a href="#6-总结" class="headerlink" title="6. 总结"></a>6. 总结</h2><p>首先,我们通过<code>Object.defineProperty</code>方法实现了对<code>object</code>数据的可观测,并且封装了<code>Observer</code>类,让我们能够方便的把object数据中的所有属性(包括子属性)都转换成<code>getter/seter</code>的形式来侦测变化。</p>
<p>接着,我们学习了什么是依赖收集?并且知道了在<code>getter</code>中收集依赖,在<code>setter</code>中通知依赖更新,以及封装了依赖管理器<code>Dep</code>,用于存储收集到的依赖。</p>
<p>最后,我们为每一个依赖都创建了一个<code>Watcher</code>实例,当数据发生变化时,通知<code>Watcher</code>实例,由<code>Watcher</code>实例去做真实的更新操作。</p>
<p>其整个流程大致如下:</p>
<ol>
<li><code>Data</code>通过<code>observer</code>转换成了<code>getter/setter</code>的形式来追踪变化。</li>
<li>当外界通过<code>Watcher</code>读取数据时,会触发<code>getter</code>从而将<code>Watcher</code>添加到依赖中。</li>
<li>当数据发生了变化时,会触发<code>setter</code>,从而向<code>Dep</code>中的依赖(即<code>Watcher</code>)发送通知。</li>
<li><code>Watcher</code>接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。</li>
</ol>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</div>
</article>
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2020/03/28/VueDOM-Diff/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="ChenShuoYu">
<meta itemprop="description" content>
<meta itemprop="image" content="/images/avatar.gif">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Chasony's Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2020/03/28/VueDOM-Diff/" itemprop="url">VueDOM-Diff</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2020-03-28T10:23:55+08:00">
2020-03-28
</time>
</span>
<span class="post-category">
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/vuejs/" itemprop="url" rel="index">
<span itemprop="name">vuejs</span>
</a>
</span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<h3 id="1-前言"><a href="#1-前言" class="headerlink" title="1. 前言"></a>1. 前言</h3><p>在上一篇文章介绍VNode的时候我们说了,VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点,然后就可以对比新旧两份VNode,找出差异所在,然后更新有差异的DOM节点,最终达到以最少操作真实DOM更新视图的目的。而对比新旧两份VNode并找出差异的过程就是所谓的DOM-Diff过程。DOM-Diff算法是整个虚拟DOM的核心所在,那么接下来,我们就以源码出发,深入研究一下Vue中的DOM-Diff过程是怎样的。</p>
<h3 id="2-patch"><a href="#2-patch" class="headerlink" title="2. patch"></a>2. patch</h3><p>在Vue中,把 DOM-Diff过程叫做patch过程。patch,意为“补丁”,即指对旧的VNode修补,打补丁从而得到新的VNode,非常形象哈。那不管叫什么,其本质都是把对比新旧两份VNode的过程。我们在下面研究patch过程的时候,一定把握住这样一个思想:所谓旧的VNode(即oldVNode)就是数据变化之前视图所对应的虚拟DOM节点,而新的VNode是数据变化之后将要渲染的新的视图所对应的虚拟DOM节点,所以我们要以生成的新的VNode为基准,对比旧的oldVNode,如果新的VNode上有的节点而旧的oldVNode上没有,那么就在旧的oldVNode上加上去;如果新的VNode上没有的节点而旧的oldVNode上有,那么就在旧的oldVNode上去掉;如果某些节点在新的VNode和旧的oldVNode上都有,那么就以新的VNode为准,更新旧的oldVNode,从而让新旧VNode相同。<br>总之一句话:<strong>以新的VNode为基准,改造旧的oldVNode使之成为跟新的VNode一样,这就是patch过程要干的事</strong>。</p>
<p>整个patch无非就是干三件事:</p>
<ul>
<li>创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。</li>
<li>删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。</li>
<li>更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode。<h3 id="3-创建节点"><a href="#3-创建节点" class="headerlink" title="3. 创建节点"></a>3. 创建节点</h3>在上篇文章中我们分析了,VNode类可以描述6种类型的节点,而实际上只有3种类型的节点能够被创建并插入到DOM中,它们分别是:元素节点、文本节点、注释节点。所以Vue在创建节点的时候会判断在新的VNode中有而旧的oldVNode中没有的这个节点是属于哪种类型的节点,从而调用不同的方法创建并插入到DOM中。</li>
</ul>
<p>其实判断起来也不难,因为这三种类型的节点其特点非常明显,在源码中是怎么判断的:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源码位置: /src/core/vdom/patch.js</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createElm</span> (<span class="params">vnode, parentElm, refElm</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> data = vnode.data</span><br><span class="line"> <span class="keyword">const</span> children = vnode.children</span><br><span class="line"> <span class="keyword">const</span> tag = vnode.tag</span><br><span class="line"> <span class="keyword">if</span> (isDef(tag)) {</span><br><span class="line"> vnode.elm = nodeOps.createElement(tag, vnode) <span class="comment">// 创建元素节点</span></span><br><span class="line"> createChildren(vnode, children, insertedVnodeQueue) <span class="comment">// 创建元素节点的子节点</span></span><br><span class="line"> insert(parentElm, vnode.elm, refElm) <span class="comment">// 插入到DOM中</span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (isTrue(vnode.isComment)) {</span><br><span class="line"> vnode.elm = nodeOps.createComment(vnode.text) <span class="comment">// 创建注释节点</span></span><br><span class="line"> insert(parentElm, vnode.elm, refElm) <span class="comment">// 插入到DOM中</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> vnode.elm = nodeOps.createTextNode(vnode.text) <span class="comment">// 创建文本节点</span></span><br><span class="line"> insert(parentElm, vnode.elm, refElm) <span class="comment">// 插入到DOM中</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<p>从上面代码中,我们可以看出:</p>
<ul>
<li>判断是否为元素节点只需判断该VNode节点是否有tag标签即可。如果有tag属性即认为是元素节点,则调用createElement方法创建元素节点,通常元素节点还会有子节点,那就递归遍历创建所有子节点,将所有子节点创建好之后insert插入到当前元素节点里面,最后把当前元素节点插入到DOM中。</li>
<li>判断是否为注释节点,只需判断VNode的isComment属性是否为true即可,若为true则为注释节点,则调用createComment方法创建注释节点,再插入到DOM中。</li>
<li>如果既不是元素节点,也不是注释节点,那就认为是文本节点,则调用createTextNode方法创建文本节点,再插入到DOM中。<h3 id="4-删除节点"><a href="#4-删除节点" class="headerlink" title="4.删除节点"></a>4.删除节点</h3>如果某些节点再新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除。删除节点非常简单,只需在要删除节点的父元素上调用removeChild方法即可。源码如下:</li>
</ul>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">removeNode</span> (<span class="params">el</span>) </span>{</span><br><span class="line"> <span class="keyword">const</span> parent = nodeOps.parentNode(el) <span class="comment">// 获取父节点</span></span><br><span class="line"> <span class="keyword">if</span> (isDef(parent)) {</span><br><span class="line"> nodeOps.removeChild(parent, el) <span class="comment">// 调用父节点的removeChild方法</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="5-更新节点"><a href="#5-更新节点" class="headerlink" title="5. 更新节点"></a>5. 更新节点</h3><p>创建节点和删除节点都比较简单,而更新节点就相对较为复杂一点了,其实也不算多复杂,只要理清逻辑就能理解了。</p>
<p>更新节点就是当某些节点在新的VNode和旧的oldVNode中都有时,我们就需要细致比较一下,找出不一样的地方进行更新。</p>
<p>介绍更新节点之前,我们先介绍一个小的概念,就是什么是静态节点?我们看个例子:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><p>我是不会变化的文字<<span class="regexp">/p></span></span><br></pre></td></tr></table></figure>
<p>上面这个节点里面只包含了纯文字,没有任何可变的变量,这也就是说,不管数据再怎么变化,只要这个节点第一次渲染了,那么它以后就永远不会发生变化,这是因为它不包含任何变量,所以数据发生任何变化都与它无关。我们把这种节点称之为静态节点。</p>
<p>OK,有了这个概念以后,我们开始更新节点。更新节点的时候我们需要对以下3种情况进行判断并分别处理:</p>
<ol>
<li>如果VNode和oldVNode均为静态节点</li>
</ol>
<p>我们说了,静态节点无论数据发生任何变化都与它无关,所以都为静态节点的话则直接跳过,无需处理。</p>
<ol start="2">
<li>如果VNode是文本节点</li>
</ol>
<p>如果VNode是文本节点即表示这个节点内只包含纯文本,那么只需看oldVNode是否也是文本节点,如果是,那就比较两个文本是否不同,如果不同则把oldVNode里的文本改成跟VNode的文本一样。如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把它改成文本节点,并且文本内容跟VNode相同。</p>
<ol start="3">
<li>如果VNode是元素节点</li>
</ol>
<p>如果VNode是元素节点,则又细分以下两种情况:</p>
<ul>
<li>该节点包含子节点</li>
</ul>
<p>如果新的节点内包含了子节点,那么此时要看旧的节点是否包含子节点,如果旧的节点里也包含了子节点,那就需要递归对比更新子节点;如果旧的节点里不包含子节点,那么这个旧节点有可能是空节点或者是文本节点,如果旧的节点是空节点就把新的节点里的子节点创建一份然后插入到旧的节点里面,如果旧的节点是文本节点,则把文本清空,然后把新的节点里的子节点创建一份然后插入到旧的节点里面。</p>
<ul>
<li>该节点不包含子节点</li>
</ul>
<p>如果该节点不包含子节点,同时它又不是文本节点,那就说明该节点是个空节点,那就好办了,不管旧节点之前里面都有啥,直接清空即可。</p>
<p>OK,处理完以上3种情况,更新节点就算基本完成了,接下来我们看下源码中具体是怎么实现的,源码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 更新节点</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">patchVnode</span> (<span class="params">oldVnode, vnode, insertedVnodeQueue, removeOnly</span>) </span>{</span><br><span class="line"> <span class="comment">// vnode与oldVnode是否完全一样?若是,退出程序</span></span><br><span class="line"> <span class="keyword">if</span> (oldVnode === vnode) {</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> elm = vnode.elm = oldVnode.elm</span><br><span class="line"></span><br><span class="line"> <span class="comment">// vnode与oldVnode是否都是静态节点?若是,退出程序</span></span><br><span class="line"> <span class="keyword">if</span> (isTrue(vnode.isStatic) &&</span><br><span class="line"> isTrue(oldVnode.isStatic) &&</span><br><span class="line"> vnode.key === oldVnode.key &&</span><br><span class="line"> (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))</span><br><span class="line"> ) {</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> oldCh = oldVnode.children</span><br><span class="line"> <span class="keyword">const</span> ch = vnode.children</span><br><span class="line"> <span class="comment">// vnode有text属性?若没有:</span></span><br><span class="line"> <span class="keyword">if</span> (isUndef(vnode.text)) {</span><br><span class="line"> <span class="comment">// vnode的子节点与oldVnode的子节点是否都存在?</span></span><br><span class="line"> <span class="keyword">if</span> (isDef(oldCh) && isDef(ch)) {</span><br><span class="line"> <span class="comment">// 若都存在,判断子节点是否相同,不同则更新子节点</span></span><br><span class="line"> <span class="keyword">if</span> (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 若只有vnode的子节点存在</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (isDef(ch)) {</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 判断oldVnode是否有文本?</span></span><br><span class="line"><span class="comment"> * 若没有,则把vnode的子节点添加到真实DOM中</span></span><br><span class="line"><span class="comment"> * 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">if</span> (isDef(oldVnode.text)) nodeOps.setTextContent(elm, <span class="string">''</span>)</span><br><span class="line"> addVnodes(elm, <span class="literal">null</span>, ch, <span class="number">0</span>, ch.length - <span class="number">1</span>, insertedVnodeQueue)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 若只有oldnode的子节点存在</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (isDef(oldCh)) {</span><br><span class="line"> <span class="comment">// 清空DOM中的子节点</span></span><br><span class="line"> removeVnodes(elm, oldCh, <span class="number">0</span>, oldCh.length - <span class="number">1</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 若vnode和oldnode都没有子节点,但是oldnode中有文本</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (isDef(oldVnode.text)) {</span><br><span class="line"> <span class="comment">// 清空oldnode文本</span></span><br><span class="line"> nodeOps.setTextContent(elm, <span class="string">''</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 若有,vnode的text属性与oldVnode的text属性是否相同?</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (oldVnode.text !== vnode.text) {</span><br><span class="line"> <span class="comment">// 若不相同:则用vnode的text替换真实DOM的文本</span></span><br><span class="line"> nodeOps.setTextContent(elm, vnode.text)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上面代码里注释已经写得很清晰了,接下来我们画流程图来梳理一下整个过程,流程图如下:<br><img src="https://img-blog.csdnimg.cn/20200328101807520.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NoYXNvbnk=,size_16,color_FFFFFF,t_70" alt="流程图"><br>通过对照着流程图以及代码,相信更新节点这部分逻辑你很容易就能理解了。</p>
<p>另外,你可能注意到了,如果新旧VNode里都包含了子节点,那么对于子节点的更新在代码里调用了updateChildren方法,而这个方法的逻辑到底是怎样的我们放在下一篇文章中展开学习。</p>
<h3 id="6-总结"><a href="#6-总结" class="headerlink" title="6. 总结"></a>6. 总结</h3><p>在本篇文章中我们介绍了<code>Vue</code>中的<code>DOM-Diff</code>算法:<code>patch</code>过程。我们先介绍了算法的整个思想流程,然后通过梳理算法思想,了解了整个<code>patch</code>过程干了三件事,分别是:创建节点,删除节点,更新节点。并且对每件事情都对照源码展开了细致的学习,画出了其逻辑流程图。另外对于更新节点中,如果新旧<code>VNode</code>里都包含了子节点,我们就需要细致的去更新子节点,关于更新子节点的过程我们在下一篇文章中展开学习。</p>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</div>
</article>
<article class="post post-type-normal" itemscope itemtype="http://schema.org/Article">
<div class="post-block">
<link itemprop="mainEntityOfPage" href="http://yoursite.com/2020/03/20/Vue虚拟DOM/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="name" content="ChenShuoYu">
<meta itemprop="description" content>
<meta itemprop="image" content="/images/avatar.gif">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Chasony's Blog">
</span>
<header class="post-header">
<h1 class="post-title" itemprop="name headline">
<a class="post-title-link" href="/2020/03/20/Vue虚拟DOM/" itemprop="url">Vue虚拟DOM</a></h1>
<div class="post-meta">
<span class="post-time">
<span class="post-meta-item-icon">
<i class="fa fa-calendar-o"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建于" itemprop="dateCreated datePublished" datetime="2020-03-20T09:33:49+08:00">
2020-03-20
</time>
</span>
<span class="post-category">
<span class="post-meta-divider">|</span>
<span class="post-meta-item-icon">
<i class="fa fa-folder-o"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/vuejs/" itemprop="url" rel="index">
<span itemprop="name">vuejs</span>
</a>
</span>
</span>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<h3 id="1、什么是虚拟DOM?"><a href="#1、什么是虚拟DOM?" class="headerlink" title="1、什么是虚拟DOM?"></a>1、什么是虚拟DOM?</h3><p>所谓虚拟DOM,就是用一个JS对象来描述一个DOM节点,像如下示例:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><div <span class="class"><span class="keyword">class</span></span>=<span class="string">"a"</span> id=<span class="string">"b"</span>>我是内容<<span class="regexp">/div></span></span><br><span class="line"><span class="regexp"></span></span><br><span class="line"><span class="regexp">{</span></span><br><span class="line"><span class="regexp"> tag:'div', /</span><span class="regexp">/ 元素标签</span></span><br><span class="line"><span class="regexp"> attrs:{ /</span><span class="regexp">/ 属性</span></span><br><span class="line"><span class="regexp"> class:'a',</span></span><br><span class="line"><span class="regexp"> id:'b'</span></span><br><span class="line"><span class="regexp"> },</span></span><br><span class="line"><span class="regexp"> text:'我是内容', /</span><span class="regexp">/ 文本内容</span></span><br><span class="line"><span class="regexp"> children:[] /</span><span class="regexp">/ 子元素</span></span><br><span class="line"><span class="regexp">}</span></span><br></pre></td></tr></table></figure>
<p>我们把组成一个DOM节点的必要东西通过一个JS对象表示出来,那么这个JS对象就可以用来描述这个DOM节点,我们把这个JS对象就称为是这个真实DOM节点的虚拟DOM节点。</p>
<h3 id="2、为什么要有虚拟DOM?"><a href="#2、为什么要有虚拟DOM?" class="headerlink" title="2、为什么要有虚拟DOM?"></a>2、为什么要有虚拟DOM?</h3><p>我们知道,Vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM,而操作真实DOM又是非常耗费性能的,这是因为浏览器的标准就把DOM设计的非常复杂,所以一个真正的DOM元素是非常庞大的,如下所示:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> div = <span class="built_in">document</span>.createElement(<span class="string">'div'</span>)</span><br><span class="line"><span class="keyword">let</span> str = <span class="string">''</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> key <span class="keyword">in</span> div) {</span><br><span class="line"> str += key + <span class="string">''</span></span><br><span class="line">}</span><br><span class="line"><span class="built_in">console</span>.log(str)</span><br></pre></td></tr></table></figure>
<p><img src="https://img-blog.csdnimg.cn/20200318175640578.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NoYXNvbnk=,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>上图中我们打印一个简单的空 div 标签,就打印出这么多东西,更不用说复杂的、深嵌套的 DOM 节点了。由此可见,真实的 DOM 节点数据会占据更大的内存,当我们频繁的去做 DOM 更新,会产生一定的性能问题,因为 DOM 的更新有可能带来页面的重绘或重排。</p>
<p>那么有没有什么解决方案呢?当然是有的。我们可以用 JS 的计算性能来换取操作 DOM 所消耗的性能。</p>
<p>既然我们逃不掉操作DOM这道坎,但是我们可以尽可能少的操作 DOM 。那如何在更新视图的时候尽可能少的操作 DOM 呢?最直观的思路就是我们不要盲目的去更新视图,而是通过对比数据变化前后的状态,计算出视图中哪些地方需要更新,只更新需要更新的地方,而不需要更新的地方则不需关心,这样我们就可以尽可能少的操作 DOM 了。这也就是上面所说的用 JS 的计算性能来换取操作 DOM 的性能。</p>
<p>我们可以用 JS 模拟出一个 DOM 节点,称之为虚拟 DOM 节点。当数据发生变化时,我们对比变化前后的虚拟DOM节点,通过DOM-Diff算法计算出需要更新的地方,然后去更新需要更新的视图。</p>
<p>这就是虚拟 DOM 产生的原因以及最大的用途。</p>
<p>另外,使用虚拟 DOM 也能使得 Vue 不再依赖于浏览器环境。我们可以很容易的在 Broswer 端或者服务器端操作虚拟 DOM, 需要 render 时再将虚拟 DOM 转换为真实 DOM 即可。这也使得 Vue 有了实现服务器端渲染的能力。</p>
<h3 id="3、Vue中的虚拟DOM"><a href="#3、Vue中的虚拟DOM" class="headerlink" title="3、Vue中的虚拟DOM"></a>3、Vue中的虚拟DOM</h3><p>前文我们介绍了虚拟DOM的概念以及为什么要有虚拟DOM,那么在Vue中虚拟DOM是怎么实现的呢?接下来,我们从源码出发,深入学习一下。</p>
<h5 id="3-1-VNode类"><a href="#3-1-VNode类" class="headerlink" title="3.1 VNode类"></a>3.1 VNode类</h5><p>我们说了,虚拟DOM就是用JS来描述一个真实的DOM节点。而在Vue中就存在了一个VNode类,通过这个类,我们就可以实例化出不同类型的虚拟DOM节点,源码如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="class"><span class="keyword">class</span> <span class="title">VNode</span> </span>{</span><br><span class="line"> <span class="keyword">constructor</span> (</span><br><span class="line"> tag?: string,</span><br><span class="line"> data?: VNodeData,</span><br><span class="line"> children?: ?Array<VNode>,</span><br><span class="line"> text?: string,</span><br><span class="line"> elm?: Node,</span><br><span class="line"> context?: Component,</span><br><span class="line"> componentOptions?: VNodeComponentOptions,</span><br><span class="line"> asyncFactory?: Function</span><br><span class="line"> ) {</span><br><span class="line"> <span class="keyword">this</span>.tag = tag <span class="comment">/*当前节点的标签名*/</span></span><br><span class="line"> <span class="keyword">this</span>.data = data <span class="comment">/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/</span></span><br><span class="line"> <span class="keyword">this</span>.children = children <span class="comment">/*当前节点的子节点,是一个数组*/</span></span><br><span class="line"> <span class="keyword">this</span>.text = text <span class="comment">/*当前节点的文本*/</span></span><br><span class="line"> <span class="keyword">this</span>.elm = elm <span class="comment">/*当前虚拟节点对应的真实dom节点*/</span></span><br><span class="line"> <span class="keyword">this</span>.ns = <span class="literal">undefined</span> <span class="comment">/*当前节点的名字空间*/</span></span><br><span class="line"> <span class="keyword">this</span>.context = context <span class="comment">/*当前组件节点对应的Vue实例*/</span></span><br><span class="line"> <span class="keyword">this</span>.fnContext = <span class="literal">undefined</span> <span class="comment">/*函数式组件对应的Vue实例*/</span></span><br><span class="line"> <span class="keyword">this</span>.fnOptions = <span class="literal">undefined</span></span><br><span class="line"> <span class="keyword">this</span>.fnScopeId = <span class="literal">undefined</span></span><br><span class="line"> <span class="keyword">this</span>.key = data && data.key <span class="comment">/*节点的key属性,被当作节点的标志,用以优化*/</span></span><br><span class="line"> <span class="keyword">this</span>.componentOptions = componentOptions <span class="comment">/*组件的option选项*/</span></span><br><span class="line"> <span class="keyword">this</span>.componentInstance = <span class="literal">undefined</span> <span class="comment">/*当前节点对应的组件的实例*/</span></span><br><span class="line"> <span class="keyword">this</span>.parent = <span class="literal">undefined</span> <span class="comment">/*当前节点的父节点*/</span></span><br><span class="line"> <span class="keyword">this</span>.raw = <span class="literal">false</span> <span class="comment">/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/</span></span><br><span class="line"> <span class="keyword">this</span>.isStatic = <span class="literal">false</span> <span class="comment">/*静态节点标志*/</span></span><br><span class="line"> <span class="keyword">this</span>.isRootInsert = <span class="literal">true</span> <span class="comment">/*是否作为跟节点插入*/</span></span><br><span class="line"> <span class="keyword">this</span>.isComment = <span class="literal">false</span> <span class="comment">/*是否为注释节点*/</span></span><br><span class="line"> <span class="keyword">this</span>.isCloned = <span class="literal">false</span> <span class="comment">/*是否为克隆节点*/</span></span><br><span class="line"> <span class="keyword">this</span>.isOnce = <span class="literal">false</span> <span class="comment">/*是否有v-once指令*/</span></span><br><span class="line"> <span class="keyword">this</span>.asyncFactory = asyncFactory</span><br><span class="line"> <span class="keyword">this</span>.asyncMeta = <span class="literal">undefined</span></span><br><span class="line"> <span class="keyword">this</span>.isAsyncPlaceholder = <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> child (): Component | void {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.componentInstance</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>从上面的代码中可以看出:VNode类中包含了描述一个真实DOM节点所需要的一系列属性,如tag表示节点的标签名,text表示节点中包含的文本,children表示该节点包含的子节点等。通过属性之间不同的搭配,就可以描述出各种类型的真实DOM节点。</p>
<h5 id="3-2-VNode的类型"><a href="#3-2-VNode的类型" class="headerlink" title="3.2 VNode的类型"></a>3.2 VNode的类型</h5><p>上一小节最后我们说了,通过属性之间不同的搭配,VNode类可以描述出各种类型的真实DOM节点。那么它都可以描述出哪些类型的节点呢?通过阅读源码,可以发现通过不同属性的搭配,可以描述出以下几种类型的节点。</p>
<ul>
<li>注释节点</li>
<li>文本节点</li>
<li>元素节点</li>
<li>组件节点</li>
<li>函数式组件节点</li>
<li>克隆节点</li>
</ul>
<p>接下来,我们就把这几种类型的节点描述方式从源码中一一对应起来。</p>
<p><a href="https://nlrx-wjc.github.io/Learn-Vue-Source-Code/virtualDOM/#_3-vue%E4%B8%AD%E7%9A%84%E8%99%9A%E6%8B%9Fdom" target="_blank" rel="noopener">详情见</a></p>
</div>
<footer class="post-footer">