使用一個例子來研究 map 操作符的工作原理。
推薦閲讀本文之前,先瀏覽這篇文章RxJs fromEvent 工作原理分析以瞭解相關知識。
源代碼:
import { Component, OnInit, Inject } from '@angular/core';
import { fromEvent, combineLatest } from 'rxjs';
import { mapTo, startWith, scan, tap, map } from 'rxjs/operators';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-combine-latest',
templateUrl: './combine-latest.component.html'
})
export class CombineLatestComponent implements OnInit {
readonly document: Document;
constructor(
// https://github.com/angular/angular/issues/20351
@Inject(DOCUMENT) document: any) {
this.document = document as Document;
}
redTotal:HTMLElement;
blackTotal: HTMLElement;
total:HTMLElement;
test:HTMLElement;
ngOnInit(): void {
this.redTotal = this.document.getElementById('red-total');
this.blackTotal = this.document.getElementById('black-total');
this.total = this.document.getElementById('total');
this.test = this.document.getElementById('test');
combineLatest(this.addOneClick$('red'),
this.addOneClick$('black')).subscribe(([red, black]: any) => {
this.redTotal.innerHTML = red;
this.blackTotal.innerHTML = black;
this.total.innerHTML = red + black;
});
fromEvent(this.test, 'click').pipe(map( event => event.timeStamp)).subscribe((event) => console.log(event));
}
addOneClick$ = id =>
fromEvent(this.document.getElementById(id), 'click').pipe(
// map every click to 1
mapTo(1),
// keep a running total
scan((acc, curr) => acc + curr, 0),
startWith(0)
);
}
打開頁面,點擊 Test 按鈕,能在 Chrome 控制枱裏看到每次點擊發生時的 timestamp 時間戳:
下面介紹 map 操作符是如何起作用的。
先縷一縷順序:
- 首先執行fromEvent,返回一個 Observable 對象。
- 執行 map 操作符,其結果作為輸入,傳入 pipe
2.執行 pipe:
- 執行 subscribe 操作。
我們可以把 pipe 形象地想象成管道,通過 fromEvent 返回的 Observable 對象,流過一根根管道,最後觸發其訂閲者,執行訂閲者的邏輯。那麼 RxJs 提供的各種 operator,就是安裝在管道里的處理器。
map 操作的輸入是我們定義的映射函數,在 RxJs 上下文裏,稱為 project:
map 返回一個新的函數,名為 mapOperation. 新函數體裏,基於傳入的 project,創建一個新的 MapOperator. 這個 MapOperator,作為新函數輸入參數 source 的 lift 方法調用的輸入參數。到現在為止,我們尚且不知道 source 參數的類型。
接下來執行 Observable 的 pipe 方法。
operations 參數是 map operator 返回的新函數,mapOperation:
pipeFromArray 的實現,如果 pipe 輸入只有一個 operator,這種情況比較簡單,進入第 9 行的 IF 分支,直接將 map 返回的 mapOperation 函數作為 pipeFromArray 調用的返回結果。
注意到 Observable.js 實現裏,在 pipeFromArray(operations) 返回之後,緊跟了另一個括號,説明這是另一個函數調用,輸入參數為 this,即 Observable 對象本身。
現在進入到 map 操作返回的 新函數 mapOperation 的函數體內部了:
因為此時 button 尚未點擊,因此 Observable 對象並沒有 emit 值,只是完成相關的 setup 工作。
這行語句:
return source.lift(new MapOperator(project, thisArg));
只是返回一個新的 Observable 對象,其 source 屬性指向調用 lift 操作的原始 Observable 對象,而 operator 屬性指向 new MapOperator 返回的結果,後者是 project 的 wrapper.
如此一來,調用 subscribe 方法註冊應用程序監聽函數的 Observable 對象,再也不是 fromEvent 返回的原始 Observable 對象,而是前者調用了 pipe,接收了 map 指定的 project 之後,由 source.lift( new MapOperator) 返回的新 Observable 對象。
這個新的 Observable 對象,調用 subscribe 方法,執行邏輯和這篇文章RxJs fromEvent 工作原理分析介紹的相比有所差異,複雜度稍稍增加了。
把 Observable 對象 operator 屬性值提取出來:
接下來的 21行代碼執行,和之前沒有 operator 時相比,沒有差異,略過。
前一篇文章進入 ELSE 分支,而本文因為 operator 的存在,進入 22 行的 IF 分支:
首先執行 operator.call 方法:
MapSubscriber 也是 Subscriber 的子類之一,和其父類相比,多了 project 屬性。
再次執行 subscribe:
因為這次傳入的 Observable 是最原始的即 fromEvent 返回的 Observable,因此不存在 operator,所以進入 ELSE 分支執行:
重點分析 this 和 sink:
this 是 fromEvent 返回的原始 Observable,而 sink 是包含了 map operator 以及應用程序定義的訂閲邏輯的 Subscriber:
_trySubscribe 調用 _subscribe:
最終仍舊進入了 fromEvent 的核心邏輯:
這段代碼,定義了 fromEvent,以什麼樣的方式,emit 何種類型的數據。
- 什麼樣的方式?addEventListener,每次 eventTarget 定義的 HTMLElement 發生 click 事件時,emit 數據
- emit 的數據格式為 MouseEvent.
至此 Observable 相關的 setup 執行完畢。
點擊按鈕,觸發之前通過 addListener 註冊的 handler 函數。fromEvent.js 此處 subscriber 不是原始的 subscriber,而是 MapSubscriber,其 destination 屬性的 _next, 指向了應用程序指定的訂閲處理邏輯。Emit 的數據是 MouseEvent.
MapSubscriber 的特色:在將原始值 MouseEvent 交給應用程序之前,先要執行 project 對其進行處理:
這個 project 的邏輯是,將 MouseEvent 對象映射成 timestamp 時間戳:
將 project 處理結果返回給destination 繼續進行傳遞:
this._next 指向的是應用程序定義的 console.log(event), 在這裏得到執行:
更多Jerry的原創文章,盡在:"汪子熙":