RxJSincludes操作符完全指南:在 Observable 流中检索元素与 fromIndex 起始索引解析
【免费下载链接】RxJSThe Reactive Extensions for JavaScript项目地址: https://gitcode.com/gh_mirrors/rxj/RxJS
本文是 RxJS(The Reactive Extensions for JavaScript)Rx.Observable.prototype.includes(searchElement, [fromIndex])操作符的技术详解,聚焦于如何在异步数据流中"从指定起始位置开始查找某个值是否存在"。读完本文,你将掌握includes的参数语义、返回值契约、边界行为(负数索引、Infinity、NaN、-0),并能结合仓库源码与单元测试理解其底层实现原理,在实际项目中正确选用它替代已弃用的contains。
一、includes是什么
includes是一个聚合类(aggregates)操作符,用于判断一个 Observable 序列是否包含指定的元素,并支持通过fromIndex指定搜索的起始位置。它在概念上等价于数组方法Array.prototype.includes,但以响应式的方式工作:它不直接返回布尔值,而是返回一个只发射单个布尔元素的 Observable。
该操作符在 关联文档 中定义如下:
Rx.Observable.prototype.includes(searchElement, [fromIndex])同时文档还声明了一个**已弃用(DEPRECATED)**的别名:
Rx.Observable.prototype.contains(searchElement, [fromIndex]) **DEPRECATED**注意:从当前仓库源码看,src/core/linq/observable/includes.js 只实现了
observableProto.includes;contains作为历史遗留别名已被弃用,新代码应统一使用includes。
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
searchElement | Any | 是 | 要在源序列中定位的值 |
fromIndex | Number | 否 | 开始搜索的索引位置,未指定时默认为 0 |
返回值
Observable:一个包含单个元素的 Observable,该元素为布尔值,表示源序列从fromIndex位置起是否包含与searchElement匹配的值。
二、基础用法示例
以下示例完整取自 关联文档,可直接复制运行。
不带起始索引
/* Without an index */ var source = Rx.Observable.of(42) .includes(42); var subscription = source.subscribe( function (x) { console.log('Next: %s', x); }, function (err) { console.log('Error: %s', err); }, function () { console.log('Completed'); }); // => Next: true // => Completed带起始索引
/* With an index */ var source = Rx.Observable.of(1,2,3) .includes(2, 1); var subscription = source.subscribe( function (x) { console.log('Next: %s', x); }, function (err) { console.log('Error: %s', err); }, function () { console.log('Completed'); }); // => Next: true // => Completed第二个例子中,源序列为1, 2, 3,从索引1(即元素2)开始搜索,命中2,因此发射true并立即完成。
三、源码实现剖析
includes的完整实现位于 src/core/linq/observable/includes.js,整体采用"自定义 Observable + 自定义 Observer"的经典 RxJS 4 架构。代码结构清晰,由三个部分组成:
1.IncludesObservable:参数归一化与负索引短路
var IncludesObservable = (function (__super__) { inherits(IncludesObservable, __super__); function IncludesObservable(source, elem, idx) { var n = +idx || 0; Math.abs(n) === Infinity && (n = 0); this.source = source; this._elem = elem; this._n = n; __super__.call(this); } IncludesObservable.prototype.subscribeCore = function (o) { if (this._n < 0) { o.onNext(false); o.onCompleted(); return disposableEmpty; } return this.source.subscribe(new IncludesObserver(o, this._elem, this._n)); }; return IncludesObservable; }(ObservableBase));构造函数中的fromIndex归一化逻辑值得注意:
var n = +idx || 0:使用一元+将索引强制转为数字;当fromIndex未传入(undefined)时,+undefined为NaN,NaN || 0回退为0,这就是"未指定时默认为 0"的底层来源;Math.abs(n) === Infinity && (n = 0):当传入Infinity或-Infinity时,将其归一化为0。
而subscribeCore中的短路逻辑:只要fromIndex < 0,就直接发射false并完成,根本不会订阅源序列。这意味着负数索引始终返回false,这一点与原生数组includes的行为不同(数组的includes会将负数索引换算为length + fromIndex)。
2.IncludesObserver:逐元素匹配与提前终止
var IncludesObserver = (function (__super__) { inherits(IncludesObserver, __super__); function IncludesObserver(o, elem, n) { this._o = o; this._elem = elem; this._n = n; this._i = 0; __super__.call(this); } IncludesObserver.prototype.next = function (x) { if (this._i++ >= this._n && comparer(x, this._elem)) { this._o.onNext(true); this._o.onCompleted(); } }; IncludesObserver.prototype.error = function (e) { this._o.onError(e); }; IncludesObserver.prototype.completed = function () { this._o.onNext(false); this._o.onCompleted(); }; return IncludesObserver; }(AbstractObserver));匹配逻辑由三行核心代码构成:
- 观察者内部维护计数器
_i(从 0 递增),this._i++ >= this._n保证只有索引达到fromIndex之后的元素才参与比较; - 一旦命中(
comparer返回true),立即发射true并调用onCompleted()提前终止——源序列中后续元素不会再产生任何输出; - 若源序列正常结束仍未命中,
completed回调发射false并完成; - 源序列出错时,
error回调将错误原样透传给下游,不做吞掉或转换。
3. 比较器comparer:处理0、-0与NaN
function comparer(a, b) { return (a === 0 && b === 0) || (a === b || (isNaN(a) && isNaN(b))); }这里的相等语义与===有两点关键差异:
NaN等于NaN:NaN === NaN在 JavaScript 中为false,但includes通过isNaN(a) && isNaN(b)显式让两个NaN视为相等;0、+0、-0统一处理:(a === 0 && b === 0)分支确保0、+0、-0之间的任意组合都被判定为相等。
这意味着你可以用includes(NaN)在流中查找NaN值,这在原生===语义下是做不到的。
四、fromIndex边界行为速查
结合源码(src/core/linq/observable/includes.js)与单元测试(tests/observable/includes.js、src/modular/test/includes.js),fromIndex的边界行为可归纳如下:
fromIndex取值 | 行为 | 测试用例 |
|---|---|---|
未指定(undefined) | +undefined \|\| 0归一为 0,从开头搜索 | includes return positive等 |
0 | 从索引 0 开始搜索 | includes fromIndex zero |
正整数(如1) | 跳过前n个元素,从第n个索引起搜索;命中即true,否则序列结束时为false | includes fromIndex greater than zero hits / misses |
负数(如-1) | 直接发射false并完成,不订阅源序列 | includes fromIndex less than zero |
Infinity/-Infinity | 归一化为 0,等价于从开头搜索 | includes fromIndex Infinity |
NaN等无法转数字的值 | +idx \|\| 0回退为 0 | 从实现推断 |
需要特别强调的是Infinity被归一为 0这一行为:测试includes fromIndex Infinity中,序列在 210ms 发射2,调用xs.includes(2, Infinity)仍期望onNext(210, true)——因为索引被重置为 0,所以能够命中。
五、单元测试验证:行为契约一览
tests/observable/includes.js使用 RxJS 的TestScheduler与ReactiveTest对includes进行了完整覆盖,每个用例都精确断言了时间点与值,是理解该操作符行为契约的最佳参考:
- 空序列:
onCompleted(250)的源序列调用includes(42),期望onNext(250, false)+onCompleted(250)——空序列返回false; - 命中即完成:源在 210ms 发射
2,includes(2)期望onNext(210, true)+onCompleted(210),发射结果的时间点与被命中元素的时间点一致,且不再等待源序列的onCompleted(250),证明命中后立即提前终止; - 未命中:源发射
2但搜索-2,期望等到源序列结束时(250ms)才输出false; - 错误透传:源在 210ms 抛错,
includes期望onError(210, error),错误被原样传递; - 永不完结序列(
never):includes不会产生任何输出(results.messages.assertEqual()为空),符合"永不完结则结果永不出现"的响应式语义; fromIndex大于 0 且不命中:源2,3,4,5在 250ms 完成,includes(2, 1)期望onNext(250, false)——2位于索引 0,被fromIndex = 1跳过;fromIndex大于 0 且无终点:同一序列但无onCompleted,includes(2, 1)期望无任何输出;-0/+0/NaN匹配:includes(0)对源中的-0、+0均返回true,includes(NaN)对源中的NaN返回true。
模块化版本在 src/modular/test/includes.js 中提供了几乎一致的用例(使用 tape 风格),两套测试共同锁定了该操作符的语义。
六、模块化版本与分发渠道
模块化实现
除核心单文件版本外,仓库还提供了 webpack 模块化实现 src/modular/observable/includes.js,采用module.exports导出,并在 src/modular/index.js 中注册:
includes: require('./observable/includes'),模块化版本与核心版本的实现完全一致(同样的索引归一化、同样的比较器、同样的提前终止逻辑),只是换用了 CommonJS 依赖注入风格。
可用分发渠道
根据 关联文档 的 Location 章节,includes属于聚合类操作符,随以下构建产物分发:
- Dist 文件:
rx.all.js、rx.all.compat.js、rx.aggregates.js(依赖rx.js/rx.compat.js/rx.lite.js/rx.lite.compat.js作为前置); - NPM 包:
rx; - NuGet 包:
RxJS-All、RxJS-Aggregates,对应仓库中的 nuget/RxJS-All 与 nuget/RxJS-Aggregates; - 仓库模块目录:modules/rx-lite-aggregates 与 modules/rx-lite-aggregates-compat 中同样包含了聚合类操作符的独立构建。
完整的聚合类操作符清单可参阅 doc/libraries/main/rx.aggregates.md,对应的浏览器测试页为 tests/rx.aggregates.html。
七、使用建议与注意事项
- 优先使用
includes而非contains:contains已被文档标记为 DEPRECATED,当前源码也仅保留includes实现; - 结果是一个 Observable,不是布尔值:
includes返回的是发射单个布尔元素的流,需要subscribe才能获得结果,这也意味着它天然适用于与switch、merge等其他响应式操作符的组合场景; - 命中即提前终止:如果只需判断"是否存在",
includes不会等待源序列结束,适合用于长流甚至无限流的前置判断(但若元素位于流末尾且流不完结,则永远不会得出结果,参见includes never测试); - 负索引语义与原生数组不同:原生
Array.prototype.includes支持array.length + fromIndex式的负数换算,而 RxJS 的includes对负数索引直接返回false,移植代码时需留意; NaN可以匹配:得益于自定义比较器,includes(NaN)能够命中流中的NaN,这在依赖===的普通比较中无法实现。
【免费下载链接】RxJSThe Reactive Extensions for JavaScript项目地址: https://gitcode.com/gh_mirrors/rxj/RxJS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考