组件
组件是自定义元素,组件是封装了行为(方法)和模板(HTML 代码,用于呈现数据)的类。
组件定义
使用 ts 的 Component 装饰器,可定义一个组件,组件内部可用元数据进行注解,元数据定义组件如何工作、如何渲染。
@Component({
selector: "app-stock-item",
templateUrl: "./component.HTML",
styleUrls: ["./component.css"],
})
// 导出组件
export class StockItemComponent implements OnInit {
// 其他代码
}
组件的命名: xxxx.component.ts
,xxxx 是组件所在的目录,一般使用中划线(羊肉串)命名方式。
元数据或者属性
装饰器中与很多属性,只有选择器和模板是必选的,其他都是可选的。当 ng 遇到选择器时,会渲染 StockItemComponent
组件,即渲染模板代码。
- selector
选择器创建组件的实例,必填字段。 selector (选择器)接收一个字符串,用于提供 ng 识别组件,是在 HTML 中使用组件的方式,和 CSS 选择器类似。
- 元素选择器
selector:'tag-selector',使用:<tag-selector></tag-selector>
不能写成自闭合标签,比如
<tag-selector/>
,ng 这么要求的原因是为了使自定义元素符合 HTML5 规范。
- 类选择器
selector:'.class-selector',使用:<div class="class-selector"></div>
- 属性选择器
selector:'[prop-selector]',使用: <div prop-selector></div>
,在 HTML 中以属性的形式使用组件。
最推荐的选择器是元素选择器,这样很容易识别该选择器是一个组件。
- template
模板是用于描述 UI 的代码,用来呈现数据。
templateUrl 属性接收一个相当于组件的路径,绝对路径会报错。
还可以使用内联模板,即在 template 属性接收一串 HTML 代码作为模板。
使用路径引入模板,可以获更加好的编辑器支持。
- 组件样式
和模板类似,组件的样式可使用 styleUrls
属性从外部引入,或者使用 styles
属性写内联样式,只能有一种,它们都是数组。
ng 提倡组件的样式是完全封闭和隔离的,也就说组件中使用的样式不会影响到其他组件,也可以通过设置encapsulation
属性来改变样式的作用域。
encapsulation 有三个可选的值: ViewEncapsulation.Emulated
-- 默认值,组件内生效,会创建模板影子 DOM 和影子 root 行为的胶水代码。 ViewEncapsulation.Native
-- 使用影子 root。适用于支持的浏览器和平台。 ViewEncapsulation.None
-- 全局生效,没有任何封装。
有时候想要从外部修改组件的样式,如何做到?
什么是影子 DOM?
- 删除空白符
ng 允许从编译后的模板模板中删除的任何不必要的空白,包括多个空格、元素之间的空格,默认关闭的。 设置 preserveWhitespaces
为 true,开启。
- 改写插值符号
默认情况下,使用 作为插值标签,可在
interpolation
属性中指定其他符号,以免和其他框架崇冲突。
- 动画
不太理解这里
- 视图提供者
- 导出组件
- 变更检测
默认情况下,ng 会对 UI 中的每个绑定值检查,如果值一变化,就更新 UI。但是随着应用越来越大,默认的检查会有性能损失,而我们希望能决定更新的时机。
ng 提供了 changeDetection
实现这一点,默认值是DetectionStrategy.Default
。
把改属性的值改为 ChangeDetectionStrategy.OnPush
,就可由开发者告诉 ng 更新时机。
- 指定插值符号 interpolation
interpolation: ['[', ']'],
在模板中这样使用:[expression]
该属性用于改写默认的插值符号。不能使用 {},因为 实 ng 默认了的插值符号
组件的输入和输出
组件可重用才是我们想要的,函数会根据不同的参数得到不同的返回,组件类似,可根据不同的输入,显示不同的 UI。
ng 以装饰器的形式给出了一些钩子,这些装饰器被称为输入和输出,这些装饰器应用与类的成员变量。
可在类的成员变量上使用 Input
装饰器,在使用组件时,可以使用数据绑定语法,向组件传递数据,类似 vue 中的 prop。
组件想要向外部或者父组件传递数据,通过自定义事件的方式,类似 vue 的自定义事件。
// 导入 Input OutPut EventEmitter
import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core";
@Component({
selector: "app-input-output",
template: `
<div>
<h1>你好</h1>
<div>{{ title }}</div>
<div>{{ person.name }}</div>
<div>{{ person.age }}</div>
<button (click)="onEventEmitter($event, '你好')">触发自定义事件</button>
</div>
`,
})
export class InputOutputComponent {
@Input() public title: string;
@Input() public person: { name: string; age: number };
@Output() private eventName: EventEmitter<{
title: string;
person: { name: string; age: number };
}>; // 声明输出的类型:事件
constructor() {
this.eventName = new EventEmitter<{
title: string;
person: { name: string; age: number };
}>(); // 初始化事件对象
}
private age = 20;
onEventEmitter(event, hello): void {
// 调用 emit 触发事件
this.eventName.emit({ title: ++this.age + "", person: this.person });
}
}
把成员变量 title 通过装饰器声明为组件输入:
@Input() public title: string
成员变量的值在父组件通过数据绑定传入:
<app-input-output [title]="title"></app-input-output>
把另一个成员变量声明为输出,且类型是一个事件派发器:
// eventName 是一个事件派发器
@Output() private eventName: EventEmitter<{title: string, person: {name: string, age: number}}>
constructor(){
this.eventName = new EventEmitter<{title: string, person: {name: string, age: number}}>() // 初始化事件对象
}
private age = 20
onEventEmitter(event, hello): void{
// 调用 emit 触发事件,并传递一个参数
this.eventName.emit({title: ++this.age + '', person: this.person})
}
在组件上监听自定义事件
<app-input-output (eventName)="handler($event)"></app-input-output>
父组件的事件处理器定义:
handler({title, person}): void{
console.log('监听到自定义事件')
console.log(title, person)
}
和原生的事件类似,事件处理函数传递一个
$event
用于接收参数,如果不传递,将拿不到组件的输出。
只能接收一个参数,如果想要传递多个参数,将它们包裹成对象或者数组。
this.eventName.emit({title: ++this.age + '', person: this.person})
必须将输出初始化为一个事件派发器
输入的属性名称必须和组件中的变量名称完全相同
通过输入传递引用类型的数据,在子组件中修改改输入,父组件中也会变化
监听的自定义事件必须和组件中声明的输出完全相同
变化感知策略
每当 ng 监听到一个事件(定时器、用户交互、服务器请求),都会去遍历组件树上的所有组件,检查每个绑定值是否变更,是否需要更新 UI ,然而绑定值很多,必然有些组件的某些绑定值是无需检查的,但是每个组件都去检查,会有性能损耗,ng 提供了开发人决定是否需要检查某个组件的属性。
changeDetection
的默认值为 ChangeDetectionStrategy.Default
,将其设置为ChangeDetectionStrategy.OnPush
,组件的绑定将根据输入进行检查。
两种策略的区别,主要关注对象的输入:
OnPush 父组件修改复杂输入的属性,子组件不会更新,而 Default 会更新。
具体来说,修改输入对象的属性、修改输入数组的元素(修改元素属性、增加元素),更新策略为 OnPush 子组件不会更新,因为修改的是同一个引用,而 Default 子组件会更新。
更多信息:
Angular Change Detection - How Does It Really Work?
Understanding Change Detection Strategy in Angular
How to Use Change Detection in Angular
生命周期
组件和指令拥有自己的生命周期,在创建、渲染、更新和销毁的特定时期,执行特定的函数。当组件呈现之后,就会为每个子组件启动生命周期,直到整个应用渲染。
组件的生命周期,执行顺序是这样的:
constructor
ngOnChanges
ngOnInit
ngDoCheck
ngAfterContentInit
ngAfterContentChecked
ngAfterViewInit
ngAfterViewChecked
ngDestroy
constructor
不属于生命周期,会第一个执行,用于初始化组件类。
constructor
、ngDestroy
和其他带有 init
的函数,在整个生命周期中,只会执行一次。
每个生命周期都有一个接口,想要在某个生命周期中执行某个操作,就得实现该接口,生命周期函数去掉ng
就是相应接口的名字。
接口 | 方法 | Component or 指令 | 执行时机 |
---|---|---|---|
OnChanges | ngOnChanges(changes:SimpleChanges) | 组件、指令 | 指令被 set 后调用,每当输入属性的值变化时调用,首次调用一定在ngOnInit 之前 |
OnInit | ngOnInit() | 组件、指令 | 初始化时执行,只会执行一次即第一轮 ngOnChanges 完成后调用,也就是每个输入属性都触发了一次 ngOnChanges 后调用。此处可调用服务器接口,从关注隔离和可测试的角度看,在该函数中请求接口,比构造函数更适合。 |
DoCheck | ngDoCheck() | 组件、指令 | 每个变更检测周期中调用。紧跟在每次执行变更检测时的 ngOnChanges() 和 首次执行变更检测时的 ngOnInit() 后调用。 |
AfterContentInit | ngAfterContentInit() | 组件 | 内容投影进组件后调用,在组件整个生命周期中,只会执行一次,没有投影,立即调用。 |
AfterContentChecked | ngAfterContentChecked() | 组件 | 投影内容变更检测后触发。初始化过程,在 AfterContentInit 之后调用 |
AfterViewInit | ngAfterViewInit() | 组件 | 初始化组件视图及其子视图后触发,是 AfterContentInit 的补充 |
AfterViewChecked | ngAfterViewChecked() | 组件 | 组件视图及其子视图完成变更检测后调用。 |
OnDestroy | ngOnDestroy() | 组件、指令 | 组件或指令即将被销毁并从 UI 中移除时调用。可进行一些清理工作,比如清除定时器 |
为何 ngOnChanges 先于 ngOnInit 执行?
因为 ng 把类的初始化当成属性的变更,属性一变更,ngChanges 就马上执行。