[Angular 基础] - Observable
之前的笔记:
我以前对 Observable 的理解是 Promise 的一个超集
,重新了解了一下,感觉这个说法不太对。更准确一些的说法应该是 stream of events
,只不过大多数情况下,这个 events
是一个 Promise 而已。比如说 routes
的变化不属于 Promise,但是它是一个 event,也就自然可以被放到 Observable 里面去进行监听。
大多数 rxjs 相关的内容都在 rxjs 的 observable 里面说过了,这里主要记一些 angular 相关的知识,之后有需求的话继续拓展
先简单的说明一下项目结构:
bash
❯ tree src/app/
src/app/
├── app-routing.module.ts
├── app.component.css
├── app.component.html
├── app.component.ts
├── app.module.ts
├── home
│ ├── home.component.css
│ ├── home.component.html
│ └── home.component.ts
└── user
├── user.component.css
├── user.component.html
└── user.component.ts
3 directories, 11 files
路由的配置如下:
typescript
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'user/:id', component: UserComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
简单来说,项目总共有两个路径,默认路径渲染 Home 组件,user/:id
路径渲染 User 组件
使用 observable
这里的变化会在 HomeComponent
中实现:
typescript
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
})
export class HomeComponent implements OnInit {
constructor() {}
ngOnInit() {
interval(1000).subscribe((count) => {
console.log(count);
});
}
}
其中,interval
是 rxjs 提供的一个 observable:
typescript
export declare function interval(
period?: number,
scheduler?: SchedulerLike
): Observable<number>;
这个函数会在每个周期输出一个数字 (counter),效果如下:
需要注意这里有一个问题------当页面离开 Home 时,log 并没有停止。而是会持续输出。这个原因是因为这是一个 custom observable,它的创建和终结并不由 Angular 进行控制。因此如果不在 NgOnDestroy
中销毁,它就会一直 log 下去
最糟糕的情况是,如果当前组件被重复创建/毁灭多次,那么同一个组件就会有多个 observable 一起运行:
解决方法如下:
typescript
export class HomeComponent implements OnInit, OnDestroy {
private intervalSub: Subscription;
constructor() {}
ngOnInit() {
this.intervalSub = interval(1000).subscribe((count) => {
console.log(count);
});
}
ngOnDestroy(): void {
this.intervalSub.unsubscribe();
}
}
效果如下:
可以看到,当切换到其他页面的时候,log 的输出停止了,而从其他页面回来的时候,subscription 中的输出被清零了
👀:subscribe
中的就是 observer
,它主要的目的就是用来提供 Observable 中,对下一步操作的处理,包括 next
, error
和 complete
创建 observable
这里会创建一个新的 observable:
typescript
export class HomeComponent implements OnInit, OnDestroy {
private intervalSub: Subscription;
constructor() {}
ngOnInit() {
const customIntervalObservable = new Observable((observer) => {
let count = 0;
setInterval(() => {
observer.next(++count);
console.log(customIntervalObservable);
}, 1000);
});
this.intervalSub = customIntervalObservable.subscribe((data) => {
console.log(data);
});
}
ngOnDestroy(): void {
this.intervalSub.unsubscribe();
}
}
实现效果如下:
可以看到,实现方面基本上没什么变化
error
报错也是一个常见的情况,observable 这里对于报错的处理也很简单,如下:
typescript
const customIntervalObservable = new Observable((observer) => {
let count = 0;
setInterval(() => {
observer.next(++count);
if (count > 3) {
observer.error('count is greater than 3');
}
}, 1000);
});
效果如下:
可以看到,一旦报错后,observable 就会被取消。在 subscription 处理报错方法如下:
typescript
this.intervalSub = customIntervalObservable.subscribe(
(data) => {
console.log(data);
},
(error) => {
console.error(error);
}
);
效果如下:
complete
因为 observable 是一个 stream of events,所以它也有提供对于 events 终止的处理,也即是 complete:
typescript
const customIntervalObservable = new Observable((observer) => {
let count = 0;
setInterval(() => {
observer.next(++count);
if (count === 2) {
observer.complete();
}
if (count > 3) {
observer.error('count is greater than 3');
}
}, 1000);
});
this.intervalSub = customIntervalObservable.subscribe(
(data) => {
console.log(data);
},
(error) => {
console.error(error);
},
() => {
console.log('%c' + 'Observable completes.', 'color: #198754; ');
}
);
效果如下:
⚠️:如果在 complete
之前 subscription 就遇到报错,那么 complete
状态不会被触发
operators
先前的笔记有提到过,rxjs 说自己是 observable 版本的 lodash,其中一个原因就是 rxjs 提供了相当丰富的 operators,这样操作 observables 变得更加简单------虽然所有功能都是可以写在 observer 中进行实现的,不过 rxjs 提供的了很多开箱即用的 operators,可以极大地简化操作
pipe
的用处在于可以接受多个 operators,并合并成一个 operators。这样 operator 就可以承接上一个 observable 的返回值,对其进行修改,并 emit 到一个 新的 observable 中。这代表通过 pipe
对数据进行操作,并不会修改上一个 observable 中的值。
map
这里使用 map
做一个案例,修改的代码如下:
typescript
const customIntervalObservable = new Observable((observer) => {
let count = 0;
setInterval(() => {
observer.next(++count);
}, 1000);
});
const pipedObservable = customIntervalObservable.pipe(
map((data: number) => {
console.log('%c' + 'Round: ' + (data + 1), 'color: #0d6efd; ');
return data + 1;
})
);
customIntervalObservable.subscribe((data) => {
console.log('%c' + data, 'color: #198754; ');
});
pipedObservable.subscribe((data) => {
console.log('%c' + data, 'color: #bada55; background-color: #222;');
});
输出结果为:
可以看到:
- 原本的 observable ------即绿色字体的 observable------其 conter 从 1 开始输出
- 通过 pipe,并使用
map
进行操作的 observable------即黄色字体的 observable------其 conter 从 2 开始输出
这也说明了通过 pipe 的 operator 对数据操作,并不会 修改上一个 observable 的值
filter
修改如下:
typescript
const customIntervalObservable = new Observable((observer) => {
let count = 0;
setInterval(() => {
observer.next(++count);
}, 1000);
});
const pipedObservable = customIntervalObservable.pipe(
filter((data: number) => {
return data % 2 === 0;
}),
map((data: number) => {
return 'Round: ' + (data + 1);
})
);
pipedObservable.subscribe((data) => {
console.log(
'%c' + data,
'color: #bada55; background-color: #222; padding: 5px;'
);
});
其中:
filter
过滤掉所有的偶数值map
只是单纯返回会被 log 的字符串
最终运行结果为:
⚠️:这里运行的 observable 只有一个,就是被 subscribed 的 pipedObservable
subjects
当需要触发一些数据的变动,并将其传递到不同的组件时,之前的做法都是使用 EventEmitter
去实现,如下面这个 service 案例:
typescript
import { EventEmitter, Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UserService {
activatedEmitter = new EventEmitter<boolean>();
}
实际上在应用的时候,也可以用 subject
去取代 EventEmitter
,用法如下:
typescript
export class UserService {
activatedEmitter = new Subject<boolean>();
}
替换了 Subject
后,就不能使用 emit
,而是需要使用 next
去触发事件:
typescript
export class UserComponent implements OnInit {
id: number;
constructor(
private route: ActivatedRoute,
private userService: UserService
) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.id = +params.id;
});
}
}
其他 subscribe 的使用方法一致。
二者使用上的区别主要是是否与 @Output
搭配使用,本质上来说 EventEmitter
的 interface 是这样的:
typescript
/**
* Use in components with the `@Output` directive to emit custom events
* synchronously or asynchronously, and register handlers for those events
* by subscribing to an instance.
*/
export declare interface EventEmitter<T> extends Subject<T> {}
它的初衷就是为了搭配 @Output
使用的,因此它做了一些限制,比如说只有父组件可以 subscribe @Output
项目部分更新
这里是对 第一个 Angular 项目 部分的更新,主要就是清理了一些代码,将 EventEmitter
更换成了 Subject
,其他方面没什么大的变动
除了清理部分的代码修改如下: