[譯] 通過構建 Observable 來學習 Observable

SangKa發表於2017-09-24

原文連結: medium.com/@benlesh/le…
本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!
如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】

通過社交媒體或在活動現場,我經常會被問到關於“熱的” vs “冷的” observables,或者 observable 究竟是“多播”還是“單播”。對於 Rx.Observable,人們覺得它內部的工作原理完全是黑魔法,這令他們感到十分困惑。當被問及如何描述 observable 時,人們會說:“他們是流”或“他們類似於 promises ”。事實上,我在很多場合甚至在公開演講中都談論過這些內容。

與 promises 進行比較是必要的,但也是不幸的。鑑於 promieses 和 observables 都是非同步基本型別 ( async primitives ),並且 promises 已被 JavaScript 社群廣泛使用和熟悉,這通常是一個很好的起點。將 promise 的 then 與 observable 的 subscribe 進行比較,promise 是立即執行,而 observable 是惰性執行,是可取消、可複用的,等等。這是向初學者介紹 observables 的理想方式。

但這樣有個問題: Observables 與 promises 的不同點要遠多於它們之間的相同點。Promises 永遠是多播的。Promise 的解析 ( resolution ) 和拒絕 ( rejection ) 永遠是非同步的。當人們處理 observables 時,彷彿就是在處理 promises,所以他們期望兩者的行為也是相似的,但這不併總是對的。Observables 有時是多播的。Observables 通常是非同步的。我也有些自責,因為我助長了這種誤解的蔓延。

Observables 只是一個函式,它接收 observer 並返回函式

如果你真的想要理解 observable,你可以自己寫個簡單的。這並沒有聽上去那麼困難,真心的。將 observable 歸結為最精簡的部分,無外乎就是某種特定型別的函式,該函式有其針對性的用途。

模型:

  • 函式
  • 接收 observer: observer 是有 nexterrorcomplete 方法的物件
  • 返回一個可取消的函式

目的:

將觀察者 ( observer ) 與生產者 ( producer ) 連線,並返回一種手段來拆解與生產者之間的連線。觀察者實際上是處理函式的登錄檔,處理函式可以隨時間推移推送值。

基本實現:

function myObservable(observer) {
    const datasource = new DataSource();
    datasource.ondata = (e) => observer.next(e);
    datasource.onerror = (err) => observer.error(err);
    datasource.oncomplete = () => observer.complete();
    return () => {
        datasource.destroy();
    };
}複製程式碼

(你可以點選這裡進行線上除錯)

如你所見,並沒有太多東西,只是一個相當簡單的契約。

安全的觀察者: 讓觀察者變得更好

當談論到 RxJS 或響應式程式設計時,通常 observables 出現的頻率是最高的。但實際上,觀察者的實現才是這類響應式程式設計的中流砥柱。Observables 是惰性的。它們只是函式而已。它們什麼也不做直到你 subscribe 它們,它們裝配好了觀察者,然後就完事了,與沉悶的老式函式並無差別,等待著被呼叫而已。另一方面,觀察者保持活躍狀態並監聽來自生產者的事件。

你可以使用任何有 nexterrocomplete 方法的簡單 JavaScript 物件 (POJO) 來訂閱 observable,但你所用來訂閱 observable 的 POJO 觀察者真的只是個開始。在 RxJS 5中,我們需要為你提供一些保障。下面羅列了一些重要的保障:

觀察者保障

  1. 如果你傳遞的觀察者完全沒有以上所述的三個方法,也是可以的。
  2. 你不想在 completeerror 之後呼叫 next
  3. 如果取消訂閱了,那麼你不想任何方法被呼叫。
  4. 呼叫 completeerror 需要呼叫取消訂閱邏輯。
  5. 如果 nextcompleteerror 處理方法丟擲異常,你想要呼叫取消訂閱邏輯,以確保不會洩露資源。
  6. nexterrorcomplete 實際上都是可選的。你無需處理每個值、錯誤或完成。你可能只是想要處理其中一二。

為了完成列表中的任務,我們需要將你提供的匿名觀察者包裝在 “SafeObserver” 中以實施上述保障。因為上面的#2,我們需要追蹤 completeerror 是否被呼叫過。因為#3,我們需要使 SafeObserver 知道消費者何時要取消訂閱。最後,因為#4,SafeObserver 實際上需要了解取消訂閱邏輯,這樣當 completeerror 被呼叫時才可以呼叫它。

如果我們想要用上面臨時實現的 observable 函式來做這些的話,會變得有些粗糙... 這裡有個 JSBin 程式碼片段,你可以看看並感受下有多粗糙。我並沒有想要在這個示例中實現非常正宗的 SafeObserver,因為那麼佔用整篇文章篇幅,下面是我們的 observable,這次它使用了 SafeObserver:

function myObservable(observer) {
   const safeObserver = new SafeObserver(observer);
   const datasource = new DataSource();
   datasource.ondata = (e) => safeObserver.next(e);
   datasource.onerror = (err) => safeObserver.error(err);
   datasource.oncomplete = () => safeObserver.complete();

   safeObserver.unsub = () => {
       datasource.destroy();
   };

   return safeObserver.unsubscribe.bind(safeObserver);
}複製程式碼

設計 Observable: 確保觀察者安全

將 observables 作為類/物件使我們能夠輕鬆地將 SafeObserver 應用於傳入的匿名觀察者(和處理函式,如果你喜歡 RxJS 中的 subscribe(fn, fn, fn) 簽名的話) 併為開發人員提供更好的開發體驗。通過在 Observable 的 subscribe 實現中處理 SafeObserver 的建立,Observables 可以再次以最簡單的方式來定義:

const myObservable = new Observable((observer) => {
    const datasource = new DataSource();
    datasource.ondata = (e) => observer.next(e);
    datasource.onerror = (err) => observer.error(err);
    datasource.oncomplete = () => observer.complete();
    return () => {
        datasource.destroy();
    };
});複製程式碼

你會注意到上面的程式碼片段與第一個示例看起來幾乎一樣。但它更容易閱讀,也更容易理解。我擴充套件了 JSBin 示例來展示 Observable 的最小化實現

操作符: 同樣只是函式

RxJS 中的“操作符”只不過是接收源 observable 的函式,並返回一個新的 observable,當你訂閱該 observable 時它會訂閱源 observable 。我們可以實現一個基礎、獨立的操作符,如這個線上 JSBin 示例所示:

function map(source, project) {
  return new Observable((observer) => {
    const mapObserver = {
      next: (x) => observer.next(project(x)),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    };
    return source.subscribe(mapObserver);
  });
}複製程式碼

最重要的是要注意此操作符在做什麼: 當你訂閱它返回的 observable 時,它會建立 mapObserver 來完成工作並將 observermapObserver 連線起來。
構建操作符鏈實際上只是建立一個將觀察者與訂閱 ( subscription ) 連線起來的模板。

設計 Observable: 更優雅的操作符鏈

如果我們所有的操作符都是像上面示例中那樣用獨立函式實現的話,將操作符連結起來會有些難看:

map(map(myObservable, (x) => x + 1), (x) => x + 2);複製程式碼

可以想象一下上面的程式碼,巢狀了5個或6個更復雜的操作符將會產生更多的引數。導致程式碼完全不可讀。

你可以使用簡單的 pipe 實現 (正如 Texas Toland 所建議的那樣),它會對操作符陣列進行累加以生成最終的 observable,但這意味著要編寫更復雜的、返回函式的操作符,(點選這裡檢視 JSBin 示例)。這同樣沒有使得一切變得完美:

pipe(myObservable, map(x => x + 1), map(x => x + 2));複製程式碼

理想的是我們能夠將操作符以一種更自然的方式連結起來,比如這樣:

myObservable.map(x => x + 1).map(x => x + 2);複製程式碼

幸運的是,我們的 Observable 類已經支援這種操作符的鏈式行為。它不會給操作符的實現程式碼任何額外的複雜度,但它的代價是違背了我所提倡的“摒棄原型 ( prototype )”,一旦新增了足夠你使用的操作符,原型中的方法或許就太多了。點選 (這裡的 JSBin 示例) 檢視我們新增到 Observable 實現原型中的 map 操作符:

Observable.prototype.map = function (project) {
    return new Observable((observer) => {
        const mapObserver = {
            next: (x) => observer.next(project(x)),
            error: (err) => observer.error(err),
            complete: () => observer.complete()
        };
        return this.subscribe(mapObserver);
    });
};複製程式碼

現在我們擁有了更好的語法。這種方法還有其他好處,也更高階。例如,我們可以將 Observable 子類化為特定型別的 observables (例如包裝 Promise 或一組靜態值的 observables),並通過覆蓋它們來對我們的運算子進行優化。

TLDR: Observable 是函式,它接收 observer 並返回函式

記住,閱讀以上所有內容後,所有的這一切都是圍繞一個簡單的函式設計的。Observables 是函式,它接收 observer 並返回函式。僅此而已。如果你編寫了一個函式,它接收 observer 並返回函式,那它是非同步的,還是同步的?都不是,它就是個函式。任何函式的行為都完全取決於它是如何實現的。所以,當處理 Observable 時,就像你所傳遞的函式引用那樣對待它,而不是一些所謂的魔法,有狀態的外星型別。當你構建操作符鏈式,你真正要做的是構成一個函式,該函式會設定連結在一起的觀察者鏈,並將值傳遞給觀察者。

注意: 示例中的 Observable 實現仍然返回的是函式,而 RxJS 和 es-observable 規範返回的是 Subscription 物件。Subscription 物件是一種更好的設計,但我又得寫一整篇文章來講它。所以我只保留了它的取消訂閱功能,以保持本文中所有示例的簡單性。

相關文章