文章目录

  • 快速入门
    • 一、Angular 环境搭建
    • 二、基本概念
      • 模块简介
      • 组件简介
      • 服务与依赖注入
      • 路由
    • 三、基础教程
      • 使用路由
      • 自定义组件
      • 常用结构型指令
      • 事件绑定
      • 模板引用
      • 模板表达式中的运算符
      • 注入服务
      • 指令概览
      • 输入和输出属性
      • 使用双向绑定
      • 组件样式
      • attribute、class 和 style 绑定
  • 服务篇
    • 依赖注入的概念
    • 依赖注入的应用
    • 提供服务
    • DI 令牌
    • 依赖提供者
  • 组件篇
    • 生命周期钩子
    • 动态创建组件
    • 组件样式
      • 视图封装
        • :host
        • :: ng-deep
        • :host-context
      • 把样式加载进组件中
    • 自定义属性型指令实践
      • HostListener
      • 向指令传递值
      • HostBinding
    • 自定义结构型指令实践
    • 自定义管道实践
  • Element篇
    • ElementRef 简介
    • TemplateRef 简介
    • ViewContainerRef 简介
    • ngTemplateOutlet 简介
    • ngComponentOutlet 简介
    • ViewChild 和 ViewChildren
    • ng-container vs ng-template
    • ng-content 内容投影
    • constructor vs ngOnInit
  • RxJS篇
    • Observable 简介
      • Observer Pattern(观察者模式)
      • Iterator Pattern(迭代器模式)
      • Observable(可观察对象)
      • Pull(拉取) vs Push(推送)
      • Observable vs Promise
    • 创建 Observable
      • Observer(观察者)
      • Subscription(订阅)
      • 常见创建操作符
      • Angular 中的 Observable
    • Subject
      • 订阅 Observable
      • RxJS Subject(主题)
      • RxJS Subject & Observable
      • BehaviorSubject
      • ReplaySubject
      • AsyncSubject
      • RxJS Subject 在 Angular 中的应用
      • 何时应该取消 Subscription?
        • 不需要手动取消的场景
        • 需要手动取消的场景
  • 参考资源

快速入门

一、Angular 环境搭建

前提条件
请先确保开发环境中包括Node.js 和 npm包管理器。

Angular需要 Node.js 版本10.9.0 或以上。Node.js 已经默认安装了 npm 客户端。想检查版本请在终端/控制台运行 node -vnpm -v

步骤

  1. 使用 npm命令全局安装 Angular CLI:npm install -g @angular/cli
  2. 使用 CLI命令创建新的项目:ng new 项目名称
选项 说明
–skipInstall=true / false 若为true,则不安装依赖包。默认:false。
–routing=true / false 若为true,则为初始项目生成路由模块。
–skipTests=true / false 若为true,则不生成“spec.ts”测试文件。默认:false。
–style= css/scss/sass/less/styl 样式文件的文件扩展名或预处理器。
  1. 进入项目目录,使用 CLI命令运行应用:ng serve

补充:
Yarn
由于本人的网络有时比较龟速,使用npm new 项目名称 或者npm install时可能会出错,因此通常使用 yarn来下载并安装 npm包。不再碰到以前 ng new 创建新项目时出现的问题(此为文章链接)。

  • 简介(中文文档)
    yarn也是一款包管理工具,它会缓存每个下载过的包,再次使用时无需重复下载。同时利用并行下载以最大化资源利用率,因此安装速度更快。
  • 安装
    使用npm安装:npm install -g yarn
  • 和npm命令的比较:
NPM YARN
npm init yarn init
npm install yarn
npm install lodash --save yarn add lodash
npm install lodash --save-dev yarn add lodash --dev
npm install lodash --global yarn global add lodash
npm uninstall yarn remove

二、基本概念

模块简介

NgModule 是一个带有 @NgModule 装饰器的类。
Angular CLI 在创建新应用时会生成一个基本模块 AppModule,该根模块就是你用来启动此应用的模块。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';import { AppComponent } from './app.component';@NgModule({declarations: [AppComponent],imports: [BrowserModule],providers: [],bootstrap: [AppComponent]
})
export class AppModule { }
  • declarations —— 该应用所拥有的组件。
  • imports —— 导入 BrowserModule 以获取浏览器特有的服务。
  • providers —— 各种服务提供者。
  • bootstrap —— 根组件,Angular 创建它并插入 index.html 宿主页面。

declarations 数组
只能接受可声明对象,包括组件、指令和管道。
imports 数组
所需模块。
providers 数组
所需服务。当直接把服务列在这里时,它们是全应用范围的。 当使用特性模块和惰性加载时,它们是范围化的。

常用模块

组件简介

一个带有 @Component() 装饰器的类,和它的伴生模板关联在一起。组件类及其模板共同定义了一个 视图。

组件是 指令 的一种特例。@Component() 装饰器扩展了 @Directive() 装饰器,增加了一些与模板有关的特性。

Angular 的组件类负责暴露数据,并通过 数据绑定机制 来处理绝大多数视图的显示和用户交互逻辑。

import { Component } from '@angular/core';@Component({selector: 'app-root',templateUrl: './app.component.html',styleUrls: ['./app.component.less']
})
export class AppComponent {title = 'angular-demo';
}

服务与依赖注入

服务 是一个广义的概念,它包括应用所需的任何值、函数或特性。

组件应该把诸如从服务器获取数据、验证用户输入或直接往控制台中写日志等工作委托给各种服务。把组件和服务区分开,以提高模块性和复用性。

依赖注入(dependency injection),简称 DI,既是设计模式,同时又是一种机制:当应用程序的一些部件(即一些依赖)需要另一些部件时, 利用依赖注入来创建被请求的部件,并将它们注入到需要它们的部件中。

在 Angular 中,依赖通常是服务,但是也可以是值,比如字符串或函数。组件是服务的消费者,要把一个类定义为服务,就要用 @Injectable() 装饰器来提供元数据,以便让 Angular 可以把它作为 依赖 注入到组件中,让组件类得以访问该服务类。

@Injectable({providedIn: 'root',
})

路由

用来配置和实现 Angular 应用中各个状态和视图之间的导航。要想实现这种导航,你可以使用 Angular 的Router(路由器)。路由器会把浏览器 URL 解释成改变视图的操作指南,以完成导航。

三、基础教程

使用路由

使用命令 ng new routing-app --routing 会生成一个带有路由模块(AppRoutingModule)的基本 Angular 应用。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';const routes: Routes = [];@NgModule({declarations: [],imports: [RouterModule.forRoot(routes)],exports: [RouterModule]
})
export class AppRoutingModule { }

默认跳转至 admin
router-outlet 指令用于告诉 Angular 在哪里加载组件,当路由匹配到响应路径,并成功找到需要加载的组件时,它将动态创建对应的组件,并将其作为兄弟元素,插入到 router-outlet 元素中。

import { Component } from '@angular/core';
import { Router } from '@angular/router';@Component({selector: 'app-root',template: `<router-outlet></router-outlet>`,styleUrls: ['./app.component.less']
})
export class AppComponent {constructor(private router: Router) {}ngOnInit() {this.router.navigateByUrl('/admin');}
}

导入路由模块和所需模块

// ...
import { AppRoutingModule } from './app-routing.module';
import { LayoutModule } from './layout/layout.module';@NgModule({declarations: [AppComponent],imports: [BrowserModule,LayoutModule,AppRoutingModule],providers: [],bootstrap: [AppComponent]
})
export class AppModule { }

配置路由信息

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from './layout.component';const routes: Routes = [{ path: 'admin', component: LayoutComponent }
];@NgModule({imports: [RouterModule.forChild(routes)]
})
export class LayoutRoutingModule { }

自定义组件

创建 LayoutComponent 组件

定义组件的元信息和定义组件类,如下:

@Component({selector: 'app-hero',templateUrl: './hero.component.html',styleUrls: ['./hero.component.less']
})
export class HeroComponent {constructor() { }ngOnInit(): void {}
}

定义 Hero 接口

interface Hero {id: string;name: string;
}

使用 Hero 接口

export class HeroComponent{address: string;hero: Hero;// ...
}

在构造函数中执行数据初始化

@Component({...})
export class HeroComponent {address: string;hero: Hero;constructor() {this.address = '华山';this.hero = {id: '中神通',name: '王重阳'}}
}

可以使用插值语法实现数据绑定。

绑定普通文本和绑定对象属性,如下:

<h2>大家好,欢迎来到 {{ address }}</h2>
<p>第一次论剑的最终胜利者是 {{hero.id}} {{hero.name}}!</p>

常用结构型指令

结构型指令的职责是 HTML 布局。通过添加和移除 DOM 元素改变 DOM 布局。

NgIf 指令
从模板中创建或销毁子视图。

<div *ngIf="hero" class="name">{{hero.name}}</div>

指令名的星号(*)前缀是语法糖。从内部实现来说,Angular 把 *ngIf 属性翻译成一个 < ng-template > 元素,并用它来包裹宿主元素,如下:

<ng-template [ngIf]="hero"><div class="name">{{hero.name}}</div>
</ng-template>

*ngIf 指令被移到了 元素上。在那里它变成了一个属性绑定 [ngIf]。
< div > 上其余部分,包括它的 class 属性在内,移到了内部的 < ng-template > 元素上。

NgFor 指令
用于基于可迭代对象中的每一项。

<li *ngFor="let hero of heroes">...</li>

使用示例

import { Component, OnInit } from '@angular/core';
import { HEROES, Hero } from '../mock-heroes';@Component({...})
export class HeroComponent {address: string;showHeroes: boolean = true;heroes: Hero[] = HEROES;constructor() {this.address = '华山';}
}
<h2>大家好,欢迎来到 {{ address }}</h2>
<div *ngIf="showHeroes"><h3>五绝名单</h3><ul><li *ngFor="let hero of heroes">{{hero.name}}</li></ul>
</div>

我们使用 let item of items; 语法迭代数组中的每一项,另外我们使用 index as i 访问数组中每一项的索引值。除此之外,我们还可以获取以下的值:

  • first: boolean —— 若当前项是可迭代对象的第一项,则返回 true
  • last: boolean —— 若当前项是可迭代对象的最后一项,则返回 true
  • even: boolean —— 若当前项的索引值是偶数,则返回 true
  • odd: boolean —— 若当前项的索引值是奇数,则返回 true

NgSwitch 指令
一组在备用视图之间切换的指令。它根据切换条件显示几个可能的元素中的一个。Angular 只会将选定的元素放入 DOM。

<div *ngFor="let hero of heroes" [ngSwitch]="hero?.name"><div *ngSwitchCase="'东邪'">...</div><div *ngSwitchCase="'西毒'">...</div><div *ngSwitchCase="'南帝'">...</div><div *ngSwitchCase="'北丐'">...</div><div *ngSwitchDefault>...</div>
</div>

NgSwitch 实际上是三个协作指令的集合:NgSwitch、NgSwitchCase 和 NgSwitchDefault。

NgSwitch 本身不是结构型指令,而是一个属性型指令,它控制其它两个 switch 指令的行为。 因此写成 [ngSwitch] 而不是 *ngSwitch。

NgSwitchCase 和 NgSwitchDefault 都是结构型指令。 因此使用星号(*)前缀来把它们附着到元素上。 *ngSwitchCase 会在它的值匹配上选项值时显示它的宿主元素。 当 NgSwitchCase 没有匹配上时,会显示*ngSwitchDefault 的宿主元素。

事件绑定

通过 (eventName) 的语法,实现事件绑定。

使用示例

import { Component, OnInit } from '@angular/core';
import { HEROES, Hero } from '../mock-heroes';@Component({...})
export class HeroComponent {// ...showSkill: boolean = false;// ...toggleSkill() {this.showSkill = !this.showSkill;}
}
<div *ngIf="showHeroes"><h3>五绝名单</h3><button (click)="toggleSkill()">{{ showSkill ? "隐藏技能" : "显示技能" }}</button><ul><li *ngFor="let hero of heroes">{{hero.name}}<span *ngIf="showSkill">:{{hero.skill}}</span></li></ul>
</div>

模板引用

可以使用 #variableName 的语法,定义模板引用。

<div><input #myInput type="text"><button (click)="onClick(myInput.value)">点击</button>
</div>

模板表达式中的运算符

管道运算符( | )
管道运算符会把左侧的表达式结果传给右侧的管道函数,从而对表达式的结果进行一些转换。

例如:

<p>Item json pipe: {{item | json}}</p>

安全导航运算符( ? )
安全导航运算符 ? 可以对在属性路径中出现 null 和 undefined 值进行保护,防止视图渲染失败。

<p>The item name is: {{item?.name}}</p>

非空断言运算符(!)
如果无法在运行类型检查器期间确定变量是否 null 或 undefined,则会抛出错误。你可以通过应用后缀非空断言运算符 ! 来告诉类型检查器不要抛出错误。

例如,在使用 *ngIf 检查过 item 是否已定义之后,就可以断言 item 属性也已定义。

<!-- Assert color is defined, even if according to the `Item` type it could be undefined. -->
<p>The item's color is: {{item.color!.toUpperCase()}}</p>

与 安全导航运算符 不同的是,非空断言运算符不会防止出现 null 或 undefined。 它只是告诉 TypeScript 的类型检查器对特定的属性表达式,不做 “严格空值检测”。

非空断言运算符 !,是可选的,但在打开严格空检查选项时必须使用它。

注入服务

组件中注入服务步骤:

  1. 创建服务
@Injectable({providedIn: 'root'
})
export class HeroService {
  1. 导入已创建的服务
import { HeroService } from '../hero.service';
  1. 在构造函数里注入服务
constructor(private heroService: HeroService) {}

使用示例

import { Injectable } from '@angular/core';
import { Hero } from './hero';@Injectable({providedIn: 'root'
})
export class HeroService {heroes: Hero[] = [{ id: 0, name: '东邪', skills: ['弹指神通'] },{ id: 1, name: '西毒', skills: ['蛤蟆功'] },{ id: 2, name: '南帝', skills: ['一阳指'] },{ id: 3, name: '北丐', skills: ['降龙十八掌'] },{ id: 4, name: '中神通', skills: ['先天功'] }];
}
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';@Component({...})
export class HeroComponent implements OnInit {// ...heroes: Hero[];constructor(private heroService: HeroService) {this.heroes = this.heroService.heroes;}
}

指令概览

在 Angular 中有三种类型的指令:

  • 组件:拥有模板的指令
  • 结构型指令:修改视图的结构的指令。
  • 属性型指令:改变元素、组件或其它指令的外观和行为的指令。

输入和输出属性

@Input()@Output() 允许 Angular 在父子组件之间共享数据。

@Input()
允许将数据从父组件输入到子组件中。使用 [message]="message" 属性绑定的语法,实现数据传递。

import { Component, OnInit, Input } from '@angular/core';@Component({selector: 'app-hero-detail',template: `<p>{{hero.name}} 信息</p>`,styleUrls: ['./hero-detail.component.less']
})
export class HeroDetailComponent implements OnInit {@Input() hero;// ...
}
<div *ngFor="let hero of heroes"><app-hero-detail [hero]="hero"></app-hero-detail>
</div>

@Output()
允许数据从子级流出到父级。通常将 @Output() 属性初始化为 Angular EventEmitter,并将值作为 事件 从组件中向外发送。

在子组件中使用 Output 装饰器

import { Component, OnInit, Output, EventEmitter } from '@angular/core';@Component({selector: 'app-hero-handle',template: `<label>Add a skill: <input #newSkill></label><button (click)="addSkill(newSkill.value)">Add to parent's skill</button>`,styleUrls: ['./hero-handle.component.less']
})
export class HeroHandleComponent implements OnInit {@Output() addNewSkill = new EventEmitter();addSkill(value: string) {this.addNewSkill.emit(value);}
}

在父组件中使用 (eventName) 事件绑定的语法,监听我们自定义的事件。当在 HeroHandleComponent 组件中点击新增按钮,将会调用 AppComponent 组件类中的 onAddSkill() 方法,更新对应信息。

<ul><li *ngFor="let hero of heroes">{{hero.name}}: <span *ngFor="let skill of hero.skills">{{skill}} </span><br/><app-hero-handle (addNewSkill)="onAddSkill(hero.id, $event)"></app-hero-handle></li>
</ul>
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';@Component({...})
export class LayoutComponent implements OnInit {heroes: Hero[];constructor(private heroService: HeroService) {this.heroes = this.heroService.heroes;}onAddSkill(id, newSkill) {if(!newSkill) returnthis.heroes.forEach(ele => {if(ele.id === id) ele.skills.push(newSkill);})}
}

使用双向绑定

双向绑定提供了一种在组件类及其模板之间共享数据的方式。

通过 [(...)] ,来实现双向绑定。

拿官网中改变文本大小的 SizerComponent 组件来举例说明。

import { Component, Input, Output, EventEmitter } from '@angular/core';@Component({selector: 'app-sizer',templateUrl: './sizer.component.html',styleUrls: ['./sizer.component.less']
})
export class SizerComponent {@Input() size: number | string;@Output() sizeChange = new EventEmitter<number>();dec() { this.resize(-1); }inc() { this.resize(+1); }resize(delta: number) {this.size = Math.min(40, Math.max(8, +this.size + delta));this.sizeChange.emit(this.size);}
}
<div><button (click)="dec()">-</button><button (click)="inc()">+</button><label [style.font-size.px]="size">FontSize: {{size}}px</label>
</div>

修改 HeroComponent

<h2>大家好,欢迎来到 {{ address }}</h2>
<div *ngIf="showHeroes" [style.font-size.px]="fontSizePx"><h3>五绝名单</h3><button (click)="toggleSkill()">{{ showSkill ? "隐藏技能" : "显示技能" }}</button><ul><li *ngFor="let hero of heroes">{{hero.name}}<span *ngIf="showSkill">:{{hero.skill}}</span></li></ul>
</div><app-sizer [(size)]="fontSizePx"></app-sizer>

在 hero.component.ts 中添加

fontSizePx = 16;

单击按钮就会通过双向绑定更新 AppComponent.fontSizePx。

双向绑定做了两件事:设置特定的元素属性和监听元素的变更事件。实际上就是 属性绑定 和 事件绑定 的语法糖。 Angular 将 SizerComponent 的绑定分解成这样:

<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>

[(ngModel)] : 双向绑定
记住,要导入 FormsModule 才能让 [(ngModel)] 可用。

<input [(ngModel)]="currentItem.name" id="example-ngModel">

为了简化语法,ngModel 指令把技术细节隐藏在其输入属性 ngModel 和输出属性 ngModelChange 的后面:

<input [ngModel]="currentItem.name" (ngModelChange)="currentItem.name=$event" id="example-change">

组件样式

设置组件元数据时通过 styles 或 styleUrls 属性,来设置组件的内联样式和外联样式。

NgClass
用 ngClass 同时添加或删除几个 CSS 类。

<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>

要添加或删除单个类,请使用类绑定而不是 NgClass。

NgStyle
通过 Angular 表达式,设置 DOM 元素的 CSS 属性。

用法:

<div [ngStyle]="{color: 'white', 'background-color': 'blue'}">...</div>

attribute、class 和 style 绑定

attribute
通常, 使用 Property 绑定设置元素的 Property 优于使用字符串设置 Attribute。但是,当没有要绑定的元素的 Property 时,可以采用 Attribute 绑定。

<button [attr.aria-label]="actionName">...</button>

类绑定
使用类绑定来为一个元素添加和移除 CSS 类。

绑定类型 语法 输入类型 输入值范例
单个类绑定 [class.foo]=“hasFoo” boolean / undefined / null true, false
多个类绑定 [class]=“classExpr” string “my-class-1 my-class-2”
: {[key: string]: boolean / undefined / null} {foo: true, bar: false}
: Array< string > [‘foo’, ‘bar’]

需要同时管理多个类名时,请考虑使用 NgClass 指令。

样式绑定
通过样式绑定来动态设置样式。

绑定类型 语法 输入类型 输入值范例
单一样式绑定 [style.width]=“width” boolean / undefined / null “100px”
带单位的单一样式绑定 [style.width.px]=“width” number / undefined / null 100
多个样式绑定 [style]=“styleExpr” string “width: 100px; height: 100px”
: {[key: string]: string / undefined / null} {width: ‘100px’, height: ‘100px’}
: Array< string > [‘width’, ‘100px’]

NgStyle 指令可以作为 [style] 绑定的替代指令。但是,应该把 [style] 样式绑定语法作为首选,因为随着 Angular 中样式绑定的改进,NgStyle 将不再提供重要的价值,并最终在未来的某个版本中删除。

服务篇

依赖注入的概念

控制反转(IoC)是一种设计思想。传统的程序设计,我们直接在对象(客户端)内部通过 new 创建对象,是客户端主动创建依赖对象。而 IoC 意味着将你设计好的对象交给容器控制,即由 IoC容器 来控制对象的创建。传统应用程序是由对象主动去创建和获取依赖对象,当由容器来创建和注入依赖对象,对象只是被动地接受依赖对象,因此,依赖对象的获取被反转了。

IoC 很好的体现了面向对象设计法则之一—— 好莱坞法则:“别打给我们,我们会打给你 (don’t call us, we’ll call you)”;

在软件工程中,依赖注入是对 IoC 的一种实现。一般情况下,如果服务A需要服务B,那就意味着服务A要在内部创建服务B的实例,也就是说服务A依赖于服务B。Angular 利用依赖注入机制改变了这一点,在该机制下,如果服务A依赖于服务B,那么我们希望服务B能被自动注入到服务A中。

依赖注入的应用

当 Angular 创建组件类的新实例时,它会查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。比如 HeroComponent 需要 HeroService:

constructor(private service: HeroService) { }

当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供者来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。
当所有请求的服务已解析并返回时,Angular 可以用这些服务实例为参数,调用该组件的构造函数。

HeroService 的注入过程如下所示:

提供服务

对于要用到的任何服务,必须至少注册一个 提供者。你可以在三种位置之一设置元数据,以便在应用的不同层级使用提供者来配置注入器:

  • 在服务本身的 @Injectable() 装饰器中:默认情况下, ng generate service 命令会在 @Injectable() 装饰器里一个名叫 providedIn 的元数据选项中把提供者注册到根注入器中。在根一级提供服务时,会创建一个单一的共享实例,并且可以把它注入到任何想要它的类中。
    这种在 @Injectable 元数据中注册提供者的方式还让 Angular 能够通过移除那些从未被用过的服务来优化大小。
@Injectable({providedIn: 'root',
})
  • 在 NgModule 的 @NgModule() 装饰器中:请用 @NgModule() 装饰器中的 providers 属性。当使用这种方式注册提供者时,该服务的同一个实例将会对该 NgModule 中的所有组件可用。
@NgModule({providers: [HeroService],...
})
  • 在组件的 @Component() 装饰器中:即在 @Component() 元数据的 providers 属性中注册服务提供者。当使用这种方式注册提供者时,会为该组件的每一个新实例提供该服务的一个新实例。
@Component({selector:    'app-hero-list',templateUrl: './hero-list.component.html',providers:  [ HeroService ]
})

DI 令牌

当使用提供者配置注入器时,就会把提供者和一个 DI 令牌关联起来。注入器维护一个内部令牌-提供者的映射表,当请求一个依赖项时就会引用它。令牌就是这个映射表的键。

通过把 HeroService 类型作为令牌,可以直接从注入器中获得一个 HeroService 实例。

heroService: HeroService;

当使用 HeroService 类的类型来定义构造函数参数时,Angular 就会知道要注入与 HeroService 类这个令牌相关的服务。

constructor(heroService: HeroService)

依赖提供者

类提供者
类提供者的语法实际上是一种简写形式,它会扩展成一个由 Provider 接口定义的提供者配置对象。如下:

providers: [Logger]
[{ provide: Logger, useClass: Logger }]

扩展的提供者配置是一个具有两个属性的对象字面量。

  • provide 属性存有令牌,它作为一个 key,在定位依赖值和配置注入器时使用。
  • 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。 提供者定义对象中的 key 可以是 useClass —— 如上例子同。 也可以是 useExisting、useValue 或 useFactory。 每一个 key 都用于提供一种不同类型的依赖。

值提供者
并非所有的依赖都是类。 也可以注入字符串、函数或对象。

@NgModule({declarations: [AppComponent,],providers: [{ provide: 'api', useValue: '/api/pizzas' }]
})
export class AppModule {}

工厂提供者
有时候你需要动态创建依赖值,创建时需要的信息你要等运行期间才能拿到。

假设你不希望直接把 UserService 注入到 HeroService 中,因为你不希望把这个服务与那些高度敏感的信息牵扯到一起。 这样 HeroService 就无法直接访问到用户信息,来决定谁有权访问,谁没有。
要解决这个问题,我们给 HeroService 的构造函数一个逻辑型标志,以控制是否显示秘密英雄。

HeroService:

constructor(private logger: Logger,private isAuthorized: boolean) { }getHeroes() {let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';this.logger.log(`Getting heroes for${auth}user.`);return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

你可以注入 Logger 但是不能注入 isAuthorized 标志。不过你可以改用工厂提供者来为 HeroService 创建一个新的 logger 实例。工厂提供者需要一个工厂函数:

let heroServiceFactory = (logger: Logger, userService: UserService) => {return new HeroService(logger, userService.user.isAuthorized);
};

虽然 HeroService 不能访问 UserService,但是工厂函数可以。 你把 Logger 和 UserService 注入到了工厂提供者中,并让注入器把它们传给这个工厂函数。

 providers: [{provide: HeroService,useFactory: heroServiceFactory,deps: [Logger, UserService]}]
  • useFactory 字段告诉 Angular 该提供者是一个工厂函数,该函数的实现代码是 heroServiceFactory。
  • deps 属性是一个提供者令牌数组。注入器解析这些令牌,并把与之对应的服务注入到相应的工厂函数参数表中。

组件篇

生命周期钩子

一种接口,使用它来监听指令和组件的生命周期,比如创建、更新和销毁等。

每个接口只有一个钩子方法,方法名是接口名加前缀 ng。不必实现所有生命周期钩子,只要实现你所需要的就可以了。Angular 会按以下顺序调用钩子方法。

钩子方法 用途 时机
ngOnChanges() 当数据绑定输入属性的值发生变化时响应 在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。
ngOnInit() 初始化指令/组件 在第一轮 ngOnChanges() 完成之后调用,只调用一次。
ngDoCheck() 用于检测和处理值的改变 每次执行变更检测时的 ngOnChanges() 和 首次执行变更检测时的 ngOnInit() 后调用。
ngAfterContentInit() 把外部内容投影进组件视图或指令所在的视图之后调用 第一次 ngDoCheck() 之后调用,只调用一次。
ngAfterContentChecked() 每检查完被投影到组件或指令中的内容之后调用 ngAfterContentInit() 和每次 ngDoCheck() 之后调用。
ngAfterViewInit() 初始化完组件视图及其子视图或包含该指令的视图之后调用 第一次 ngAfterContentChecked() 之后调用,只调用一次。
ngAfterViewChecked() 每做完组件视图和子视图或包含该指令的视图的变更检测之后调用 ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。
ngOnDestroy() 指令销毁前调用 销毁指令之前立即调用。

ngOnChanges
当数据绑定输入属性的值发生变化时,Angular 就会调用 ngOnChanges() 方法。它会获得一个 SimpleChanges 对象,该对象包含绑定属性的新值和旧值等,它主要用于监测组件输入属性的变化。

import { Component, OnInit} from '@angular/core';@Component({selector: 'on-changes-parent',template: `<div><h2>OnChanges</h2><p>姓名(hero.name):<input type="text" [(ngModel)]="hero.name"></p><p>技能(skills):<input type="text" [(ngModel)]="skills"></p></div><on-changes [hero]="hero" [skills]="skills"></on-changes>`,
})
export class OnChangesParentComponent{hero: { name: string } = { name: 'Windstorm' };skills: string = 'sing,fly';
}

OnChangesComponent 组件有两个输入属性:hero 和 skills。

import { Component, OnChanges, SimpleChanges, Input } from '@angular/core';@Component({selector: 'on-changes',templateUrl: './on-changes.component.html',styleUrls: ['./on-changes.component.less']
})
export class OnChangesComponent implements OnChanges {@Input() hero: { name: string };@Input() skills: string;changeLog: any[] = [];ngOnChanges(changes: SimpleChanges) {for(let propName in changes) {let chng = changes[propName];let cur = JSON.stringify(chng.currentValue);let prev = JSON.stringify(chng.previousValue);console.log(`${propName}: currentValue =${cur}, previousValue =${prev}`);this.changeLog.push(`${propName}: currentValue =${cur}, previousValue =${prev}`);}}
}

请注意:ngChanges() 不会捕获对 hero.name 的更改。这是因为只有当输入属性的值发生变化时,才会调用该钩子。在这里, hero 属性的值是对 hero 对象的引用。


动态创建组件

使用 ComponentFactoryResolver 来动态添加组件。

定义指令
定义一个名叫 AdDirective 的指令来在模板中标记插入点。

import { Directive, ViewContainerRef } from '@angular/core';@Directive({selector: '[ad-host]'
})
export class AdDirective {constructor(public viewContainerRef: ViewContainerRef) { }
}

AdDirective 注入了 ViewContainerRef 来获取对容器视图的访问,这个容器就是动态加入的组件的宿主。

创建指令容器
放置指令/组件的地方称为 容器。创建一个模板元素< ng-template>作为我们的指令容器,把选择器 ad-host 应用到该容器上,大部分代码都在 ad-banner.component.ts 中。

动态创建组件
AdItem 对象指定要加载的组件类,以及绑定到该组件上的任意数据。ads 为要动态加载的组件类数组。通过点击按钮,调用 loadComponent() 来加载新组件。

  • 注入 ComponentFactoryResolver 服务对象,该服务对象提供一个resolveComponentFactory()方法,该方法接收一个组件类作为参数,并返回ComponentFactory 实例。
  • 接下来,把 viewContainerRef 指向这个组件的现有实例。怎样找到这个实例呢?它指向 AdHost,而 adHost 是我们设置的把动态组件插入什么位置的指令。
  • 调用 ViewContainerRef 实例 的 createComponent() 来创建对应组件,并将组件添加到容器中。
  • 每次创建组件时,需要删除之前的视图,否则组件容器会出现多个视图(若允许多个组件的话。则不需要执行清除操作)。
  • createComponent() 方法返回一个引用,指向这个刚刚加载的组件。使用这个引用就可以与该组件进行交互,比如设置它的属性或调用它的方法。
import { Component, ComponentFactoryResolver, Input, ViewChild, ViewContainerRef } from '@angular/core';
import { AdItem } from '../ad-item';
import { AdDirective } from '../directives/ad.directive';@Component({selector: 'ad-banner',template: `<div><h3>Advertisements</h3><ng-template ad-host></ng-template><button (click)="loadComponent()">Load Component</button></div>`})
export class AdBannerComponent{@Input() ads: AdItem[];currentAdIndex = -1;@ViewChild(AdDirective, {static: true}) adHost: AdDirective;constructor(private componentFactoryResolver: ComponentFactoryResolver) { }loadComponent() {this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;const adItem = this.ads[this.currentAdIndex];const componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);const viewContainerRef = this.adHost.viewContainerRef;viewContainerRef.clear();const componentRef = viewContainerRef.createComponent(componentFactory);componentRef.instance.data = adItem.data;}}

< ng-template > 元素是动态加载组件的最佳选择,因为它不会渲染任何额外的输出。

import { Type } from '@angular/core';export class AdItem {constructor(public component: Type<any>, public data: any) {}
}

最后需要把动态组件添加到 NgModule 的 entryComponents 数组中:

entryComponents: [HeroProfileComponent, HeroJobAdComponent]

其余代码

import { Component, OnInit } from '@angular/core';
import { AdService }         from './ad.service';
import { AdItem }            from './ad-item';@Component({selector: 'app-root',template: `<div><ad-banner [ads]="ads"></ad-banner></div>`
})
export class AppComponent implements OnInit {ads: AdItem[];constructor(private adService: AdService) {}ngOnInit() {this.ads = this.adService.getAds();}
}
import { Injectable } from '@angular/core';
import { AdItem } from '../ad-item';
import { HeroProfileComponent } from '../hero-profile/hero-profile.component';
import { HeroJobAdComponent } from '../hero-job-ad/hero-job-ad.component';@Injectable({providedIn: 'root'
})
export class AdService {getAds() {return [new AdItem(HeroProfileComponent, {name: 'Bombasto', bio: 'Brave as they come'}),new AdItem(HeroProfileComponent, {name: 'Dr IQ', bio: 'Smart as they come'}),new AdItem(HeroJobAdComponent, {headline: 'Openings in all departments', body: 'Apply today'})]}
}
import { Component, Input } from '@angular/core';
import { AdComponent } from '../ad.component';@Component({template: `<div><h3>Featured Hero Profile</h3><h4>{{data.name}}</h4><p>{{data.bio}}</p></div>`
})
export class HeroProfileComponent implements AdComponent {@Input() data: any;
}
import { Component, Input } from '@angular/core';
import { AdComponent } from '../ad.component';@Component({template: `<div><h4>{{data.headline}}</h4>{{data.body}}</div>`
})
export class HeroJobAdComponent implements AdComponent {@Input() data: any;
}
export interface AdComponent {data: any;
}

组件样式

视图封装

import { Component } from '@angular/core';@Component({selector: 'app-hero',template: `<h1>The hero group</h1><hero-main></hero-main>`,styles: ['h1 { font-weight: normal; }']
})
export class HeroComponent { }


从页面生成的 HTML 结构,我们发现了 _nghost-aqp-c50_ngcontent-aqp-c50 等属性。

  • 当应用程序启动的时候,宿主元素将会拥有一个唯一的属性,该属性的值取决于组件的处理顺序,比如 _nghost-c0_nghost-c1
  • 每个组件内的元素,将会应用唯一的属性,比如 _ngcontent-c0_ngcontent-c1

这些属性是如何进行视图封装的呢?接下来介绍特殊的选择器。

:host

当我们只想为宿主元素设置样式,而不影响到宿主元素下的其它元素时,可以使用 : host

:host 是把宿主元素作为目标的唯一方式。因为宿主不是组件自身模板的一部分,而是父组件模板的一部分,除此之外,没有办法指定它。

import { Component, Input } from '@angular/core';@Component({selector: 'hero-details',template: `<hero-team [heroes]="heroes"></hero-team><ng-content></ng-content>`,styleUrls: ['./hero-details.component.less']
})
export class HeroDetailsComponent {@Input() heroes: any;
}
//hero-details.component.less
:host {display: inline-block;border: 1px solid black;width: 200px;
}


:host 可以结合其它选择器,比如:

//hero-details.component.less
:host h3 {color: red;
}


把宿主样式作为条件,就要像 函数 一样把条件选择器放在 :host 后面的括号中。

//hero-details.component.less
:host(.active) {border-width: 3px;
}

:: ng-deep

把 伪类 ::ng-deep 应用到任何一条 css 规则上就会禁止对那条规则的视图包装。任何带有 ::ng-deep 的样式都会变成全局样式。

为了把指定的样式限定在当前组件及其下级组件,请在 ::ng-deep 之前带上 :host 选择器,避免该样式污染其它组件。

//hero-details.component.less
:host ::ng-deep h3 {font-style: italic;
}

::ng-deep 还有两个别名: /deep/ 和 >>> 。

:host-context

对于开发主题样式很有用。:host-context() 在当前组件宿主元素的祖先节点中查找 CSS 类,直到文档的根节点为止。

在下面的例子中,只有当某个祖先元素有 css类 red-theme(/blue-theme) 时,才会把 相应 border-color 和 background 样式应用到组件内部的所有相应元素中。

import { Component } from '@angular/core';@Component({selector: 'hero-main',template: `<h2>hero-main</h2><div class="red-theme"><hero-details [heroes]="group1" [class.active]="group1.active"><hero-controls [heroes]="group1"></hero-controls></hero-details></div><div class="blue-theme"><hero-details [heroes]="group2" [class.active]="group2.active"><hero-controls [heroes]="group2"></hero-controls></hero-details></div>`,styles: ['div { display: inline-block;}']
})
export class HeroMainComponent {//...
}
//hero-details.component.less
:host-context(.red-theme) {border-color: red;
}
:host-context(.blue-theme) {border-color: blue;
}

再来看看 HeroControlsComponent:

import { Component, Input } from '@angular/core';@Component({selector: 'hero-controls',template: `<style>.btn {color: white;border: 1px solid #777;}:host-context(.red-theme) .btn-theme {background: red;}:host-context(.blue-theme) .btn-theme {background: blue;}</style><h3>Controls</h3><button class="btn btn-theme" (click)="activate()">Activate</button>`
})
export class HeroControlsComponent {@Input() heroes;activate() {this.heroes.active = !this.heroes.active;}
}

把样式加载进组件中

以上例子中,我们使用了不同的方式将样式加载进组件中。现在我们说说常用的把样式加入组件的方式:

设置 styles 或 styleUrls 元数据

styles: ['h1 { font-weight: normal; }']
 styleUrls: ['./hero-details.component.less']

注意:这些样式只对当前组件有效,它们既不会作用于嵌入的任何组件,也不会作用于投影进来的组件(如 ng-content)。

模板内联样式

CSS @imports 语法

//hero-details.component.less
@import './hero-details-box.less';

外部以及全局样式文件
当使用 CLI 进行构建时,必须配置 angular.json 文件,使其包含所有外部资源(包括外部的样式表文件)。

在它的 styles 区注册这些全局样式文件,默认情况下,它会有一个预先配置的全局 styles.css 文件。

自定义属性型指令实践

属性型指令用于改变元素的外观或行为。

创建一个简单的属性型指令,当鼠标悬停在一个元素上时,改变它的背景色:

ng g directive highlight

CLI 会创建 highlight.directive.ts 及相应测试文件(highlight.directive.spec.ts),使用 --skipTests=true 将不会创建测试文件,并且在模块中声明这个指令类。

import { Directive, ElementRef } from '@angular/core';@Directive({selector: '[appHighlight]'
})
export class HighlightDirective {constructor(el: ElementRef) {console.log(this.el);}
}
<p appHighlight>Highlight me!</p>

HostListener

属性装饰器,一般用来为宿主元素添加事件监听。此外,也可以监听 window 或 document 对象上的事件。

现在我们来监听宿主元素的鼠标进入和离开,并设置它的背景色:

//highlight.directive.ts
@HostListener('mouseenter')onMouseEnter() {this.highlight('yellow');}@HostListener('mouseleave')
onMouseLeave() {this.highlight(null);
}private highlight(color: string) {this.el.nativeElement.style.backgroundColor = color;
}

向指令传递值

使用 @Input 数据绑定向指令传递值。

可以指定要用哪种颜色高亮:

<p [appHighlight]="'orange'">Highlight me!</p>

[appHighlight] 属性同时做了两件事:把高亮指应用到了元素上,并且通过属性绑定设置了该指令的高亮颜色。这是清爽、简约的语法。

在指令内部,使用 @Input 别名来反映该属性的意图,保持可读性:

@Input('appHighlight') highlightColor: string;
@HostListener('mouseenter')
onMouseEnter() {//如果未指定高亮颜色,就用红色高亮this.highlight(this.highlightColor || 'red');
}

通常真实的应用会有多个可定制属性。
目前,默认颜色被硬编码为红色,我们来允许设置默认颜色:

<p [appHighlight]="color" defaultColor="violet">Highlight me!</p>
@Input() defaultColor: string;
@HostListener('mouseenter')
onMouseEnter() {this.highlight(this.highlightColor || this.defaultColor || 'red');
}

HostBinding

属性装饰器,用来动态设置宿主元素的属性值。

我们来为 HighlightDirective 新增一个 border 属性:

import { Directive, ElementRef, HostListener, Input, HostBinding } from '@angular/core';@Directive({selector: '[appHighlight]'
})
export class HighlightDirective {@Input('appHighlight') highlightColor: string;@Input() defaultColor: string;@HostBinding('style.border')border: string;@HostListener('mouseenter')onMouseEnter() {this.highlight(this.highlightColor || this.defaultColor || 'red');this.border = '2px solid grey';}@HostListener('mouseleave')onMouseLeave() {this.highlight(null);this.border = null;}//...
}

自定义结构型指令实践

结构型指令用于塑造或重塑 DOM 的结构。比如添加、移除或维护这些元素。

创建一个名为 UnlessDirective 的结构型指令,它的功能与 NgIf 相反,即在条件为 false 时显示模板内容:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}

1、指令的属性名应该采用小驼峰形式,带有一个前缀,但不能用 ng,因为它只属于 Angular 本身。请选择一些简短的,适合的前缀。
2、使用 TemplateRef 取得 < ng-template>的内容,并通过 ViewContainerRef 来访问这个视图容器。

由于我们会把一个 true/false 条件绑定到 [appUnless] 属性上,因此该指令需要一个带有 @Input 的 appUnless 属性。

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';@Directive({ selector: '[appUnless]'})
export class UnlessDirective {private hasView = false;constructor(private templateRef: TemplateRef<any>,private viewContainer: ViewContainerRef) { }@Input() set appUnless(condition: boolean) {if(!condition && !this.hasView) {this.viewContainer.createEmbeddedView(this.templateRef);this.hasView = true;}else if(condition && this.hasView) {this.viewContainer.clear();this.hasView = false;}}
}

一旦该值的条件发生了变化,Angular 就会设置 appUnless 属性。因为不能用 appUnless 属性,所以为它定义一个设置器(setter)。
由于不会读取 appUnless 属性,因此它不需要定义 getter。

试用该指令:

<p *appUnless="condition" class="unless a">(A) This paragraph is displayed because the condition is false.
</p><p *appUnless="!condition" class="unless b">(B) Although the condition is true,this paragraph is displayed because appUnless is set to false.
</p>

自定义管道实践

管道用来对输入的数据进行转换和格式化,如大小写转换、数值和日期格式化等。

内建管道及分类

String -> String
UpperCasePipe 把文本全部转换成大写
LowerCasePipe 把文本全部转换成小写
TitleCasePipe 把文本转换成标题形式
Number -> String
DecimalPipe 把数字转换成带小数点字符串, 根据本地环境中的规则进行格式化。
PercentPipe 把数字转换成百分比字符串, 根据本地环境中的规则进行格式化。
CurrencyPipe 把数字转换成金额字符串
Object -> String
JsonPipe 把一个值转换成 JSON 字符串格式
DatePipe 根据区域设置规则格式化日期值
Tools
KeyValuePipe(v6.1.0) 将对象或映射转换为键值对数组
SlicePipe 从一个 Array 或 String 中创建其元素一个新子集
AsyncPipe 从一个异步回执中解出一个值
I18nPluralPipe 将值映射到字符串,该字符串根据地区规则对值进行多元化处理。
I18nSelectPipe 显示与当前值匹配的字符串的通用选择器

在模板中使用管道:

<p>{{ 'Angular' | uppercase }}</p><!-- Output: ANGULAR -->
<p>{{ 'Angular' | lowercase }}</p><!-- Output: angular -->
<p>{{ { name: 'semlinker' } | json }}</p><!-- Output: { "name": "semlinker" } -->

使用参数和管道链来格式化数据:

<p>{{ 3.14159265 | number: '1.4-4' }}</p><!-- Output: 3.1416 -->
<p>{{ today | date: 'shortTime' }}</p><!-- Output: 2:13 PM -->
<p>{{ 'semlinker' | slice:0:3 }}</p><!-- Output: sem -->
<p>{{ 'semlinker' | slice:0:3 | uppercase }}</p><!-- Output: SEM -->

创建一个指数级转换管道

ng g p exponential-strength

实现 PipeTransform 接口中定义的 transform 方法

import { Pipe, PipeTransform } from '@angular/core';@Pipe({name: 'exponentialStrength'
})
export class ExponentialStrengthPipe implements PipeTransform {transform(value: number, exponent?: number): number {return Math.pow(value, isNaN(exponent) ? 1 : exponent);}
}
<p>Super power boost: {{2 | exponentialStrength: 10}}</p>

管道分类

  • pure管道:仅当输入值变化的时候,才执行转换操作,为默认类型。(输入值变化是指原始数据类型如:string、number、boolean 等的数值或对象的引用值发生变化)
  • impure 管道:在每次变化检测期间都会执行,如鼠标点击或移动都会执行 impure 管道。

Element篇

为了支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。

ElementRef 简介

通过 ElementRef 可以封装视图层中的 native 元素(在浏览器中, native 元素通常指 DOM 元素)。

应用
需求:在页面渲染成功以后,获取页面的 div 元素,并改变该元素的背景色。

import { Component, ElementRef, AfterViewInit } from '@angular/core';@Component({selector: 'my-app',template: `<div #greet>Hello {{name}}</div>`,
})
export class AppComponent {name:string = 'Angular';@ViewChild('greet') greetDiv: ElementRef;constructor() { } ngAfterViewInit() {this.greetDiv.nativeElement.style.backgroundColor = 'red';}
}

以上设置 div 元素背景的代码,我们是默认应用的运行环境在浏览器中。在应用层直接操作 DOM,会造成应用层与渲染层之间强耦合,导致应用无法运行在不同环境。

当需要直接访问 DOM时,请把本 API 作为最后选择。直接操作 DOM,会造成应用与渲染层之间强耦合。优先使用 Angular 提供的模板和数据绑定机制。或者使用 Renderer2。

import { Component, ElementRef, AfterViewInit } from '@angular/core';@Component({selector: 'my-app',template: `<div #greet>Hello {{name}}</div>`,
})
export class AppComponent {// ...constructor(private renderer: Renderer2) { }ngAfterViewInit() {//this.greetDiv.nativeElement.style.backgroundColor = 'red';this.renderer.setStyle(this.greetDiv.nativeElement, 'backgroundColor', 'red');}
}

最后,我们通过Renderer2 实例提供的 API 优雅地设置了 div 元素的背景颜色。

Renderer2 API 有哪些常用的方法?

export abstract class Renderer2 {abstract createElement(name: string, namespace?: string | null): any; //创建元素abstract createComment(value: string): any; //创建注释元素abstract createText(value: string): any; //创建文本元素abstract setAttribute(el: any, name: string, value: string,namespace?: string | null): void; //设置属性abstract removeAttribute(el: any, name: string, namespace?: string|null): void; //移除属性abstract addClass(el: any, name: string): void; //添加样式类abstract removeClass(el: any, name: string): void; //移除样式类abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void; //设置样式abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void; //移除样式abstract setProperty(el: any, name: string, value: any): void; //设置 DOM 对象属性,不同于元素属性abstract setValue(node: any, value: string): void; //设置元素值abstract listen(target: 'window'|'document'|'body'|any, eventName: string,callback: (event: any) => boolean | void): () => void; //注册事件
}

TemplateRef 简介

< template> 模板元素是一种机制,允许其包含内容在加载页面时不渲染,可将模板视为存储在页面上稍后使用的内容。TemplateRef,表示可用于实例化内嵌视图的内嵌模板。

应用
利用 TemplateRef 实例,可以灵活地创建内嵌视图。

@Component({selector: "hello-world",template: `<ng-template #tpl><span>I am span in template</span></ng-template>`
})
export class HelloWorldComponent implements AfterViewInit {@ViewChild("tpl") tplRef: TemplateRef<HTMLElement>;ngAfterViewInit(){// 模板中的<ng-template>元素会被编译为<!---->元素let commentElement = this.tplRef.elementRef.nativeElement;// 创建内嵌视图let embeddedView = this.tplRef.createEmbeddedView(null);// 动态添加子节点embeddedView.rootNodes.forEach(node => {commentElement.parentNode.insertBefore(node, commentElement.nextSibling);});}
}

ViewContainerRef 简介

有没有发现上例显示出模板元素中的内容整个流程太复杂了。接下来,我们说说 ViewContainerRef。

ViewContainerRef 表示一个视图容器,可添加一个或多个视图。通过 ViewContainer
Ref 实例,可基于 TemplateRef 实例创建内嵌视图,并指定内嵌视图的插入位置。简而言之,ViewContainerRef 的主要作用是创建和管理内嵌视图或组件视图。

应用

@Component({selector: "hello-world",template: `<ng-template #tpl><span>I am span in template</span></ng-template>`
})
export class HelloWorldComponent implements AfterViewInit {@ViewChild("tpl") tplRef: TemplateRef<HTMLElement>;@ViewChild("tpl", { read: ViewContainerRef }) tplVcRef: ViewContainerRef;ngAfterViewInit(){// let commentElement = this.tplRef.elementRef.nativeElement;// //创建内嵌视图// let embeddedView = this.tplRef.createEmbeddedView(null);// //动态添加子节点// embeddedView.rootNodes.forEach(node => {//   commentElement.parentNode.insertBefore(node, commentElement.nextSibling);// });this.tplVcRef.createEmbeddedView(this.tplRef);});}
}

ngTemplateOutlet 简介

用于标识指定的 DOM 元素作为视图容器,然后自动地插入设定的内嵌视图。不需要像 ViewContainerRef 示例那样,手动创建内嵌视图。

应用

@Component({selector: "hello-world",template: `<ng-container *ngTemplateOutlet="tpl"></ng-container><ng-template #tpl><span>I am span in template</span></ng-template>`
})
export class HelloWorldComponent implements AfterViewInit {@ViewChild("tpl") tplRef: TemplateRef<HTMLElement>;@ViewChild("tpl", { read: ViewContainerRef }) tplVcRef: ViewContainerRef;ngAfterViewInit(){}
}

ngComponentOutlet 简介

我们使用过 ComponentFactoryResolver 对象来动态创建组件,但过程有些繁琐,为了提高开发者体验和开发效率,引入了 ngComponentOutlet 指令。该指令用于使用声明式的语法,动态加载组件。

应用

@Component({selector: "hello-world",template: `<div><div *ngComponentOutlet="authFormComponent"></div></div>`
})
export class HelloWorldComponent {authFormComponent = AuthFormComponent;
}

ViewChild 和 ViewChildren

ViewChild 和 ViewChildren 装饰器用于获取模板视图中匹配的元素。视图查询在 ngAfterViewInit 钩子函数调用前完成,因此在 ngAfterViewInit 钩子函数中,就能正常获取查询的元素。

ViewChild
一个简单的例子,通过 @ViewChild 来获取 AuthMessageComponent 组件,并且在 ngAfterViewInit 中重新设置天数:

import { Component } from '@angular/core';@Component({selector: 'auth-message',template: `<div>保持登录 {{days}} 天</div>`,
})
export class AuthMessageComponent {days: number = 7;
}
import { Component, AfterViewInit, ViewChild, Output, EventEmitter, ViewChildren, QueryList, ChangeDetectorRef, ElementRef, Renderer2 } from '@angular/core';
import { AuthMessageComponent } from '../auth-message/auth-message.component';
import { Hero } from '../../hero';@Component({selector: 'auth-form',template: `<div><form (ngSubmit)="onSubmit(form.value)" #form="ngForm"><label>姓名<input name="name" ngModel></label><label>技能<input name="skills" ngModel #skills></label><auth-message [style.display]="(showMessage ? 'inherit' : 'none')"></auth-message></form></div>`
})
export class AuthFormComponent implements AfterViewInit {showMessage: boolean = true;@ViewChild(AuthMessageComponent) message: AuthMessageComponent;@ViewChild('skills') skills: ElementRef;constructor(private cd: ChangeDetectorRef) { }ngAfterViewInit() {this.message.days = 30;this.cd.detectChanges(); // !注意}
}

倘若没加 this.cd.detectChanges(); ,控制台会抛出以下异常:

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '7'. Current value: '30'.

详细解答请参考 Angular-关于ExpressionChangedAfterItHasBeenCheckedError你需要知道的一切。

ViewChildren
该装饰器用来从模板视图中获取匹配的多个元素,返回一个 QueryList 集合。

 @ViewChildren(AuthMessageComponent) message2: QueryList<AuthMessageComponent>;

ng-container vs ng-template

ng-container
逻辑容器,可用于对节点进行分组,但它不会添加额外的标签,因为它不会被放进 DOM 中。

<div [ngSwitch]="value"><ng-container *ngSwitchCase="0">Text one</ng-container><ng-container *ngSwitchCase="1">Text two</ng-container>
</div>

ng-template
表示 Angular 模板。NG 模板标签只是定义了一个模板, 如果没有使用结构型指令,那些元素是不可见的。在渲染视图之前,Angular 会把 ng-template 及其内容替换为一个注释。

<p>Hip!</p>
<ng-template><p>Hip!</p></ng-template>
<p>Hooray!</p>

ng-content 内容投影

使用 ng-content 来实现内容投影的功能。

import { Component, Output, EventEmitter } from '@angular/core';
import { User } from '../../user';@Component({selector: 'auth-form',template: `<div><form (ngSubmit)="onSubmit(form.value)" #form="ngForm"><ng-content></ng-content><label>姓名<input name="name" ngModel></label><label>密码<input type="password" name="password"></label><button type="submit">提交</button></form></div>`
})
export class AuthFormComponent {@Output() submitted: EventEmitter<User> = new EventEmitter<User>();onSubmit(user: User) {this.submitted.emit(user);}
}
import { Component } from "@angular/core";
import { User } from '../../user';@Component({selector: "app-root",template: `<div><auth-form (submitted)="createUser($event)"><h3>注册</h3></auth-form><auth-form (submitted)="loginUser($event)"><h3>登录</h3></auth-form></div>`
})
export class AppComponent {createUser(user: User) {console.log("Create account", user);}loginUser(user: User) {console.log("Login", user);}
}

包含在 auth-form 标签内的内容,会被投影到 AuthFormComponent 组件的 ng-content 所在区域。

select 属性
select 属性用于匹配想要的内容,进行选择性内容投影。

import { Component, Output, EventEmitter } from '@angular/core';
import { User } from '../../user';@Component({selector: 'auth-form',template: `<div><form (ngSubmit)="onSubmit(form.value)" #form="ngForm"><ng-content select="h3"></ng-content><label>姓名<input name="name" ngModel></label><label>密码<input type="password" name="password"></label><ng-content select="button"></ng-content></form></div>`
})
export class AuthFormComponent {@Output() submitted: EventEmitter<User> = new EventEmitter<User>();onSubmit(user: User) {this.submitted.emit(user);}
}
import { Component } from "@angular/core";
import { User } from '../../user';@Component({selector: "app-root",template: `<div><auth-form (submitted)="createUser($event)"><h3>注册</h3><button type="submit">注册</button></auth-form><auth-form (submitted)="loginUser($event)"><h3>登录</h3><button type="submit">登录</button></auth-form></div>`
})
export class AppComponent {createUser(user: User) {console.log("Create account", user);}loginUser(user: User) {console.log("Login", user);}
}


ContentChild
使用 ContentChild 装饰器来获取投影的元素。

@Component({selector: "auth-form",template: `<div><form (ngSubmit)="onSubmit(form.value)" #form="ngForm"><ng-content select="h3"></ng-content><label>姓名:<input name="name" ngModel></label><label>密码:<input type="password" name="password"></label><ng-content select="auth-remember"></ng-content><div *ngIf="showMessage">保持登录状态30天</div><ng-content select="button"></ng-content></form></div>`
})
export class AuthFormComponent implements AfterContentInit {showMessage: boolean;@ContentChild(AuthRememberComponent) remember: AuthRememberComponent;ngAfterContentInit() {if (this.remember) {this.remember.checked.subscribe((checked: boolean) => (this.showMessage = checked));}}// ...
}
import { Component, Output, EventEmitter } from '@angular/core';@Component({selector: 'auth-remember',template: `<label><input type="checkbox" (change)="onChecked($event.target.checked)">Keep me logged in</label>`,
})
export class AuthRememberComponent {@Output() checked: EventEmitter<boolean> = new EventEmitter<boolean>();onChecked(value: boolean) {this.checked.emit(value);}
}

在生命周期钩子 ngAfterContentInit 中通过订阅 remember 的 checked 输出属性来监听 checkbox 输入框的变化。同时根据 AuthRememberComponent 组件中 checkbox 的值来控制是否显示 ”保持登录30天“ 的提示消息。

ContentChildren
用来从通过 Content Projection 方式设置的视图中获取匹配的多个元素,返回的结果是一个 QueryList 集合。

constructor vs ngOnInit

constructor
constructor(构造函数)是类中的特殊方法,在进行类实例化操作时,会被自动调用。尽量保持简单明了,只执行一些简单的数据初始化操作或依赖注入。

ngOnInit
ngOnInit 是 Angular 组件生命周期中的一个钩子。把其它的初始化操作放在这里面执行。

构造函数会优先执行,当组件的输入属性变化时会自动触发 ngOnChanges 钩子,然后再调用 ngOnInit 钩子方法。

RxJS篇

Observable 简介

我们先来了解两个设计模式:观察者模式和迭代器模式。这两个模式是 Observable 的基础。

Observer Pattern(观察者模式)

在观察者模式中,一个目标对象管理所有相依于它的观察者对象,并且当目标对象本身的状态改变时主动向观察者对象发出通知。

观察者模式相似于发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当该主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

观察者模式中有两个重要角色: Subject(主题)和 Observer(观察者)。它们的关系就好像日常生活中的期刊订阅:

  • 期刊出版方:负责期刊的出版和发行工作。
  • 订阅者:只需执行订阅操作,新的期刊发布后,就会主动收到通知,若取消订阅,则不再收到通知。


实战
定义一个 主题 类:

class Subject {observerCollection;constructor() {this.observerCollection = [];}registerObserver(observer) {this.observerCollection.push(observer);}unregisterObserver(observer) {let index = this.observerCollection.indexOf(observer);if(index >=0) this.observerCollection.splice(index, 1);}notifyObservers() {this.observerCollection.forEach(observer => observer.notify());}
}

定义一个 观察者 类:

class Observer {name;constructor(name) {this.name = name;}notify(){console.log(`${this.name}has been notified.`);}
}

使用示例:

let subject = new Subject();let observer1 = new Observer('angular'); // 创建观察者A
let observer2 = new Observer('rxjs'); //创建观察者Bsubject.registerObserver(observer1); //注册观察者A
subject.registerObserver(observer2); //注册观察者Bsubject.notifyObservers(); //通知观察者subject.unregisterObserver(observer1); //移除观察者Asubject.notifyObservers(); //验证是否成功移除

在观察者模式中,通常调用注册观察者后,会返回一个函数,用于移除监听。

Iterator Pattern(迭代器模式)

迭代器模式又称游标模式。它提供一种方法顺序访问一个集合对象中的各个元素,而又不需要暴露该对象的内部表示。可以把迭代的过程从业务逻辑中分离出来。

在 JavaScript 中迭代器是一个对象,它提供一个 next() 方法,返回序列中的下一项。该方法返回包含 donevalue 两个属性的对象。对象取值如下:

非最后一个元素 { done: false, value: elementValue }
最后一个元素 { done: true, value: undefined }

在 ES 6 中可以通过 Symbol.iterator 来创建可迭代对象的内部迭代器:

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

调用 next() 方法来获取数组中的元素:

ES 6 中可迭代的对象:Arrays 、Strings 、Maps 、Sets 、DOM data structures (work in progress)。

优缺点

  • 无论是对象集合或者数组和 hash 表,在使用迭代器模式后,即使不关心内部构造,也可以顺序访问其中的每个元素。
  • 封装性良好,只需得到迭代器就可以遍历,而不用去关心算法。
  • 缺点是遍历过程是一个单向且不可逆的遍历。

Observable(可观察对象)

RxJS 中有两个基本概念:Observable 和 Observer。Observables 作为被观察者,是一个值或事件的流集合;而 Observer 则作为观察者,根据 Observable进行处理。

Observable 与Observer之间的订阅发布关系如下:

  • 订阅:Observer 通过 Observable 提供的 subscribe() 方法订阅 Observable。
  • 发布:Observable 通过回调 next() 方法向 Observer 发布事件。

Pull(拉取) vs Push(推送)

拉取 和 推送 是两种不同的协议,用来描述数据生产者如何与数据消费者进行通信的。

pull
在“拉取”体系中,消费者决定何时从生产者那里获取数据,而生产者不知道数据什么时候将会被发送给消费者。

每个 JavaScript 函数都是拉取体系。函数是数据的生产者,调用该函数的代码通过从函数调用中“取出”一个单个返回值来对该函数进行消费。

push
在“推送”体系中,生产者决定何时发送数据给消费者,消费者不知道何时会接收到数据。

Promise(承诺)是 当今的 JS 中最常见的“推送”体系,一个Promise(数据生产者)发送一个解析过的值来执行一个回调(数据消费者)。不同于函数的是,Promise 决定着何时把值“推送”给回调函数。

RxJS 引入了 Observables(可观察对象),一个全新的“推”体系。一个可观察对象是一个产生多值的生产者,当产生新数据时,会主动“推送给” Observer(观察者)。

Observable vs Promise

Observable(可观察对象)是基于推送(Push)运行时执行(lazy)的多值集合。

单值 多值
拉取(Pull) 函数 遍历器
推送(Push) Promise Observable
Promise Observable
创建时就立即执行 声明式的,当订阅的时候才会开始执行
返回单个值 随着时间推移发出多个值
不可取消 可以取消
支持 map、filter、reduce 等操作符
把错误推送给它的子承诺 错误处理工作交给了订阅者的错误处理器

创建 Observable

RxJS 提供了很多创建 Observable 对象的方法,其中 create 是最基本的方法。

要执行所创建的可观察对象,并开始从中接收通知,就要调用它的 subscribe() 方法,并传入一个观察者(observer)。这是一个 JavaScript 对象,它定义了收到的这些消息的处理器(handler)。 subscribe() 调用会返回一个 Subscription 对象,该对象具有一个 unsubscribe() 方法。 当调用该方法时,就会停止接收通知。

import { Observable } from "rxjs";const observable$ = Observable.create(observer => {observer.next('Angular');observer.next('RxJS');
});observable$.subscribe(value => { //执行订阅操作console.log(value)
});

提示:

  • 虽然 Angular 框架没有对可观察对象的强制性命名约定,不过建议可观察对象的名字以“$”符号结尾。
  • 同样的,如果希望用某个属性来存储可观察对象的最近一个值,它的命名惯例是与可观察对象同名,但不带“$”后缀。

RxJS 的核心特性是它的异步处理能力,但它也可以用来处理同步的行为。示例如下:

const observable$ = Observable.create(observer => {observer.next('Angular');observer.next('RxJS');
});console.log('start');
observable$.subscribe(value => { //执行订阅操作console.log(value)
});
console.log('end');


处理异步行为:

const observable$ = Observable.create(observer => {observer.next('Angular');observer.next('RxJS');setTimeout(() => {observer.next('RxJS Observable');}, 300);
});console.log('start');
observable$.subscribe(value => { //执行订阅操作console.log(value)
});
console.log('end');

结论:Observable 可以应用于同步和异步的场合。

Observer(观察者)

Observable 可以被订阅,或者说可以被观察。 Observer(观察者) 中包含三个方法,每当 Observable 触发事件时,便会自动调用观察者的对应方法。

通知类型 说明
next 必要。每当 Observable 发送新值时,next 方法会被调用。
error 可选。当 Observable 内发生错误时,error 方法会被调用。
complete 可选。用来处理执行完毕通知。调用 complete 方法之后,next 方法就不会再次被调用。

具体示例:

const observable$ = Observable.create(observer => {observer.next('Angular');observer.next('RxJS');observer.complete();observer.next('not work');
});// 创建一个观察者
const observer = {next: function (value) {console.log(value);},error: function (error) {console.log(error);},complete: function () {console.log('complete');}
}//执行订阅操作
observable$.subscribe(observer);

我们也可以在调用 Observable 对象的 subscribe 方法时,依次传入 next、error、complete 三个函数,来创建观察者:

observable.subscribe(value => { console.log(value); },error => { console.log('Error: ', error); },() => { console.log('complete'); }
);

注意:next() 方法可以接受消息字符串、事件对象、数字值或各种结构。为了更通用一点,我们把由可观察对象发布出来的数据统称为 。任何类型的值都可以表示为可观察对象,而这些值会被发布为一个流。

Subscription(订阅)

对于一些 Observable 对象,当我们不需要的时候,要释放相关的资源,以避免资源浪费。针对这种情况,我们可以调用 Subscription 对象的 unsubscribe 方法来释放资源。示例如下:

const source$ = timer(1000, 1000);// 取得subscription对象
const subscription = source$.subscribe({next: function (value) {console.log(value);},complete: function () {console.log('complete!');},error: function (error) {console.log('Throw Error: ' + error);}
});setTimeout(() => {subscription.unsubscribe();
}, 5000);


Subscription 还可以合在一起,这样的一个 Subscription 调用 unsubscribe() 方法,可能会有多个 Subscription 取消订阅 。

const subscription = observable1.subscribe(x => console.log('first: ' + x));
const childSubscription = observable2.subscribe(x => console.log('second: ' + x));subscription.add(childSubscription);setTimeout(() => {// subscription 和 childSubscription 都会取消订阅subscription.unsubscribe();
}, 1000);

Subscriptions 还有一个 remove(otherSubscription) 方法,用来撤销一个已添加的子 Subscription 。

常见创建操作符

除了上面介绍的 create 方法,RxJS 还提供了很多操作符,用于创建 Observable 对象,比如:of 、from、fromEvent 、interval 、range 、empty 、throw 、timer 等等。

详情见:https://cn.rx.js.org/manual/overview.html#h39

Angular 中的 Observable

Angular 使用可观察对象作为处理各种常用异步操作的接口。比如:

  • EventEmitter 类派生自 Observable:EventEmitter 扩展了 RxJS Subject,并添加了一个 emit() 方法,这样它就可以发送任意值了。当你调用 emit() 时,就会把所发送的值传给订阅上来的观察者的 next() 方法。
  • HTTP 模块使用可观察对象来处理 AJAX 请求和响应:Angular 的 HttpClient 从 HTTP 方法调用中返回了可观察对象。
  • 路由器和表单模块使用可观察对象来监听对用户输入事件的响应。

Subject

订阅 Observable

在介绍 RxJS Subject 之前,先来看个例子:

import { interval } from "rxjs";
import { take } from "rxjs/operators";const interval$ = interval(1000).pipe(take(3));interval$.subscribe(value => console.log("Observer A get value:" + value));setTimeout(() => {interval$.subscribe(value => console.log("Observer B get value:" +value));
}, 1000);

通过上面示例,得出以下结论:

  • Observable 对象可以被重复订阅。
  • Observable 对象每次被订阅后,都会创建一次新的、独立的执行。

Observable 对象的默认行为,适用于大部分场景。但有时候,我们希望第二次订阅时,不从头开始接收 Observable 发出的值,而是从第一次订阅当前正在处理的值开始发送,这种处理方式叫 多播

那么,以上需求如何实现呢?观察者模式定义了一对多的关系,我们可以让多个观察者同时监听同一主题。当数据源发出新值的时候,所有的观察者就能接收到新的值。

RxJS Subject(主题)

我们利用 RxJS 的 Subject 来实现上述需求:

const interval$ = interval(1000).pipe(take(3));
const subject = new Subject();const observerA = {next: value => console.log("Observer A get value: " + value),error: error => console.log("Observer A error: " + error),complete: () => console.log("Observer A complete!")
};const observerB = {next: value => console.log("Observer B get value: " + value),error: error => console.log("Observer B error: " + error),complete: () => console.log("Observer B complete!")
};subject.subscribe(observerA); // 添加观察者A
interval$.subscribe(subject); // 订阅interval$对象
setTimeout(() => {subject.subscribe(observerB); // 添加观察者B
}, 1000);

通过上面示例,得出 Subject 的特点:

  • Subject 既是 Observable,又是 Observer。
  • 当有新消息时,Subject 会通知内部的所有观察者。

RxJS Subject & Observable

Subject 是观察者模式的实现,当观察者订阅 Subject 对象时,Subject 对象会把订阅者添加到观察者列表中,每当 subject 对象接收到新值时,它会遍历观察者列表,依次调用观察者内部的 next() 方法,把值一一送出。

允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。

Subject 具有 Observable 所有的方法,因为它继承了 Observable 类,在 subject 类中有五个重要方法:

方法名 说明
next 每当 Subject 对象接收到新值时,next 方法会被调用。
error 运行中出现异常,error 方法会被调用。
complete Subject 订阅的 Observable 对象结束后,complete 方法会被调用。
subscribe 添加观察者
unsubscribe 取消订阅

因为 Subject 是观察者,这也意味着可以把 Subject 作为参数传给任何 Observable 的 subscribe 方法:

import { Subject, from } from 'rxjs';const subject = new Subject();subject.subscribe({next: (value) => console.log('observerA ' + value)
});
subject.subscribe({next: (value) => console.log('observerB ' + value)
});const observable = from([1,2,3]);observable.subscribe(subject); //可以提供一个 Subject 进行订阅

BehaviorSubject

有时候我们希望 Subject 能够保存发送给消费者的最新值,而不是单纯的发送事件,也就是说当有新的观察者订阅时,会立即接收到“当前值”。

举例说明:

import { Subject } from "rxjs";const subject = new Subject();subject.subscribe({next: (value) => console.log('observerA: ' + value)
});subject.next(1);
subject.next(2);
subject.next(3);setTimeout(() => {subject.subscribe({next: (value) => console.log('observerB: ' + value)}); // 1秒后订阅
}, 1000);

注意: 在 observerB 订阅 Subject 对象之后,它并没有收到值,因为Subject 对象没有再调用 next() 方法。

BehaviorSubject 有一个“当前值”的概念,它保存了发送给消费者的最新值。 并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”。

BehaviorSubject 跟 Subject 最大的不同就是 BehaviorSubject 是用来保存当前最新的值,而不是单纯的发送事件。

示例:

import { BehaviorSubject } from "rxjs";const subject = new BehaviorSubject(0); // 0是初始值subject.subscribe({next: (value) => console.log('observerA: ' + value)
});subject.next(1);
subject.next(2);
subject.next(3);setTimeout(() => {subject.subscribe({next: (value) => console.log('observerB: ' + value)}); // 1秒后订阅
}, 1000);

ReplaySubject

新增订阅者的时候,如果想接收到数据源最近发送的几个值,可以使用 ReplaySubject。ReplaySubject 记录 Observable 执行中的多个值并将其回放给新的订阅者。

import { ReplaySubject } from "rxjs";
const subject = new ReplaySubject(2); // 为新的订阅者缓冲2个值subject.subscribe({next: (value) => console.log('observerA: ' + value)
});subject.next(1);
subject.next(2);
subject.next(3);subject.subscribe({next: (value) => console.log('observerB: ' + value)
});subject.next(5);

AsyncSubject

AsyncSubject 会在 Observable 执行完成时(执行 complete()),将执行的最后一个值发送给观察者。示例如下:

import { AsyncSubject } from "rxjs";const subject = new AsyncSubject();subject.subscribe({next: (value) => console.log("Observer A get value: " + value),complete: () => console.log("Observer A complete!")
});subject.next(1);
subject.next(2);
subject.next(3);subject.complete();setTimeout(() => {subject.subscribe({next: (value) => console.log("Observer B get value: " + value),complete: () => console.log("Observer B complete!")}); // 1秒后订阅
}, 1000);

RxJS Subject 在 Angular 中的应用

在 Angular 中,我们可以使用 RxJS Subject 来实现组件间的通信。示例如下:

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';@Injectable({providedIn: 'root'
})
export class MessageService {private subject = new Subject<any>();sendMessage(message: string) {this.subject.next({ text: message });}clearMessage() {this.subject.next();}getMessage(): Observable<any> {return this.subject.asObservable();}
}
import { Component } from '@angular/core';
import { MessageService } from '../services/message.service';@Component({selector: 'app-home',template: `<div><h1>Home</h1><button (click)="sendMessage()">Send Message</button><button (click)="clearMessage()">Clear Message</button></div>`
})
export class HomeComponent {constructor(private messageService: MessageService) { }sendMessage() {this.messageService.sendMessage('Message from Home Component to App Component!');}clearMessage() {this.messageService.clearMessage();}
}
import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';import { MessageService } from './message.service';@Component({selector: 'my-app',template: `<div *ngIf="message">{{message.text}}</div><app-home></app-home>`
})export class AppComponent implements OnDestroy {message: any;subscription: Subscription;constructor(private messageService: MessageService) {this.subscription = this.messageService.getMessage().subscribe(message => {this.message = message;});}ngOnDestroy() {this.subscription.unsubscribe();}
}

何时应该取消 Subscription?

为了避免内存泄露或其它不必要的操作,我们应该及时取消订阅。判断依据为 Observable产生 有限值 或者 无限值。

不需要手动取消的场景

Angular 中有些场景已进行 unsubscribe 或通过 Observable.complete() 结束订阅数据流,在开发中不需要手动取消订阅。

1)Async pipe
AsyncPipe 会订阅一个可观察对象或承诺,并返回其发出的最后一个值。当组件销毁时,自动取消订阅。

2)HostListener
通过@HostListener进行订阅的事件,和直接在模板里订阅事件一样,也是自动取消的。

3)EventEmitter
EventEmitter 类派生自 Observable,Angular 提供了一个 EventEmitter 类,它用来从组件的 @Output() 属性中发布一些值。

4)有限的Observable
有限的Observable指的是发出的值是有限的,如timer。

5)Http
Angular 通过 HttpClient 执行 Http Request 返回的 Observables 是 Cold Observable 并且只发送一个值。在 Http Response 结束时,如果请求成功会调用 responseObserver.complete() ,自动结束数据流。如果请求失败会调用 responseObserver.error(response),自动结束数据流。

需要手动取消的场景

1)Router
Angular 在组件销毁时并没有取消router的所有订阅事件。

2)Forms
表单中的 valueChanges 和 statusChanges 等 Observable 都需要手动取消。

3)Renderer Service

4)无限的 Observable
当使用 fromEvent() 、interval() 等操作符时,输出值可能为无限的可观察对象。

5)自定义Observable
所有自定义 Observable 必须在组件销毁前手动取消。
比如 Subject,BehaviorSubject,AsyncSubject,ReplaySubject 这四种 subject,都是Hot Observable,Hot Observable 不管有没有被订阅都会源源不断的发送值,如果订阅者要主动取消订阅,就需要调用 unsubscribe() 取消订阅。

参考资源

站在巨人肩膀上学习~

  • 阿宝哥的 Angular 修仙之路 教程
  • Angular 中文文档 https://angular.cn/
  • Rxjs 中文网 https://cn.rx.js.org/
  • Angular4最佳实践之unsubscribe

【Angular 基础入门】——知识点整合相关推荐

  1. C# 零基础入门知识点汇总

    C# 零基础入门 知识点汇总 前言 一,基础语法(1~10) 二,流程控制(11~20) 三,数组相关(21~30) 四,函数介绍(31~40) 五,类和对象(41~50) 六,面向对象(51~60) ...

  2. javascript基础入门知识点整理

    学习目标:- 掌握编程的基本思维- 掌握编程的基本语法 typora-copy-images-to: media JavaScript基础 HTML和CSS 京东 课前娱乐 众人皆笑我疯癫,我笑尔等看 ...

  3. MyBatis基础入门--知识点总结

    对原生态jdbc程序的问题总结 下面是一个传统的jdbc连接oracle数据库的标准代码: public static void main(String[] args) throws Exceptio ...

  4. Pandas基础入门知识点总结

    目录 1.pandas 常用类 1.1 Series 1.1.1创建 Series 1.1.2 访问 Series 数据 1.1.3 更新.插入和删除 1.2 DataFrame 1.2.1 创建 D ...

  5. Python基础入门知识点——Python中的异常

    前言 在先前的一些章节里你已经执行了一些代码,你一定遇到了程序"崩溃"或因未解决的错误而终止的情况.你会看到"跟踪记录(traceback)"消息以及随后解释器 ...

  6. insert exec 语句不能嵌套_Python基础入门知识点——if 语句简介

    前言 if 语句是最简单的选择结构.如果满足条件就执行设定好的操作,不满足条件就执行其他其他操作. PS:如有需要Python学习资料的小伙伴可以加下方的群去找免费管理员领取 点击加群即可免费获取Py ...

  7. 【JAVA】基础入门知识点回顾

    1. public static String与 public static final String的差异 String 为不可变对象,一旦产生,就不可以改变其值. public static St ...

  8. Matplotlib 基础入门知识点总结

    目录 1.绘图的一些基本命令展示 2.Matplotlib 绘制网格 3.plt.gca() 对坐标轴的操作 4. 图表的样式参数设计 5.创建图形对象 6.绘制多子图 1.add_axes():添加 ...

  9. Python零基础入门,纯干货!【Python基础知识点汇总整理】

    目录 第一章 认识Python.Python常用集成开发环境PyCharm 一.认识 Python 01. Python 的起源 1.2 Python 的设计目标 1.3 Python 的设计哲学 0 ...

最新文章

  1. 修改python plot折线图的坐标轴刻度
  2. Java Socket通信编程
  3. 怎样学好Oracle子查询,Oracle学习(六):子查询
  4. Spring-data-redis 反序列化异常
  5. uboot的环境变量分析(printenv)
  6. 机器人能翻转汉堡肉饼 短暂上岗后将“休息”四天
  7. 对大量转载贴识别算法的研究
  8. git工具 将源码clone到本地指定目录的三种方式
  9. linux平台设备驱动模型是什么意思,Linux设备驱动模型之我理解
  10. keras + tensorflow —— 文本处理
  11. [转]Intent跳转到系统应用中的拨号界面、联系人界面、短信界面及其他
  12. pyside6的MQTT客户端
  13. 放慢你的额脚步_放慢脚步使我成为更好的领导者
  14. 化学实验室改造方案怎么做?
  15. Ubuntu 安装 XDM 2018 ( Xtreme Download Manager 2018 )
  16. LVGL8学习之row and a column layout with flexbox
  17. 二类分类器构造多类分类器
  18. 六月:手动学数据分析(task02)
  19. 说出我国的超级计算机的发展历程,中国超级计算机发展史
  20. 弘辽科技:淘宝新店提升销量可以吗?怎么提升关键词?

热门文章

  1. 在Hbulider中点击事件会出现两次
  2. 主外键constraint、primary key、foreign key、check、default的用法和理解
  3. pytorch dali 加速 dali支持的数据处理列表,mxnet tensorflow caff读取数据转换 pytorch训练
  4. Java 使用wps将word文件转换pdf文件
  5. OA开发很简单 OA实施很复杂
  6. 描述一下脚本<script>放在<head>和放到<body>底部的区别
  7. D3D Surface/Texture SDL DDraw渲染视频的区别和疑问
  8. 字符串处理 扩展的脚本技巧 正则表达式
  9. Windows7 IIS7.5部署ASP网站
  10. lqc_软件仓库部署及应用