-
Notifications
You must be signed in to change notification settings - Fork 1
/
O47uUnJjbJc.tw.vtt
335 lines (244 loc) · 9.26 KB
/
O47uUnJjbJc.tw.vtt
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
WEBVTT
Kind: subtitles
Language: zh-TW
00:00.480 --> 00:03.000
ExpressionChangedAfterItHasBeenCheckedError.
00:03.000 --> 00:08.720
如果你的代码在运行完变更检测
并构建好视图之后又修改了某个值,就会遇到本错误
00:08.720 --> 00:12.480
你只会在开发模式下看到本错误
因为 Angular 会运行一次额外的
00:12.480 --> 00:16.480
变更检测来捕获此类错误
此类错误会导致古怪的 UI 行为
00:16.480 --> 00:21.520
这次额外的检查可以确保应用已进入稳定态
对数据的所有更新都已经反映到了视图上
00:21.520 --> 00:25.499
有很多原因都可能导致视图处于不一致状态
00:25.499 --> 00:30.960
比如某些代码在 AfterViewInit 钩子中更新了视图
或变更检测触发器自身进入了无限循环
00:30.960 --> 00:35.369
比如某个被绑定的方法每次都返回不同的值
00:35.369 --> 00:38.212
或者某个子组件修改了其父组件/父指令上的绑定
00:38.212 --> 00:41.331
我们来看一个简单的问题复现及其解决方案
00:41.331 --> 00:45.120
然后,我们仔细看一下 Angular 的变更检测,以便理解
00:45.120 --> 00:49.600
为什么会发生本错误,以及它为何如此重要
这里是我们正在使用的 AppComponent 模板
00:49.600 --> 00:54.480
ngIf 指令带有一个布尔值 loading
在模型(也就是我们的组件 TypeScript 代码)中
00:54.480 --> 01:00.000
我们给了它一个默认值 true
然后,在 AfterViewInit 钩子中
01:00.000 --> 01:03.120
当加载完成时,我们把它的值翻转为 false
但是当我们运行此代码时,会看到报错:
01:03.120 --> 01:08.240
ExpressionChangedAfterItHasBeenCheckedError:
Previous value: false, Current value: true.
01:08.240 --> 01:12.480
在更复杂的应用中,错误的源头可能没这么清晰
01:12.480 --> 01:16.320
但是你始终可以假设,它对模板中的绑定做了点什么
01:16.320 --> 01:20.560
在调用栈追踪中,你会发现一个链接
它指向导致本错误的组件模板的源码映射
01:20.560 --> 01:24.480
并且它直接把我们带到了导致此问题的代码行
01:24.480 --> 01:29.440
也就是我们绑定到 loading 属性的 ngIf 语句
本错误在试图告诉我们
01:29.440 --> 01:34.000
这个 loading 属性的值在变更检测循环完成之后发生了变化
01:34.000 --> 01:37.120
但是在这个例子中我们的代码到底错在哪里呢?
简短的答案是我们正在使用错误的
01:37.120 --> 01:39.680
生命周期钩子
如果我们把代码从
01:39.680 --> 01:44.160
AfterViewInit 移到 OnInit 中,本错误就消失了
这些就恢复正常了
01:44.160 --> 01:48.560
换句话说,如果你发现自己正在 AfterViewInit 中修改值
01:48.560 --> 01:53.280
就可以简单地把它移到 OnInit 或组件构造函数中来修复它
01:53.280 --> 01:58.240
现在,我们已经明白使用了错误的生命周期钩子
并且可以通过重构来修复它
01:58.240 --> 02:03.040
但要想真正理解其原因,我们就要快速回顾一下
Angular 中变更检测的工作原理
02:03.040 --> 02:06.880
变更检测的目标,是让模型(TypeScript 代码)与
02:06.880 --> 02:10.880
模板(HTML)保持同步
它实现这一点的方式,是通过
02:10.880 --> 02:15.520
在组件树中自顶向下查找数据变更
首先它检查父组件,然后是子组件
02:15.520 --> 02:19.840
然后是二级子组件,以此类推。但是如果我们在
父组件完成变更检查之后再修改某个绑定
02:19.840 --> 02:24.560
Angular 就会抛出本错误
现在,我们有了一个简化的
02:24.560 --> 02:30.000
Angular 生命周期分解图
02:30.000 --> 02:33.920
首先它像此模板中的 ngIf 指令一样修改这些绑定
02:33.920 --> 02:37.600
然后,它运行 OnInit 生命周期钩子
修改 DOM,然后运行子组件的变更检测
02:37.600 --> 02:42.000
要注意,这里的最后一步是 AfterViewInit
02:42.000 --> 02:44.880
更重要的是它运行在变更检测之后
02:44.880 --> 02:48.480
基本上,这里运行的所有代码
都不应该尝试更新其视图
02:48.480 --> 02:53.600
这就是本例子中问题的根源
把它重构到 OnInit 中对于初始值是非常合适的
02:53.600 --> 02:57.840
但是如果这没能修复本错误
那可能是因为其它原因导致了本错误
02:57.840 --> 03:02.560
那就要用其它方式来修复它
在此组件中,我们正在用 ViewChild
从 DOM 树中捕获某个元素
03:02.560 --> 03:06.320
但是此元素在调用完 AfterViewInit
钩子之前是不可用的
03:06.320 --> 03:10.080
但是我们却不能在得到此 ViewChild 元素之前
03:10.080 --> 03:13.040
更新组件的状态,这下该怎么办呢?
03:13.040 --> 03:16.720
如果我们不能重构到 ngOnInit 中
我们还有另外一些选择
03:16.720 --> 03:21.440
你在 StackOverflow 答案中经常看到的方法之一
就是进行异步更新
03:21.440 --> 03:25.120
当我们进行异步更新时,它会推到下一次变更检测周期中进行
03:25.120 --> 03:29.440
这样就会防止发生错误
我们可以把它包裹进一个延迟为 0 的 setTimeout 中
03:29.440 --> 03:34.320
来把它变成异步的
这样就会把此更新推到 JavaScript
事件循环的下一个宏任务队列中
03:34.320 --> 03:39.440
另外,我可以还可以使用一个立即解析的 Promise,然后在其
03:39.440 --> 03:44.000
回调中运行此更新。这段代码会达到同样的效果
但是有一些微妙的差异:
03:44.000 --> 03:48.880
它会运行在浏览器事件循环的当前迭代结束前的
微任务队列中
03:48.880 --> 03:52.400
进行异步更新可以奏效,但是它非常隐晦
03:52.400 --> 03:57.440
只应该把它作为最后的应急措施
它无法说清楚为什么我们要写这段异步代码
03:57.440 --> 04:01.920
除非你能理解 Angular 变更检测和浏览器事件循环中的
某些细微差异
04:01.920 --> 04:06.480
幸运的是,Angular 为我们提供了另一种
更直接、更明显的方式来触发变更检测
04:06.480 --> 04:11.680
我们可以通过在组件的构造函数中注入
ChangeDetectorRef 来手动触发它
04:11.680 --> 04:16.320
然后我们就可以调用 detectChanges() 方法
来手动运行变更检测
04:16.320 --> 04:20.080
这会要求 Angular 检查此视图及其子视图
在这个例子中,这会通知它们
04:20.080 --> 04:24.240
我们的 loading 状态已经变了
这样就给了我们另一种方式以解决本错误
04:24.240 --> 04:28.560
你还可能以一种完全不同的方式遇到本错误
如果你有一个方法(通常是一个 getter)
04:28.560 --> 04:33.520
该方法会返回不可预测的值
这样就会导致变更检测进入无限循环
04:33.520 --> 04:37.200
比如,我们组件中的这个 getter 方法会返回随机数
04:37.200 --> 04:40.400
如果我们试图在模板中使用这个值
那么每当 Angular 进行变更检测时
04:40.400 --> 04:43.280
都会得到一个不同的值
这种情况下的解决方案是
04:43.280 --> 04:47.440
让该方法基于当前组件的状态返回一个确定的值
04:47.440 --> 04:51.120
换句话说,各种 getter 都应该是从组件状态衍生出来的
04:51.120 --> 04:54.800
而不能是那些不断变化的值,如时间戳或随机数
04:54.800 --> 05:00.240
现在,我们来看一个更复杂的例子
这个例子中我们同时有父组件和子组件
05:00.240 --> 05:05.040
父组件 AppComponent 包含 loading 状态
就像以前的例子中一样
05:05.040 --> 05:08.560
但是我们不在父组件中进行修改,而是改为
05:08.560 --> 05:12.880
从子组件中使用自定义事件进行修改
在子组件 ItemComponent 中
05:12.880 --> 05:17.760
我们使用 @Output 装饰器和 EventEmitter
来创建一个自定义事件
05:17.760 --> 05:21.680
接着,在 ngOnInit 期间,我们以 true 值发出一个事件
05:22.240 --> 05:26.560
然后,回到 AppComponent 的模板中
我们继续并声明 ItemComponent
05:26.560 --> 05:29.440
当自定义事件触发时
我们把 loading 值设置为 false
05:29.440 --> 05:34.080
最终的结果是我们有一个子组件
它在父组件的变更检测执行完之后修改了父组件
05:34.080 --> 05:38.080
这时候就会发生本错误
05:38.080 --> 05:42.880
这个例子的潜在解决方案之一
是把 loading 状态移到子组件中
05:42.880 --> 05:47.040
如果这样行不通,你可以考虑
把此状态移到一个共享服务中
05:47.040 --> 05:49.332
而该服务可以注入到多个组件中
05:49.332 --> 05:51.440
在结束之前,我们复述一下重点
05:51.440 --> 05:56.960
出现 ExpressionChangedAfterItHasBeenCheckedError
是因为模板中的某个值
05:56.960 --> 06:01.040
在变更检测完成之后又被修改了
先找到在调用栈追踪中给出的模板来排错
06:01.040 --> 06:04.720
从那里,你可以分析代码来决定到底是
06:04.720 --> 06:08.640
哪个值被修改了,并且使用本视频中涵盖的方法之一来解决它
06:08.640 --> 06:13.520
比如把它重构到 OnInit 钩子中、
手动使用 ChangeDetectorRef、
06:13.520 --> 06:18.080
让 getter 返回确定的值
或者把修改变成异步的,作为最后的应急手段
06:18.080 --> 06:23.200
参考 Angular 官方文档,以了解更多详情和范例