` 元素上的 class 为 `foo baz`。
288 |
289 | ### 样式绑定/Style Binding
290 |
291 | 与特性绑定类似,不过使用的前缀不再是 `attr.` 而是 `style.`,于是我们可以绑定某个特定 CSS Class 的存在与否,例如:
292 |
293 | ```html
294 |
295 | ```
296 |
297 | 这样就能够得到 style 为 `margin: 5px;`。
298 |
299 | 不过我们还可以进一步简化,直接将单位提前到 target 部分(如果有的情况下):
300 |
301 | ```html
302 |
303 | ```
304 |
305 | 这样,我们就可以直接绑定数值而非字符串(如果本身是纯数值的情况下)。
306 |
307 | ### 事件绑定/Event Binding
308 |
309 | *事件监听是否算作 Binding 在不同的领域中略有差异,本文中为了简化概念将此作为一种绑定类型。*
310 |
311 | 除了属性绑定外,还有一个很方便的语法称为 **事件绑定(Event Binding)**,使用圆括号 `()`或者 `on-` 前缀[^8]定义,我们可以为我们的图片绑定 `click` 事件:
312 |
313 | ```html
314 |
315 | ```
316 |
317 | 或者
318 |
319 | ```html
320 |
321 | ```
322 |
323 | 事件绑定中的执行环境被称为 **模版语句(Template Statement)**,相比于模版表达式而言,允许了副作用的存在,例如我们这里使用的赋值操作。
324 |
325 | 当然,由于我无法得知之后的用户都用的什么头像,所以如果出现不适宜的内容也与本文无关哦。
326 |
327 | 实际上,模版中的事件绑定不仅能够针对该元素本身,也能指定其它的 target,例如,在 `
` 元素上增加一个全局的 `scroll` 事件监听:
328 |
329 | ```html
330 |
123'">
331 | ```
332 |
333 | 这里我们制造了一个(假的)无限滚动列表,每当滚动发生时,就对内容进行扩展。通过指定 `window` 这个 target,即使滚动事件不发生在该元素上,也能够监听到该事件的发生并进行处理。
334 |
335 | 这里的 target 仅限于针对 DOM 事件,也只有三个可能的选项:`window`,`document` 和 `body`。不过在使用时应当慎重,应当保证事件处理过程仅直接影响该模版部分的内容,否则可能会破坏可维护性。
336 |
337 | ### 双向绑定/Two-way Data Binding
338 |
339 | 不论是属性绑定还是事件绑定,数据[^9]的传递都是单向的。而有时候,为了使用上的便利,我们会把属性绑定和事件绑定这两者使用语法糖来结合,而结合后的语法,就是两者的语法之和。我们可以在图片之前增加一个 `input` 元素:
340 |
341 | ```html
342 |
343 |
344 | ```
345 |
346 | 或
347 |
348 | ```html
349 |
350 |
351 | ```
352 |
353 | 不过,这时候我们会看到控制台的报错:
354 |
355 | ```text
356 | Can't bind to 'ngModel' since it isn't a known property of 'input'.
357 | ```
358 |
359 | 这是因为我们并没有任何指令定义了 `ngModel` 这个属性输入。
360 |
361 | 其实这里也是和 AngularJS 相比明显改善了的地方,在 Angular 模版中,属性绑定是 **强类型** 的,即所有指令都会定义自己所需的属性输入,而如果我们在某个地方使用了任何指令都没有定义过的属性,Angular 就能在编译时发现错误,有效地缩短了反馈时间,提高开发效率。
362 |
363 | 可是问题来了,为什么 Angular 自带的 `ngModel` 会找不到呢?
364 |
365 | 如果我们浏览 Angular 的 [API](https://angular.io/api),就能发现 [NgModel](https://angular.io/api/forms/NgModel) 处于 `forms` 这个 package 当中,并由 [FormsModule](https://angular.io/api/forms/FormsModule) 所声明。
366 |
367 | 为此,如果我们需要使用 `ngModel` 的话,就必须要导入 `FormsModule` 这个 NgModule。为此,在 `app.module.ts` 中,添加相应的内容:
368 |
369 | ```typescript
370 | /* ... */
371 | import { FormsModule } from '@angular/forms'
372 |
373 | /* ... */
374 |
375 | @NgModule({
376 | /* ... */
377 | imports: [
378 | /* ... */
379 | FormsModule
380 | ],
381 | /* ... */
382 | })
383 | export class AppModule { }
384 | ```
385 |
386 | 现在我们就能看到页面中的输入框,其中的默认值就是我们初始化的 `avatarId` 的值。
387 |
388 | 如果我们修改该数值,就能看到图片发生变化。反过来,如果我们点击图片,也会看到该数值发生变化。
389 |
390 | 为此这种形式就叫做 **双向绑定**。
391 |
392 | 所以,刚刚究竟发生了些什么呢?
393 |
394 | 对于双向绑定的语法糖,编译器会首先将其拆分成独立的属性绑定和事件绑定,其中的事件名增加 `Change` 后缀,也就是说:
395 |
396 | ```html
397 |
398 | ```
399 |
400 | 等价于:
401 |
402 | ```html
403 |
404 | ```
405 |
406 | 所以说,双向绑定中的表达式必须可以同时作为左值和右值,实际可用的运算符非常有限,仅限于 [属性访问](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors)。
407 |
408 | 由于双向绑定只是一个普通的语法糖,我们随时可以新建一个支持双向绑定的指令,但是出于工程上的考虑,大部分情况下往往会让自定义指令来支持 `ngModel`,从而复用相应逻辑,这部分内容会在表单部分覆盖。
409 |
410 | 事件绑定的模版语句中,永远会有一个 `$event` 变量可用。对于 DOM 事件而言,这就是事件本身;对于 `@Output()` 定义的事件绑定,则为相应 `EventEmitter` 或 `Subject` 的泛型类型的实例。
411 |
412 | 因此,我们所使用的 `[(ngModel)]` 就被处理成:
413 |
414 | ```html
415 |
416 | ```
417 |
418 | 所以我们也可以仅仅使用 `[ngModel]` 而不带 `(ngModelChange)` 来实现单向的属性绑定。
419 |
420 | `ngModelChange` 是由 [`NgModel`](https://angular.io/api/forms/NgModel) 这个 Directive 所定义的输出属性,对应的 `$event` 就是更新后的值[^10]。
421 |
422 | ### 宿主绑定/Host Binding
423 |
424 | 这里的类别划分方式与之前的内容并不相同,准确地说,这里的 **宿主绑定(Host Binding)** 与之前所有绑定类型的总和相并列,因为之前的绑定都可以被列入 **模版绑定(Template Binding)**。也就是说,这里的绑定方式不借助于模版。
425 |
426 | 在 `data-binding.component.ts` 中增加一个 `foo` 属性:
427 |
428 | ```typescript
429 | /* ... */
430 | export class DataBindingComponent {
431 | /* ... */
432 | @HostBinding('class.foo')
433 | foo = true
434 | }
435 | ```
436 |
437 | 接着在审查元素中,就能看到在 `
` 元素上增加了 `foo` 这个 CSS Class。不过,这里并不是通过外部组件绑定的,而是通过该组件来绑定到自身的宿主元素上。
438 |
439 | 能使用的绑定类型也与之前的方式相似:
440 |
441 | + 不使用前缀时绑定到 DOM Property;
442 | + 使用 `attr.` 前缀时绑定到 HTML Attribute;
443 | + 使用 `class.` 前缀时绑定到 CSS Class;
444 | + 使用 `style.` 前缀时绑定到 Inline CSS Style。
445 |
446 | 除了绑定值外,当然也能够绑定事件,通过 `@HostListener()` 来实现。再次增加一个方法:
447 |
448 | ```typescript
449 | /* ... */
450 | export class DataBindingComponent {
451 | /* ... */
452 | @HostListener('mouseover')
453 | onMouseOver(): void {
454 | this.avatarId = Math.floor(Math.random() * 1e6)
455 | }
456 | }
457 | ```
458 |
459 | 这样就能监听宿主元素的 `mouseover` 事件了。类似的,我们也可以使用 `@HostListener()` 来监听全局事件。
460 |
461 | ## 可能的疑惑
462 |
463 | #### 为什么 AngularJS 中不适合在 src 属性中插值?
464 |
465 | AngularJS 使用的是基于 DOM 的模版,也就是说,模版会先被浏览器渲染成 DOM 树,之后 AngularJS 通过 DOM API 来寻找使用了插值的地方并进行修改。而浏览器对 img 内容的获取是在页面渲染过程中自动进行的,所以在这种模式下,会产生对包含模版内容的错误地址(例如 https://avatars0.githubusercontent.com/u/{{avatarId}}?v=3&s=460 )进行请求,从而引发不必要的错误。
466 |
467 | #### 为什么属性绑定和事件绑定也叫输入绑定和输出绑定?
468 |
469 | 我们知道(不知道的下一节也会知道),指令的属性绑定和事件绑定分别使用 `@Input()` 和 `@Output()`(或元数据中的 `host` 属性)来定义,所以可以简单地意会为输入和输出。不过从功能上,属性绑定也是完全可以实现输出功能的,例如主动传入一个 `EventEmitter` 作为输入。
470 |
471 | 但是问题来了,这是只表明了 **输入** 和 **输出**,那么 **属性** 和 **事件** 的概念是哪里来的呢?
472 |
473 | 事实上,在现有的语法之前,Angular 使用过 `@Property()` 和 `@Event()` 作为装饰器的名称,之后才 [改为](https://github.com/angular/angular/pull/4435) 现有语法的,而且在语法的最终版本上也经过了很多激烈的讨论。除此之外,称做属性绑定和事件绑定也符合语义上的行为,便于理解。
474 |
475 | #### 属性绑定和方括号语法之间有什么关系?
476 |
477 | 概率上的相关性。属性绑定不一定使用方括号语法(或 `bind-` 前缀,下同),方括号语法也不一定是属性绑定。
478 |
479 | 不过在日常交流时,很难要求用户保持绝对的严谨性,因此在其它地方,很可能会用属性绑定来指代方括号语法或者反之。
480 |
481 | #### 方括号绑定的 target 是 Property 还是 Attribute?
482 |
483 | 在不使用 `attr.` 的情况下,绑定的都是 Property,不论是对 DOM Element 还是 Angular Directive。虽然话是这么说,不过由于太多用户分不清 HTML Attribute 和 DOM Property,也有个别特例对用户妥协了,比如 `htmlFor`,所以现在事实上也可以使用 `