Pipeable 操作符
从5.5版本开始我们提供了 “pipeable 操作符”,它们可以通过 rxjs/operators
来访问 (注意 "operators" 是复数)。相比较于通过在 rxjs/add/operator/*
中以“打补丁”的方式来获取需要用到的操作符,这是一种更好的方式,
注意: 如果使用 rxjs/operators
而不修改构建过程的话会导致更大的包。详见下面的已知问题一节。
重命名的操作符
由于操作符要从 Observable 中独立出来,所以操作符的名称不能和 JavaScript 的关键字冲突。因此一些操作符的 pipeable 版本的名称做出了修改。这些操作符是:
-
do
->tap
-
catch
->catchError
-
switch
->switchAll
-
finally
->finalize
pipe
是 Observable
的一部分,不需要导入,并且它可以替代现有的 let
操作符。
source$.let(myOperator) -> source$.pipe(myOperator)
参见下面的“构建自己的操作符”。
之前的 toPromise()
“操作符”已经被移除了,因为一个操作符应该返回 Observable
,而不是 Promise
。现在使用 Observable.toPromise()
的实例方法来替代。
因为 throw
是关键字,你可以在导入时使用 _throw
,就像这样: import { _throw } from 'rxjs/observable/throw'
。
如果前缀_
使你困扰的话 (因为一般前缀_
表示“内部的 - 不要使用”) ,你也可以这样做:
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
...
const e = ErrorObservable.create(new Error('My bad'));
const e2 = new ErrorObservable(new Error('My bad too'));
为什么需要 pipeable 操作符?
打补丁的操作符主要是为了链式调用,但它存在如下问题:
任何导入了补丁操作符的库都会导致该库的所有消费者的
Observable.prototype
增大,这会创建一种依赖上的盲区。如果此库移除了某个操作符的导入,这会在无形之中破坏其他所有人的使用。使用 pipeable 操作符的话,你必须在每个用到它们的页面中都导入你所需要用到的操作符。通过打补丁的方式将操作符挂在原型上是无法通过像 rollup 或 webpack 这样的工具进行“摇树优化” ( tree-shakeable ) 。而 pipeable 操作符只是直接从模块中提取的函数而已。
对于在应用中导入的未使用过的操作符,任何类型的构建工具或 lint 规则都无法可靠地检测出它们。例如,比如你导入了
scan
,但后来不再使用了,但它仍会被添加到打包后的文件中。使用 pipeable 操作符的话,如果你不再使用它的简化,lint 规则可以帮你检测到。函数组合 ( functional composition )很棒。创建自定义操作符也变得非常简单,它们就像 rxjs 中的其他所有操作符一样。你不再需要扩展 Observable 或重写
lift
。
什么是 pipeable 操作符?
简而言之,就是可以与当前的 let
操作符一起使用的函数。无论名称起的是否合适,这就是它的由来。基本上来说,pipeable 操作符可以是任何函数,但是它需要返回签名为
的函数。
现在 Observable
中有一个内置的 pipe
方法 (Observable.prototype.pipe
),它可以用类似于之前的链式调用的方式来组合操作符 (如下所示)。
There is also a pipe
utility function at rxjs/util/pipe
that can be used to build reusable pipeable operators from other pipeable operators.
在 rxjs/util/pipe
中还有一个名为 pipe
的工具函数,它可用于构建基于其他 pipeable 操作符的可复用的 pipeable 操作符。
用法
你只需在 'rxjs/operators'
(注意是复数!) 中便能提取出所需要的任何操作符。还推荐直接导入所需的 Observable 创建操作符,如下面的 range
所示:
import { range } from 'rxjs/observable/range';
import { map, filter, scan } from 'rxjs/operators';
const source$ = range(0, 10);
source$.pipe(
filter(x => x % 2 === 0),
map(x => x + x),
scan((acc, x) => acc + x, 0)
)
.subscribe(x => console.log(x))
轻松创建自定义操作符
实际上,你可以一直用 let
来完成...,但是现在创建自定义操作符就像写个函数一样简单。注意,你可以将你的自定义操作符和其他的 rxjs 操作符无缝地组合起来。
import { interval } from 'rxjs/observable/interval';
import { filter, map, take, toArray } from 'rxjs/operators';
/**
* 取每第N个值的操作符
*/
const takeEveryNth = (n: number) => (source: Observable) =>
new Observable(observer => {
let count = 0;
return source.subscribe({
next(x) {
if (count++ % n === 0) observer.next(x);
},
error(err) { observer.error(err); },
complete() { observer.complete(); }
})
});
/**
* 还可以使用现有的操作符
*/
const takeEveryNthSimple = (n: number) => (source: Observable) =>
source.pipe(filter((value, index) => index % n === 0 ))
/**
* 因为 pipeable 操作符返回的是函数,还可以进一步简化
*/
const takeEveryNthSimplest = (n: number) => filter((value, index) => index % n === 0);
interval(1000).pipe(
takeEveryNth(2),
map(x => x + x),
takeEveryNthSimple(3),
map(x => x * x),
takeEveryNthSimplest(4),
take(3),
toArray()
)
.subscribe(x => console.log(x));
// [0, 2304, 9216]
已知问题
TypeScript < 2.4
在2.3及以下版本的 TypeScript 中,需要在传递给操作符的函数中添加类型,因为 TypeScript 2.4之前的版本无法推断类型。在TypeScript 2.4中,类型可以通过组合来正确地推断出来。
TS 2.3及以下版本
range(0, 10).pipe(
map((n: number) => n + '!'),
map((s: string) => 'Hello, ' + s),
).subscribe(x => console.log(x))
TS 2.4及以上版本
range(0, 10).pipe(
map(n => n + '!'),
map(s => 'Hello, ' + s),
).subscribe(x => console.log(x))
构建和摇树优化
当从清单文件导入(或重新导出)时,应用的打包文件有时会增大。现在可以从 rxjs/operators
导入 pipeable 操作符,但如果不更新构建过程的话,会经常导致应用的打包文件更大。这是因为默认情况下 rxjs/operators
会解析成 rxjs 的 CommonJS 输出。
为了使用新的 pipeable 操作符而不增加打包尺寸,你需要更新 Webpack 配置。这只适用于 Webpack 3+ ,因为需要依赖 Webpack 3中的新插件 ModuleConcatenationPlugin
。
路径映射
伴随 rxjs 5.5版本一同发布的是使用ES5 和 ES2015 两种语言级别的 ECMAScript 模块格式 (导入和导出)。你可以在 node_modules/rxjs/_esm5
和 node_modules/rxjs/_esm2015
下面分别找到这两个分发版本 ("esm"表示 ECMAScript 模块,数字"5"或"2015"代表 ES 语言级别)。在你的应用源码中,你应该从 rxjs/operators
导入,但在 Webpack 配置文件中,你需要将导入重新映射为 ESM5 (或 ESM2015) 版本。
如果 require('rxjs/_esm5/path-mapping')
,你将接收一个函数,该函数返回一个键值对的对象,该对象包含每个输入映射到磁盘上的文件位置。像下面这样使用该映射:
webpack.config.js
简单配置:
const rxPaths = require('rxjs/_esm5/path-mapping');
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: 'index.js',
output: 'bundle.js',
resolve: {
// 使用 "alias" 键来解析成 ESM 分发版
alias: rxPaths()
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
更多完整配置 (接近真正场景):
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DashboardPlugin = require('webpack-dashboard/plugin');
const nodeEnv = process.env.NODE_ENV || 'development';
const isProd = nodeEnv === 'production';
const rxPaths = require('rxjs/_esm5/path-mapping');
var config = {
devtool: isProd ? 'hidden-source-map' : 'cheap-eval-source-map',
context: path.resolve('./src'),
entry: {
app: './index.ts',
vendor: './vendor.ts'
},
output: {
path: path.resolve('./dist'),
filename: '[name].bundle.js',
sourceMapFilename: '[name].map',
devtoolModuleFilenameTemplate: function (info) {
return "file:///" + info.absoluteResourcePath;
}
},
module: {
rules: [
{ enforce: 'pre', test: /\.ts$|\.tsx$/, exclude: ["node_modules"], loader: 'ts-loader' },
{ test: /\.html$/, loader: "html" },
{ test: /\.css$/, loaders: ['style', 'css'] }
]
},
resolve: {
extensions: [".ts", ".js"],
modules: [path.resolve('./src'), 'node_modules'],
alias: rxPaths()
},
plugins: [
new webpack.DefinePlugin({
'process.env': { // eslint-disable-line quote-props
NODE_ENV: JSON.stringify(nodeEnv)
}
}),
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({
title: 'Typescript Webpack Starter',
template: '!!ejs-loader!src/index.html'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
filename: 'vendor.bundle.js'
}),
new webpack.optimize.UglifyJsPlugin({
mangle: false,
compress: { warnings: false, pure_getters: true, passes: 3, screw_ie8: true, sequences: false },
output: { comments: false, beautify: true },
sourceMap: false
}),
new DashboardPlugin(),
new webpack.LoaderOptionsPlugin({
options: {
tslint: {
emitErrors: true,
failOnHint: true
}
}
})
]
};
module.exports = config;
无法控制构建过程
如果你无法控制构建过程(或者无法更新至 Webpack 3+)的话,上述解决方案将不适合你。所以,从 rxjs/operators
导入很可能让应用的打包文件尺寸更大。但还是有解决办法的,你需要使用更深一层的导入,有点类似于5.5版本之前导入 pipeable 操作符的方式。
将:
import { map, filter, reduce } from 'rxjs/operators';
变成:
import { map } from 'rxjs/operators/map';
import { filter } from 'rxjs/operators/filter';
import { reduce } from 'rxjs/operators/reduce';
编写弹珠测试
“弹珠测试”是使用一种叫做 TestScheduler
的专用的虚拟调度器 (VirtualScheduler) 的测试。它们可以使我们以同步且可靠的方式来测试异步操作。“弹珠符号”是源自许多人的教程与文档,例如,@jhusain、@headinthebox、@mattpodwysocki 和 @staltz 。实际上,是由 @staltz 首先提出建议将其作为创建单元测试的 DSL (Domain Specific Language 领域专用语言),并且它已经经过改造并被采纳。
链接
- 贡献指南
- 行为准则
基础方法
单元测试添加了一些辅助方法,以使创建测试更容易。
-
hot(marbles: string, values?: object, error?: any)
- 创建一个“热的” Observable (Subject),它将在测试开始时表现得 好像已经在“运行中”了。一个有趣的不同点是hot
弹珠允许使用^
字符来标志“零帧”所在处。这正是 Observables 订阅的起始点, 同时也是测试的起始点。 -
cold(marbles: string, values?: object, error?: any)
- 创建一个“冷的” Observable ,当测试开始时它便开始订阅。 -
expectObservable(actual: Observable
- 当 TestScheduler flush时, 安排一个断言。在 jasmine 的).toBe(marbles: string, values?: object, error?: any) it
块结束处 TestScheduler 会自动进行 flush 。 -
expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string)
- 类似expectObservable
,当 TestScheduler flush时,安排一个断言。cold()
和hot()
都返回带有subscriptions
属性 (类型为SubscriptionLog[]
)的 Observable 。将subscriptions
作为参数传给expectSubscriptions
以断言是否匹配在toBe()
中 给定的expectObservable
弹珠图。Subscription 的弹珠图与 Observable 的弹珠图略有不同。详情请参见下面。
hot
和 cold
的默认行为是符合人类认知的
在 hot
和 cold
方法中,弹珠图中指定的值的字符都会作为字符串发出,除非将 values
参数传给了方法。因此:
hot('--a--b')
会发出 "a"
和 "b"
,但
hot('--a--b', { a: 1, b: 2 })
会发出 1
和 2
。
同样的,未指明的错误就只发出默认字符串 "error"
,所以:
hot('---#')
会发出错误 "error"
, 但
hot('---#', null, new SpecialError('test'))
会发出 new SpecialError('test')
。
弹珠语法
弹珠语法是用字符串表示随“时间”流逝而发生的事件。任何弹珠字符串的首字符永远都表示“零帧”。“帧”是有点类似于虚拟毫秒的概念。
-
"-"
时间: 10“帧”的时间段。 -
"|"
完成: 表示 Observalbe 成功完成。这是 Observable 生产者所发出的complete()
信号。 -
"#"
错误: 终止 Observable 的错误。 这是 Observable 生产者所发出的error()
信号。 -
"a"
任意字符: 所有其他字符表示由 Observalbe 生产者所发出的next()
信号的值。 -
"()"
同步分组: 当多个事件需要在同一帧中同步地发出,用圆括号来将这些事件聚集在一起。你可以以这种形式来聚合值、完成或错误。 起始(
的位置决定了值发出的时间。 -
"^"
订阅时间点: (只适用于热的 Observabe) 显示测试 Observable 订阅热的 Observable 的点。它是此 Observable 的“零帧”,在^
前的所有帧都将是无效的。
示例
'-'
或 '------'
: 相当于 Observable.never()
,或一个从不发出值或完成的 Observable
|
: 相当于 Observable.empty()
#
: 相当于 Observable.throw()
'--a--'
: 此 Observable 等待20“帧”后发出值 a
,然后永远不发出 complete
'--a--b--|
: 在20帧处发出 a
,在50帧处发出 b
,然后在80帧处发出 complete
'--a--b--#
: 在20帧处发出 a
,在50帧处发出 b
,然后在80帧处发出 error
'-a-^-b--|
: 这是个热的 Observable ,在-20帧处发出 a
,然后在20帧处发出 b
,在50帧出发出 complete
'--(abc)-|'
: 在20帧处发出 a
、b
和 c
,然后在80帧处发出 complete
'-----(a|)'
: 在50帧处发出 a
和 complete
Subscription 的弹珠语法
Subscription 的弹珠语法与常见的弹珠语法略有不同。它表示随“时间”流逝而发生的订阅和取消订阅的时间点。在此类图中不应该出现其他类型的事件。
-
"-"
时间: 10“帧”的时间段。 -
"^"
订阅时间点: 显示订阅发生的时间点。 -
"!"
取消订阅时间点: 显示取消订阅发生的时间点。
在 Subscription 的弹珠语法中 ^
和 !
时间点最多只有一个。 除此之外,-
字符是唯一允许出现的字符。
示例
'-'
或 '------'
: 没有订阅发生。
'--^--'
: 在20帧处发生了订阅,并且订阅没有被取消。
'--^--!-
: 在20帧处发生了订阅,在50帧处订阅被取消了
测试剖析
一个基础的测试看起来应该是下面这样的:
var e1 = hot('----a--^--b-------c--|');
var e2 = hot( '---d-^--e---------f-----|');
var expected = '---(be)----c-f-----|';
expectObservable(e1.merge(e2)).toBe(expected);
-
hot
observables 的^
字符应该永远是对齐的。 -
cold
observables 或预期 observables 的首字符和hot
observables 的^
应该永远彼此对齐的。 - 尽量使用默认的发送值。当必要的时候才指定值 。
使用指定值的测试用例:
var values = {
a: 1,
b: 2,
c: 3,
d: 4,
x: 1 + 3, // a + c
y: 2 + 4, // b + d
}
var e1 = hot('---a---b---|', values);
var e2 = hot('-----c---d---|', values);
var expected = '-----x---y---|';
expectObservable(e1.zip(e2, function(x, y) { return x + y; }))
.toBe(expected, values);
- 使用同一个散列表来查找所有的值,这可以确保多次使用的同一个字符有着同样的值。
- 将结果值所表示的含义表达的越明显越好,毕竟这些是测试,我们最想要的是代码的清晰程度,而不是执行效率,所以
x: 1 + 3, // a + c
比x: 4
要好。前者传达了为什么是4,而后者并没有做到这点。
使用 subscription 断言的测试用例:
var x = cold( '--a---b---c--|');
var xsubs = '------^-------!';
var y = cold( '---d--e---f---|');
var ysubs = '--------------^-------------!';
var e1 = hot( '------x-------y------|', { x: x, y: y });
var expected = '--------a---b----d--e---f---|';
expectObservable(e1.switch()).toBe(expected);
expectSubscriptions(x.subscriptions).toBe(xsubs);
expectSubscriptions(y.subscriptions).toBe(ysubs);
- 将
xsubs
图和ysubs
图的开头与expected
图对齐。 - 注意冷的 observable
x
取消订阅的同时e1
发出了y
。
在大多数测试中,是没有必要测试订阅时间点和取消订阅时间点的,它们要不就非常明显,要不就在 expected
图中有所暗示。在这些情况下是不需要编写 subscription 断言的。在有内部 subscriptions 或冷的 observables 有多个订阅者的测试用例中,这些 subscription 断言还是有用的。
基于测试生成 PNG 弹珠图
通常,Jasmine 中的测试用例都是这样写的:it('should do something', function () { /* ... */ })
。要想时测试用例可以用来生成 PNG 弹珠图,你必须使用 asDiagram(label)
函数,像这样:
it.asDiagram(operatorLabel)('should do something', function () {
// ...
});
举例来说,对于 zip
操作符,我们可以这样写:
it.asDiagram('zip')('should zip by concatenating', function () {
var e1 = hot('---a---b---|');
var e2 = hot('-----c---d---|');
var expected = '-----x---y---|';
var values = { x: 'ac', y: 'bd' };
var result = e1.zip(e2, function(x, y) { return String(x) + String(y); });
expectObservable(result).toBe(expected, values);
});
然后当运行 npm run tests2png
时,这个测试用例会解析并且在 img/
文件夹下创建一个 PNG 文件 zip.png
(文件名取决于 ${operatorLabel}.png
)。
创建操作符
创建 RxJS 的操作符可以有多种方式。在这个版本的 RxJS 中,性能是首要考虑因素,因此,在此库中将创建的操作符依附到现有结构中的这种方式可能不那么直截了当。这是一个尝试性的文档,以告诉你如何创建一个操作符,这个操作符可以是你自己使用,也可以是用来添加到库中。
想了解如何为本库开发一个自定义操作符,请参见下面。
自动动手来为终端用户提供自定义的操作符
指南
在大多数情况下,用户可能想要创建一个只在它们的应用中使用的操作符。可以用任何开发者觉得适合的方式来进行开发,但这里有一些指导方针:
- **操作符应该永远返回一个 Observable **。你正在对一个未知的集合执行操作以创建一个新的集合。只有返回一个新的集合才有意义。如果你创建 了一个返回非 Observable 的方法,那么它就不是一个操作符,好吧。
- 确保对你的操作符返回的 Observalbe 内部所创建的 subscriptions 进行管理。你的操作符需要订阅返回 Observable 中的源(或
this
), 确保它是作为取消订阅处理方法或 subscription 的一部分返回的。 - 确保处理传入函数中的异常。如果你实现的操作符接收函数作为参数,当你调用它时,你会想要将其包裹在
try/catch
中并发送 错误到 observable 的error()
路径。 - 确保在返回的 Observable 的取消订阅处理方法中释放稀缺资源。如果你设置了事件处理方法,或 web socket,或一些其他类似的,取消订阅 方法是移除事件处理方法和关闭 socket 的好地方。
示例
function mySimpleOperator(someCallback) {
// 我们可以在这写 `var self = this;` 以保存 `this` ,但看下一条注释
return Observable.create(subscriber => {
// 因为我们正在箭头函数中,`this` 来自于外部作用域。
var source = this;
// 保存我们的内部 subscription
var subscription = source.subscribe(value => {
// 重点:从用户提供的回调函数中捕获错误
try {
subscriber.next(someCallback(value));
} catch(err) {
subscriber.error(err);
}
},
// 确保处理错误,然后视情况而定进行完成并发送它们
err => subscriber.error(err),
() => subscriber.complete());
// 现在返回
return subscription;
});
}
将操作符添加到 Observalbe 中
有几种方法可以做到这点。至于使用哪种方法取决于需求和偏好:
- 使用 ES7 函数绑定操作符 (
::
),在像 BabelJS 这样的编译器中是可用的:
someObservable::mySimpleOperator(x => x + '!');
- 创建你自己的 Observable 子类并重载
lift
方法将其返回:
class MyObservable extends Observable {
lift(operator) {
const observable = new MyObservable(); //<-- 这里是关键点
observable.source = this;
observable.operator = operator;
return observable;
}
// 放在这里 .. 或 ..
customOperator() {
/* 做些事情并返回 Observable */
}
}
// ... 放在这里...
MyObservable.prototype.mySimpleOperator = mySimpleOperator;
- 直接在
Observable.prototype
上打补丁:
Observable.prototype.mySimpleOperator = mySimpleOperator;
// ... 然后 .../
someObservable.mySimpleOperator(x => x + '!');
作为纯函数的操作符
如果你不想在 Observable 原型上打补丁的话,还可以编写纯函数作为操作符,此函数接收输入 Observable 作为参数来替代对 this
关键字的依赖。
示例实现:
function mySimpleOperator(someCallback) {
// 注意这里返回的是函数
return function mySimpleOperatorImplementation(source) {
return Observable.create(subscriber => {
var subscription = source.subscribe(value => {
try {
subscriber.next(someCallback(value));
} catch(err) {
subscriber.error(err);
}
},
err => subscriber.error(err),
() => subscriber.complete());
return subscription;
});
}
}
现在可以使用 Observable 上的 pipe()
方法:
const obs = someObservable.pipe(mySimpleOperator(x => x + '!'));
将操作符作为独立的库发布
我们强烈推荐你将使用了 let
的纯函数的自定义操作符作为独立的 npm 包。RxJS 核心已经超过100个操作符,我们不应该再增加额外的操作符了,除非它们是绝对必要的,并且提供了现有操作符无法提供的功能。
将其作为一个独立的库发布将保证你的操作符能够立即为社区所用,并且这是对 RxJS 社区生态的一种促进与发展,而不是让 RxJS 库变得愈发笨重。但是,在某些情况下,新操作符还是应该添加到核心库中。
创建加入到此库中的操作符
在提议将操作符加入 RxJS 库之前,请先将其作为独立的库进行发布。 参见上一节。
要创建加入到此库中的操作符,最好是基于现有的成果来工作。像 filter
这样的操作符会是不错的开始。没有人会期望你在读完本章后会立即成为一个专家级的操作符贡献者。
如果你觉得自己很困惑,请不要担心。按照仓库中之前的示例,提交 PR,我们同你并肩作战。
当你为本库开发操作符时,希望这里所提供的信息能帮你做出决策。当开发操作符时,这里有一些需要知道并(尝试)理解的知识:
- 实际上所有的操作符方法都是在
Observable
之外的单独模块中创建的。这是因为开发者可以通过将操作符方法提取出来并在自己的模块中 将其方法添加到 observable 中来“构建自己的 observable ”。这也就意味着可以操作符可以单独加入并直接使用,无论是在 Babel 中使用 ES7 函数绑定操作符还是通过.call()
来使用。 - 每个操作符都有一个
Operator
类。Operator
类实际上是一个Subscriber
“工厂”。它会被传递给lift
方法来使“魔法”发生。 它唯一的工作就是在 subscription 上创建操作符的Subscriber
实例。 - 每个操作符都有一个
Subscriber
类。 这个类完成了操作符的所有逻辑。它的工作就是处理接下来的值(通常是通过重载_next()
), 然后将其转发给destination
,即链中的下一个观察者。- 重要的是要注意,在任何
Subscriber
服务上设置的destination
观察者不仅仅是传递的事件的目的地,如果destination
是Subscriber
的话,它也用于设置共享底层的Subscription
,其实际上也是Subscriber
,并且是链中的第一个Subscriber
。 -
Subscribers
都有add
和remove
方法,用来添加和移除共享底层的 subscription 的内部 subscriptions 。 - 当你订阅 Observable 时,传递的函数或观察者用于创建链的最终
destination
Subscriber
。它是Subscriber
,实际上也是操作符链的共享Subscription
。
- 重要的是要注意,在任何
为操作符提交 PR
请为每个要添加到 RxJS 的新操作符完成以下步骤,并作为一个 Pull Request:
- 将操作符添加到 Rx 中
- 必须有一个
-spec.ts
的测试文件来涵盖典型用例,请使用弹珠图测试 - 如果可以的话,请再编写一个
asDiagram
测试用例,用于生成 PNG 弹珠图 - spec 文件应在末尾处有类型定义测试,以验证各种用例的类型定义
- 操作符必须在实现文件中用 JSDoc 风格的文档,同样包括 PNG 弹珠图
- 操作符应该在文件
doc/operators.md
中的操作符分类中列举出来 - 还应该在操作符决策树文件
doc/decision-tree-widget/tree.yml
中将其加入 - 如果操作符与 RxJS v4 中相对应的有所不同的话,你可能需要更新文件
MIGRATION.md
内部 Subscriptions
“内部 subscriber” 或“内部 subscription” 是在操作符的主要订阅者中创建的任意 subscription 。例如,如果你要创建一个自己的 “merge” 操作符,在你想要 “合并” 的源 Observable 上接收到的 Observable 需要被订阅。这些 subscriptions 就是内部 subscriptions 。在本库中关于内部 subscriptions 的一个有趣点就是,如果你传递给并设置了 destination
,它们会尝试为它们的 unsubscribe
调用使用 destination
。意味着如果你调用它们的 unsubscribe
,它可能不会做任何事情。因此,通常不会为内部 subscriptions 设置 destination
。一个例子可能就是 switch
操作符,它具有单独的底层内部 subscription ,此内部 subscription 取消订阅需要不依赖于主 subscription 。
如果你发现自己创建了内部 subscriptions ,你可能真的需要检查,看看 _isScalar
是否传给了 observable ,因为如果是的话,你可以直接将 value
提取出来,并当操作标量 observable 时提升操作符性能。作为参考,标量 observable 是具有单一静态值的 observable 。Observable.of('foo')
会返回一个 ScalarObservable
,同样的,resolve 过的 `PromiseObservable 也充当标量。