1,可观察对象
可观察对象支持在应用中的发布者和订阅者之间传递消息。 在需要进行事件处理、异步编程和处理多个值的时候,可观察对象相对其它技术有着显著的优点。
可观察对象是声明式的 —— 也就是说,虽然你定义了一个用于发布值的函数,但是在有消费者订阅它之前,这个函数并不会实际执行。 订阅之后,当这个函数执行完或取消订阅时,订阅者就会收到通知。
可观察对象可以发送多个任意类型的值 —— 字面量、消息、事件。无论这些值是同步发送的还是异步发送的,接收这些值的 API 都是一样的。 由于准备(setup)和清场(teardown)的逻辑都是由可观察对象自己处理的,因此你的应用代码只管订阅并消费这些值就可以了,做完之后,取消订阅。无论这个流是击键流、HTTP 响应流还是定时器,对这些值进行监听和停止监听的接口都是一样的。
由于这些优点,可观察对象在 Angular 中得到广泛使用,也同样建议应用开发者好好使用它
基本用法和词汇
作为发布者,你创建一个 Observable
的实例,其中定义了一个订阅者(subscriber)函数。 当有消费者调用 subscribe()
方法时,这个函数就会执行。 订阅者函数用于定义“如何获取或生成那些要发布的值或消息”。
要执行所创建的可观察对象,并开始从中接收通知,你就要调用它的 subscribe()
方法,并传入一个观察者(observer)。 这是一个 JavaScript 对象,它定义了你收到的这些消息的处理器(handler)。 subscribe()
调用会返回一个 Subscription
对象,该对象具有一个 unsubscribe()
方法。 当调用该方法时,你就会停止接收通知。//首先创建可观察对象,调用可观察对象的subscrib()函数,参数传递一个observer,意思是把此可观察对象订阅给观察者,此observer里面定义了可观察对象返回的数据的相关消息处理,而subscribe()函数最终会发返回一个subscription对象,此对象有一个unsubscribe()方法,调用此方法,停止观察者接收可观察对象发出的消息
下面这个例子中示范了这种基本用法,它展示了如何使用可观察对象来对当前地理位置进行更新。
// Create an Observable that will start listening to geolocation updates
// when a consumer subscribes.
const locations = new Observable((observer) => { //创建可观察对象,
// Get the next and error callbacks. These will be passed in when
// the consumer subscribes.
const {next, error} = observer;
let watchId;
// Simple geolocation API check provides values to publish
if ('geolocation' in navigator) {
watchId = navigator.geolocation.watchPosition(next, error);
} else {
error('Geolocation not available');
}
// When the consumer unsubscribes, clean up data ready for next subscription.
return {unsubscribe() { navigator.geolocation.clearWatch(watchId); }};
});
// Call subscribe() to start listening for updates.
const locationsSubscription = locations.subscribe({ //可观察对象订阅给next,error 并且返回locationsSubscription
next(position) { console.log('Current Position: ', position); },
error(msg) { console.log('Error Getting Location: ', msg); }
});
// Stop listening for location after 10 seconds
setTimeout(() => { locationsSubscription.unsubscribe(); }, 10000);//Subscription.unsubscribe()可停止接收可观察对象的消息
定义观察者
用于接收可观察对象通知的处理器要实现 Observer
接口。这个对象定义了一些回调函数来处理可观察对象可能会发来的三种通知:
通知类型 | 说明 |
---|---|
next | 必要。用来处理每个送达值。在开始执行后可能执行零次或多次。 |
error | 可选。用来处理错误通知。错误会中断这个可观察对象实例的执行过程。 |
complete | 可选。用来处理执行完毕(complete)通知。当执行完毕后,这些值就会继续传给下一个处理器。 |
观察者对象可以定义这三种处理器的任意组合。如果你不为某种通知类型提供处理器,这个观察者就会忽略相应类型的通知。
订阅
只有当有人订阅 Observable
的实例时,它才会开始发布值。 订阅时要先调用该实例的 subscribe()
方法,并把一个观察者对象传给它,用来接收通知。
为了展示订阅的原理,我们需要创建新的可观察对象。它有一个构造函数可以用来创建新实例,但是为了更简明,也可以使用 Observable
上定义的一些静态方法来创建一些常用的简单可观察对象:
-
of(...items)
—— 返回一个Observable
实例,它用同步的方式把参数中提供的这些值发送出来。 -
from(iterable)
—— 把它的参数转换成一个Observable
实例。 该方法通常用于把一个数组转换成一个(发送多个值的)可观察对象。
面的例子会创建并订阅一个简单的可观察对象,它的观察者会把接收到的消息记录到控制台中:
// Create simple observable that emits three values
const myObservable = of(1, 2, 3); //生成可观察对象实例
// Create observer object
const myObserver = { //观察对象
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
// Execute with the observer object
myObservable.subscribe(myObserver); //订阅 只有在订阅之后才开始发送消息
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification
另外,subscribe()
方法还可以接收定义在同一行中的回调函数,无论 next
、error
还是 complete
处理器。比如,下面的 subscribe()
调用和前面指定预定义观察者的例子是等价的。
myObservable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
创建可观察对象
使用 Observable
构造函数可以创建任何类型的可观察流。 当执行可观察对象的 subscribe()
方法时,这个构造函数就会把它接收到的参数作为订阅函数来运行。 订阅函数会接收一个 Observer
对象,并把值发布给观察者的 next()
方法。
比如,要创建一个与前面的 of(1, 2, 3)
等价的可观察对象,你可以这样做:
// This function runs when subscribe() is called
function sequenceSubscriber(observer) { //observer用来发布事件
// synchronously deliver 1, 2, and 3, then complete
observer.next(1);//observer用来发布事件 发出1
observer.next(2);//observer用来发布事件 发出2
observer.next(3);/observer用来发布事件 发出2
observer.complete();//observer用来发布事件 发出完成事件
// unsubscribe function doesn't need to do anything in this
// because values are delivered synchronously
return {unsubscribe() {}};
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);//创建可观察对象
// execute the Observable and print the result of each notification
sequence.subscribe({//订阅
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// 1
// 2
// 3
// Finished sequence
多播
典型的可观察对象会为每一个观察者创建一次新的、独立的执行。 当观察者进行订阅时,该可观察对象会连上一个事件处理器,并且向那个观察者发送一些值。当第二个观察者订阅时,这个可观察对象就会连上一个新的事件处理器,并独立执行一次,把这些值发送给第二个可观察对象。
有时候,不应该对每一个订阅者都独立执行一次,你可能会希望每次订阅都得到同一批值 —— 即使是那些你已经发送过的。这在某些情况下有用,比如用来发送 document
上的点击事件的可观察对象。
多播用来让可观察对象在一次执行中同时广播给多个订阅者。借助支持多播的可观察对象,你不必注册多个监听器,而是复用第一个(next
)监听器,并且把值发送给各个订阅者。
当创建可观察对象时,你要决定你希望别人怎么用这个对象以及是否对它的值进行多播。
修改这个可观察对象以支持多播,代码如下:
function multicastSequenceSubscriber() {
const seq = [1, 2, 3];
// Keep track of each observer (one for every active subscription)
const observers = [];
// Still a single timeoutId because there will only ever be one
// set of values being generated, multicasted to each subscriber
let timeoutId;
// Return the subscriber function (runs when subscribe()
// function is invoked)
return (observer) => {
observers.push(observer);
// When this is the first subscription, start the sequence
if (observers.length === 1) {
timeoutId = doSequence({
next(val) {
// Iterate through observers and notify all subscriptions
observers.forEach(obs => obs.next(val));
},
complete() {
// Notify all complete callbacks
observers.slice(0).forEach(obs => obs.complete());
}
}, seq, 0);
}
return {
unsubscribe() {
// Remove from the observers array so it's no longer notified
observers.splice(observers.indexOf(observer), 1);
// If there's no more listeners, do cleanup
if (observers.length === 0) {
clearTimeout(timeoutId);
}
}
};
};
}
// Run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(observer, arr, idx) {
return setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doSequence(observer, arr, ++idx);
}
}, 1000);
}
// Create a new Observable that will deliver the above sequence
const multicastSequence = new Observable(multicastSequenceSubscriber());
// Subscribe starts the clock, and begins to emit after 1 second
multicastSequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1 1/2 seconds, subscribe again (should "miss" the first value).
setTimeout(() => {
multicastSequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 1500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3 seconds): 2nd subscribe: 3
// (at 3 seconds): 2nd sequence finished
2,RxJS 库
创建可观察对象的函数
RxJS 提供了一些用来创建可观察对象的函数。这些函数可以简化根据某些东西创建可观察对象的过程,比如事件、定时器、承诺等等。比如:、
Create an observable from a promise:
import { from } from 'rxjs'; //从rxjs导入from
// Create an Observable out of a promise
const data = from(fetch('/api/endpoint')); //创建可观察对象
// Subscribe to begin listening for async result
data.subscribe({
next(response) { console.log(response); },
error(err) { console.error('Error: ' + err); },
complete() { console.log('Completed'); }
});
Create an observable from a counter:
import { interval } from 'rxjs'; //从rxjs导入interval 定时器
// Create an Observable that will publish a value on an interval
const secondsCounter = interval(1000); //创建定时器可观察对象
// Subscribe to begin publishing values
secondsCounter.subscribe(n =>
console.log(`It's been ${n} seconds since subscribing!`));
Create an observable from an event:
import { fromEvent } from 'rxjs'; //从rxjs导入fromevent
const el = document.getElementById('my-element');
// Create an Observable that will publish mouse movements
const mouseMoves = fromEvent(el, 'mousemove'); //创建formevent可观察对象
// Subscribe to start listening for mouse-move events
const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
// Log coords of mouse movements
console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
// When the mouse is over the upper-left of the screen,
// unsubscribe to stop listening for mouse movements
if (evt.clientX < 40 && evt.clientY < 40) {
subscription.unsubscribe();
}
});
Create an observable that creates an AJAX request:
import { ajax } from 'rxjs/ajax'; //从rxjs导入ajax
// Create an Observable that will create an AJAX request
const apiData = ajax('/api/data'); //创建ajax可观察对象
// Subscribe to create the request
apiData.subscribe(res => console.log(res.status, res.response));
操作符:
操作符是基于可观察对象构建的一些对集合进行复杂操作的函数。RxJS 定义了一些操作符,比如 map()
、filter()
、concat()
和 flatMap()
。
操作符接受一些配置项,然后返回一个以来源可观察对象为参数的函数。当执行这个返回的函数时,这个操作符会观察来源可观察对象中发出的值,转换它们,并返回由转换后的值组成的新的可观察对象。下面是一个简单的例子:
import { map } from 'rxjs/operators';
const nums = of(1, 2, 3);
const squareValues = map((val: number) => val * val); //map转换 阶乘
const squaredNums = squareValues(nums); //返回转换后的操作
squaredNums.subscribe(x => console.log(x));
// Logs
// 1
// 4
// 9
你可以使用管道来把这些操作符链接起来。管道让你可以把多个由操作符返回的函数组合成一个。pipe()
函数以你要组合的这些函数作为参数,并且返回一个新的函数,当执行这个新函数时,就会顺序执行那些被组合进去的函数。
应用于某个可观察对象上的一组操作符就像一个菜谱 —— 也就是说,对你感兴趣的这些值进行处理的一组操作步骤。这个菜谱本身不会做任何事。你需要调用 subscribe()
来通过这个菜谱生成一个结果。
例子如下:
import { filter, map } from 'rxjs/operators';
const nums = of(1, 2, 3, 4, 5); //使用of来生成可观察对象
// Create a function that accepts an Observable.
const squareOddVals = pipe( //通pipe来组合不同的函数
filter((n: number) => n % 2 !== 0),
map(n => n * n)
);
// Create an Observable that will run the filter and map functions
const squareOdd = squareOddVals(nums); //通过squareOddVals函数来返回新的可观察对象
// Subscribe to run the combined functions
squareOdd.subscribe(x => console.log(x));
pipe()
函数也同时是 RxJS 的 Observable
上的一个方法,所以你可以用下列简写形式来达到同样的效果:
import { filter, map } from 'rxjs/operators';
const squareOdd = of(1, 2, 3, 4, 5)
.pipe( //rxjs本身也有pipe函数
filter(n => n % 2 !== 0),
map(n => n * n)
);
// Subscribe to get values
squareOdd.subscribe(x => console.log(x));
常用操作符
RxJS 提供了很多操作符,不过只有少数是常用的。 下面是一个常用操作符的列表和用法范例,参见 RxJS API 文档。
类别 | 操作 |
---|---|
创建 |
|
组合 | combineLatest , concat , merge , startWith , withLatestFrom , zip |
过滤 | debounceTime , distinctUntilChanged , filter , take , takeUntil |
转换 | bufferTime , concatMap , map , mergeMap , scan , switchMap |
工具 | tap |
多播 | share |
错误处理
除了可以在订阅时提供 error()
处理器外,RxJS 还提供了 catchError
操作符,它允许你在管道中处理已知错误。
假设你有一个可观察对象,它发起 API 请求,然后对服务器返回的响应进行映射。如果服务器返回了错误或值不存在,就会生成一个错误。如果你捕获这个错误并提供了一个默认值,流就会继续处理这些值,而不会报错。
下面是使用 catchError
操作符实现这种效果的例子:
import { ajax } from 'rxjs/ajax';
import { map, catchError } from 'rxjs/operators';
// Return "response" from the API. If an error happens,
// return an empty array.
const apiData = ajax('/api/data').pipe(
map(res => {
if (!res.response) {
throw new Error('Value expected!');
}
return res.response;
}),
catchError(err => of([])) // 此处使用catcherror 来处理错误
);
apiData.subscribe({
next(x) { console.log('data: ', x); },
error(err) { console.log('errors already caught... will not run'); }
});
重试失败的可观察对象
catchError
提供了一种简单的方式进行恢复,而 retry
操作符让你可以尝试失败的请求。
可以在 catchError
之前使用 retry
操作符。它会订阅到原始的来源可观察对象,它可以重新运行导致结果出错的动作序列。如果其中包含 HTTP 请求,它就会重新发起那个 HTTP 请求。
下列代码把前面的例子改成了在捕获错误之前重发请求:
import { ajax } from 'rxjs/ajax';
import { map, retry, catchError } from 'rxjs/operators';
const apiData = ajax('/api/data').pipe(
retry(3), // Retry up to 3 times before failing //此处使用retry来再次尝试失败操作
map(res => {
if (!res.response) {
throw new Error('Value expected!');
}
return res.response;
}),
catchError(err => of([]))
);
apiData.subscribe({
next(x) { console.log('data: ', x); },
error(err) { console.log('errors already caught... will not run'); }
});
可观察对象的命名约定
由于 Angular 的应用几乎都是用 TypeScript 写的,你通常会希望知道某个变量是否可观察对象。虽然 Angular 框架并没有针对可观察对象的强制性命名约定,不过你经常会看到可观察对象的名字以“$”符号结尾。
这在快速浏览代码并查找可观察对象值时会非常有用。同样的,如果你希望用某个属性来存储来自可观察对象的最近一个值,它的命名惯例是与可观察对象同名,但不带“$”后缀。
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-stopwatch',
templateUrl: './stopwatch.component.html'
})
export class StopwatchComponent {
stopwatchValue: number;//可观察对象返回的数据的变量,通常和可观察对象同名
stopwatchValue$: Observable<number>; //可观察对象
start() {
this.stopwatchValue$.subscribe(num =>
this.stopwatchValue = num
);
}
}
3,Angular 中的可观察对象
Angular 使用可观察对象作为处理各种常用异步操作的接口。比如:
-
EventEmitter
类派生自Observable
。 -
HTTP 模块使用可观察对象来处理 AJAX 请求和响应。
-
路由器和表单模块使用可观察对象来监听对用户输入事件的响应。
事件发送器 EventEmitter
Angular 提供了一个 EventEmitter
类,它用来从组件的 @Output()
属性中发布一些值。EventEmitter
扩展了 Observable
,并添加了一个 emit()
方法,这样它就可以发送任意值了。当你调用 emit()
时,就会把所发送的值传给订阅上来的观察者的 next()
方法。
这种用法的例子参见 EventEmitter 文档。下面这个范例组件监听了 open
和 close
事件:
<zippy (open)="onOpen($event)" (close)="onClose($event)"></zippy>
组件的定义如下:
@Component({
selector: 'zippy',
template: `
<div class="zippy">
<div (click)="toggle()">Toggle</div>
<div [hidden]="!visible">
<ng-content></ng-content>
</div>
</div>`})
export class ZippyComponent {
visible = true;
@Output() open = new EventEmitter<any>(); //EventEmitter
扩展了 Observable
,并添加了一个 emit()
方法,这样它就可以发送任意值了
@Output() close = new EventEmitter<any>(); //EventEmitter
扩展了 Observable
,并添加了一个 emit()
方法,这样它就可以发送任意值了
toggle() {
this.visible = !this.visible;
if (this.visible) {
this.open.emit(null); //通过emit()方法发送事件,传给订阅者的next()方法
} else {
this.close.emit(null);//通过emit()方法发送事件,传给订阅者的next()方法
}
}
}
HTTP
Angular 的 HttpClient
从 HTTP 方法调用中返回了可观察对象。例如,http.get(‘/api’)
就会返回可观察对象。相对于基于承诺(Promise)的 HTTP API,它有一系列优点:
-
可观察对象不会修改服务器的响应(和在承诺上串联起来的
.then()
调用一样)。反之,你可以使用一系列操作符来按需转换这些值。 -
HTTP 请求是可以通过
unsubscribe()
方法来取消的。 -
请求可以进行配置,以获取进度事件的变化。
-
失败的请求很容易重试。
Async 管道
AsyncPipe 会订阅一个可观察对象或承诺,并返回其发出的最后一个值。当发出新值时,该管道就会把这个组件标记为需要进行变更检查的(译注:因此可能导致刷新界面)。
下面的例子把 time
这个可观察对象绑定到了组件的视图中。这个可观察对象会不断使用当前时间更新组件的视图。
@Component({
selector: 'async-observable-pipe',
template: `<div><code>observable|async</code>:
Time: {{ time | async }}</div>`// 把time可观察对象绑定到了组件的试图当中
})
export class AsyncObservablePipeComponent {
time = new Observable(observer => //time是个可观察对象
setInterval(() => observer.next(new Date().toString()), 1000)
);
}
路由器 (router)
Router.events
以可观察对象的形式提供了其事件。 你可以使用 RxJS 中的 filter()
操作符来找到感兴趣的事件,并且订阅它们,以便根据浏览过程中产生的事件序列作出决定。 例子如下:
import { Router, NavigationStart } from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-routable',
templateUrl: './routable.component.html',
styleUrls: ['./routable.component.css']
})
export class Routable1Component implements OnInit {
navStart: Observable<NavigationStart>;
constructor(private router: Router) {
// Create a new Observable that publishes only the NavigationStart event
this.navStart = router.events.pipe( //创建一个可观察对象,且只有当事件为NavigationStart的时候才发布
filter(evt => evt instanceof NavigationStart)
) as Observable<NavigationStart>;
}
ngOnInit() {
this.navStart.subscribe(evt => console.log('Navigation Started!'));
}
}
ActivatedRoute 是一个可注入的路由器服务,它使用可观察对象来获取关于路由路径和路由参数的信息。比如,ActivateRoute.url
包含一个用于汇报路由路径的可观察对象。例子如下:
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-routable',
templateUrl: './routable.component.html',
styleUrls: ['./routable.component.css']
})
export class Routable2Component implements OnInit {
constructor(private activatedRoute: ActivatedRoute) {}
ngOnInit() {
this.activatedRoute.url //ActivateRoute.url
包含一个用于汇报路由路径的可观察对象
.subscribe(url => console.log('The URL changed to: ' + url));
}
}
响应式表单 (reactive forms)
响应式表单具有一些属性,它们使用可观察对象来监听表单控件的值。 FormControl
的 valueChanges
属性和 statusChanges
属性包含了会发出变更事件的可观察对象。订阅可观察的表单控件属性是在组件类中触发应用逻辑的途径之一。比如:
import { FormGroup } from '@angular/forms';
@Component({
selector: 'my-component',
template: 'MyComponent Template'
})
export class MyComponent implements OnInit {
nameChangeLog: string[] = [];
heroForm: FormGroup;
ngOnInit() {
this.logNameChange();
}
logNameChange() {
const nameControl = this.heroForm.get('name');
nameControl.valueChanges.forEach( //nameControl 响应式表单 valueChanges包含了可发出事件的可观察对象
(value: string) => this.nameChangeLog.push(value)
);
}
}
4,可观察对象用法实战
可观察对象可以简化输入提示建议的实现方式。典型的输入提示要完成一系列独立的任务:
-
从输入中监听数据。
-
移除输入值前后的空白字符,并确认它达到了最小长度。
-
防抖(这样才能防止连续按键时每次按键都发起 API 请求,而应该等到按键出现停顿时才发起)
-
如果输入值没有变化,则不要发起请求(比如按某个字符,然后快速按退格)。
-
如果已发出的 AJAX 请求的结果会因为后续的修改而变得无效,那就取消它。
完全用 JavaScript 的传统写法实现这个功能可能需要大量的工作。使用可观察对象,你可以使用这样一个 RxJS 操作符的简单序列:
import { fromEvent } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
const searchBox = document.getElementById('search-box');
const typeahead = fromEvent(searchBox, 'input').pipe( //fromEvent产生可观察对象 pipe管道 转换以及过滤相关函数处理原数据,并且返回新数据
map((e: KeyboardEvent) => e.target.value),
filter(text => text.length > 2),
debounceTime(10),
distinctUntilChanged(),
switchMap(() => ajax('/api/endpoint'))
);
typeahead.subscribe(data => {
// Handle the data from the API
});
指数化退避
指数化退避是一种失败后重试 API 的技巧,它会在每次连续的失败之后让重试时间逐渐变长,超过最大重试次数之后就会彻底放弃。 如果使用承诺和其它跟踪 AJAX 调用的方法会非常复杂,而使用可观察对象,这非常简单:‘
import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) {
return pipe(
retryWhen(attempts => zip(range(1, maxTries), attempts)
.pipe(
map(([i]) => i * i),
mergeMap(i => timer(i * ms))
)
)
);
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}