RxJS Observable - 一個奇特的函數

前言

RxJS 的 Observable 有點難理解,其實 RxJS 相關的概念都有點難理解。畢竟 RxJS 引入了響應式編程這種新的模式,會不習慣是正常的。不過總得去理解嘛,而認識新的事物時,如果能夠參照一個合適的已知事物比對著,會比較容易理解吧。對于 Observable,類比 JS 中的函數,還是比較好的。

開始

封裝

先來看一個普通函數調用的例子:

function foo() {
  console.log('process...')
}

foo()
// 輸出:
// process...

很簡單,函數 foo() 封裝了一段邏輯(這里只是向控制臺輸出),然后通過調用函數,函數執行內部的邏輯。

再來看 RxJS Observable 的一個例子:

var foo = Rx.Observable.create(() => {
  console.log('process...')
})

foo.subscribe()
// 輸出:
// process...

上例中,通過 Rx.Observable.create() 來創建 Observable 對象,將同樣將一段代碼邏輯封裝到 Observable 對象 foo 中,然后通過 foo.subscribe() 來執行封裝的代碼邏輯。

對于普通函數和 Observable 對象,封裝的代碼邏輯在每次調用時都會重新執行一次。從這一點來看,Observable 能夠和普通函數一樣實現封裝代碼進行復用。

返回值

函數調用后可以有返回值:

function foo() {
  console.log('process...')
  return 42
}

console.log(foo())
// 輸出:
// process...
// 42

Observable 執行后也會產生值,不過和函數直接返回的方式不同,要通過回調函數方式獲取:

var foo = Rx.Observable.create((observer) => {
  console.log('process...')
  observer.next(42)
})

foo.subscribe(value => console.log(value))
// 輸出:
// process...
// 42

Observable 對象內部是通過 observer.next(42) 這種方式返回值,而調用方則通過回調函數來接收返回的數據。形式上比普通函數直接返回值啰嗦一些。

從調用方的角度來看,兩個過程分別是:

  • 普通函數:調用 > 執行邏輯 > 返回數據
  • Observable:訂閱(subscribe) > 執行邏輯 > 返回數據

從獲取返回值方式來看,調用函數是一種直接獲取數據的模式,從函數那里“拿”(pull)數據;而 Observable 訂閱后,是要由 Observable 通過間接調用回調函數的方式,將數據“推”(push)給調用方。

這里 pull 和 push 的重要區別在于,push 模式下,Observable 可以決定什么時候返回值,以及返回幾個值(即調用回調函數的次數)。

var foo = Rx.Observable.create((observer) => {
  console.log('process...')
  observer.next(1)
  setTimeout(() => observer.next(2), 1000)
})

console.log('before')
foo.subscribe(value => console.log(value))
console.log('after')
// 輸出:
// before
// process...
// 1
// after
// 2

上面例子中,Observable 返回了兩個值,第1個值同步返回,第2個值則是過了1秒后異步返回。

也就是說,從返回值來說,Observable 相比普通函數區別在于:

  • 可以返回多個值
  • 可以異步返回值

異常處理

函數執行可能出現異常情況,例如:

function foo() {
  console.log('process...')
  throw new Error('BUG!')
}

我們可以捕獲到異常狀態進行處理:

try {
   foo()
} catch(e) {
  console.log('error: ' + e)
}

對于 Observable,也有錯誤處理的機制:

var foo = Rx.Observable.create((observer) => {
  console.log('process...')
  observer.error(new Error('BUG!'))
})

foo.subscribe(
  value => console.log(value),
  e => console.log('error: ' + e)
)

Observable 的 subscribe() 方法支持傳入額外的回調函數,用于處理異常情況。和函數執行類似,出現錯誤之后,Observable 就不再繼續返回數據了。

subscribe() 方法還支持另一種形式傳入回調函數:

foo.subscribe({
  next(value) { console.log(value) },
  error(e) { console.log('error: ' + e) }
})

而這種形式下,傳入的對象和 Observable 內部執行函數中的 observer 參數在形式上就比較一致了。

中止執行

Observable 內部的邏輯可以異步多個返回值,甚至返回無數個值:

var foo = Rx.Observable.create((observer) => {
  let i = 0
  setInterval(() => observer.next(i++), 1000)
})

foo.subscribe(i => console.log(i))
// 輸出:
// 0
// 1
// 2
// ...

上面例子中,Observable 對象每隔 1 秒會返回一個值給調用方。即使調用方不再需要數據,仍舊會繼續通過回調函數向調用推送數據。

RxJS 提供了中止 Observable 執行的機制:

var foo = Rx.Observable.create((observer) => {
  console.log('start')
  let i = 0
  let timer = setInterval(() => observer.next(i++), 1000)
  return () => {
    clearInterval(timer)
    console.log('end')
  }
})

var subscription = foo.subscribe(i => console.log(i))
setTimeout(() => subscription.unsubscribe(), 2500)
// 輸出:
// start
// 0
// 1
// 2
// end

subscribe() 方法返回一個訂閱對象(subscription),該對象上的 unsubscribe() 方法用于取消訂閱,也就是中止 Observable 內部邏輯的執行,停止返回新的數據。

對于具體的 Observable 對象是如何中止執行,則要由 Observable 在執行后返回一個用于中止執行的函數,像上面例子中的這種方式。

Observable 執行結束后,會觸發觀察者的 complete 回調,所以可以這樣:

foo.subscribe({
  next(value) { console.log(value) },
  complete() { console.log('completed') }
})

Observable 的觀察者共有上面三種回調:

  • next:獲得數據
  • error:處理異常
  • complete:執行結束

其中 next 可以被多次調用,error 和 complete 最多只有一個被調用一次(任意一個被調用后不再觸發其他回調)。

數據轉換

對于函數返回值,有時候我們要轉換后再使用,例如:

function foo() {
  return 1
}

console.log(f00() * 2)
// 輸出:
// 2

對于 Observable 返回的值,也會有類似的情況,不過通常采用下面的方式:

var foo = Rx.Observable.create((observer) => {
  let i = 0
  setInterval(() => observer.next(i++), 1000)
})

foo.map(i => i * 2).subscribe(i => console.log(i))
// 輸出:
// 0
// 2
// 4
// ...

其實 foo.map() 返回了新的 Observable 對象,上面代碼等價于:

var foo2 = foo.map(i => i * 2)
foo2.subscribe(i => console.log(i))

Observable 對象 foo2 被訂閱時執行的內部邏輯可以簡單視為:

function subscribe(observer) {
  let mapFn = v => v * 2
  foo.subscribe(v => {
    observer.next(mapFn(v))
  })
}

將這種對數據的處理和數組進行比較看看:

var array = [0, 1, 2, 3, 4, 5]
array.map(i => i * 2).forEach(i => console.log(i))

是不是有點像?

除了 map() 方法,Observable 還提供了多種轉換方法,如 filter() 用于過濾數據,find() 值返回第一個滿足條件的數據,reduce() 對數據進行累積處理,在執行結束后返回最終的數據。這些方法和數組方法功能是類似的,只不過是對異步返回的數據進行處理。還有一些轉換方法更加強大,例如可以 debounceTime() 可以在時間維度上對數據進行攔截等等。

Observable 的轉換方法,本質不過是創建了一個新的 Observable,新的 Observable 基于一定的邏輯對原 Observable 的返回值進行轉換處理,然后再推送給觀察者。

總結

Observable 就是一個奇怪的函數,它有和函數類似的東西,例如封裝了一段邏輯,每次調用時都會重新執行邏輯,執行有返回數據等;也有更特殊的特性,例如數據是推送(push)的方式返回給調用方法,返回值可以是異步,可以返回多個值等。

不過將 Observable 視作特殊函數,至少對于理解 Observable 上是比較有幫助的。

Observable 也被視為 data stream(數據流),這是從 Observable 可以返回多個值的角度來看的,而數據轉換則是基于當前數據流創建新的數據流,例如:

observable.map(x => 10 * x)

不過上圖看到的只是數據,而將 Observable 視為特殊函數時,不應該忘了其內部邏輯,不然數據是怎么產生的呢。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容