Skip to content
On this page

组件

组件是自定义元素,组件是封装了行为(方法)和模板(HTML 代码,用于呈现数据)的类。

组件定义

使用 ts 的 Component 装饰器,可定义一个组件,组件内部可用元数据进行注解,元数据定义组件如何工作、如何渲染。

ts
@Component({
	selector: "app-stock-item",
	templateUrl: "./component.HTML",
	styleUrls: ["./component.css"],
})
// 导出组件
export class StockItemComponent implements OnInit {
	// 其他代码
}

组件的命名: xxxx.component.ts,xxxx 是组件所在的目录,一般使用中划线(羊肉串)命名方式。

元数据或者属性

装饰器中与很多属性,只有选择器模板是必选的,其他都是可选的。当 ng 遇到选择器时,会渲染 StockItemComponent 组件,即渲染模板代码。

  1. 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 中以属性的形式使用组件。

最推荐的选择器是元素选择器,这样很容易识别该选择器是一个组件。

  1. template

模板是用于描述 UI 的代码,用来呈现数据。

templateUrl 属性接收一个相当于组件的路径,绝对路径会报错。

还可以使用内联模板,即在 template 属性接收一串 HTML 代码作为模板。

使用路径引入模板,可以获更加好的编辑器支持。

  1. 组件样式

和模板类似,组件的样式可使用 styleUrls 属性从外部引入,或者使用 styles 属性写内联样式,只能有一种,它们都是数组。

ng 提倡组件的样式是完全封闭和隔离的,也就说组件中使用的样式不会影响到其他组件,也可以通过设置encapsulation属性来改变样式的作用域。

encapsulation 有三个可选的值: ViewEncapsulation.Emulated -- 默认值,组件内生效,会创建模板影子 DOM 和影子 root 行为的胶水代码。 ViewEncapsulation.Native -- 使用影子 root。适用于支持的浏览器和平台。 ViewEncapsulation.None -- 全局生效,没有任何封装。

有时候想要从外部修改组件的样式,如何做到?

什么是影子 DOM?

  1. 删除空白符

ng 允许从编译后的模板模板中删除的任何不必要的空白,包括多个空格、元素之间的空格,默认关闭的。 设置 preserveWhitespaces 为 true,开启。

  1. 改写插值符号

默认情况下,使用 作为插值标签,可在 interpolation 属性中指定其他符号,以免和其他框架崇冲突。

  1. 动画

不太理解这里

  1. 视图提供者
  1. 导出组件
  1. 变更检测

默认情况下,ng 会对 UI 中的每个绑定值检查,如果值一变化,就更新 UI。但是随着应用越来越大,默认的检查会有性能损失,而我们希望能决定更新的时机。

ng 提供了 changeDetection 实现这一点,默认值是DetectionStrategy.Default

把改属性的值改为 ChangeDetectionStrategy.OnPush,就可由开发者告诉 ng 更新时机

  1. 指定插值符号 interpolation

interpolation: ['[', ']'],

在模板中这样使用:[expression]

该属性用于改写默认的插值符号。不能使用 {},因为 实 ng 默认了的插值符号

组件的输入和输出

组件可重用才是我们想要的,函数会根据不同的参数得到不同的返回,组件类似,可根据不同的输入,显示不同的 UI。

ng 以装饰器的形式给出了一些钩子,这些装饰器被称为输入和输出,这些装饰器应用与类的成员变量。

可在类的成员变量上使用 Input 装饰器,在使用组件时,可以使用数据绑定语法,向组件传递数据,类似 vue 中的 prop。

组件想要向外部或者父组件传递数据,通过自定义事件的方式,类似 vue 的自定义事件。

ts
// 导入 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 通过装饰器声明为组件输入:

ts
@Input() public title: string

成员变量的值在父组件通过数据绑定传入:

HTML
<app-input-output [title]="title"></app-input-output>

把另一个成员变量声明为输出,且类型是一个事件派发器:

ts
// 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})
 }

在组件上监听自定义事件

HTML
<app-input-output (eventName)="handler($event)"></app-input-output>

父组件的事件处理器定义:

ts
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

生命周期

组件和指令拥有自己的生命周期,在创建、渲染、更新和销毁的特定时期,执行特定的函数。当组件呈现之后,就会为每个子组件启动生命周期,直到整个应用渲染。

组件的生命周期,执行顺序是这样的:

  1. constructor

  2. ngOnChanges

  3. ngOnInit

  4. ngDoCheck

  5. ngAfterContentInit

  6. ngAfterContentChecked

  7. ngAfterViewInit

  8. ngAfterViewChecked

  9. ngDestroy

constructor 不属于生命周期,会第一个执行,用于初始化组件类。

constructorngDestroy 和其他带有 init的函数,在整个生命周期中,只会执行一次。

每个生命周期都有一个接口,想要在某个生命周期中执行某个操作,就得实现该接口,生命周期函数去掉ng就是相应接口的名字。

接口方法Component or 指令执行时机
OnChangesngOnChanges(changes:SimpleChanges)组件、指令指令被 set 后调用,每当输入属性的值变化时调用,首次调用一定在ngOnInit 之前
OnInitngOnInit()组件、指令初始化时执行,只会执行一次即第一轮 ngOnChanges 完成后调用,也就是每个输入属性都触发了一次 ngOnChanges 后调用。此处可调用服务器接口,从关注隔离和可测试的角度看,在该函数中请求接口,比构造函数更适合。
DoCheckngDoCheck()组件、指令每个变更检测周期中调用。紧跟在每次执行变更检测时的 ngOnChanges() 和 首次执行变更检测时的 ngOnInit() 后调用。
AfterContentInitngAfterContentInit()组件内容投影进组件后调用,在组件整个生命周期中,只会执行一次,没有投影,立即调用。
AfterContentCheckedngAfterContentChecked()组件投影内容变更检测后触发。初始化过程,在 AfterContentInit 之后调用
AfterViewInitngAfterViewInit()组件初始化组件视图及其子视图后触发,是 AfterContentInit 的补充
AfterViewCheckedngAfterViewChecked()组件组件视图及其子视图完成变更检测后调用。
OnDestroyngOnDestroy()组件、指令组件或指令即将被销毁并从 UI 中移除时调用。可进行一些清理工作,比如清除定时器

为何 ngOnChanges 先于 ngOnInit 执行?

因为 ng 把类的初始化当成属性的变更,属性一变更,ngChanges 就马上执行。

Released under the MIT License.