2019-05-18 Rx.js

通過「換一種思路」來解決「異步」問題
Rx.js比async還要好

我們的所有網(wǎng)頁應(yīng)用都是異步的:
腳本加載
播放器
數(shù)據(jù)訪問
動(dòng)畫
DOM事件綁定、數(shù)據(jù)事件綁定

異步編程


image.png

我們可以看到,異步編程中的狀態(tài)(state)是很難跟蹤的


image.png

三處用到了movieTicket變量
當(dāng)項(xiàng)目變復(fù)雜時(shí),你很難理解某個(gè)狀態(tài)是如何變化的。

另一方面,使用回調(diào)時(shí),try...catch 語法基本是沒用的


image.png

這是異步的,try catch捕獲不了

另外,如果你監(jiān)聽了一個(gè)事件卻忘了銷毀它,很容易造成內(nèi)存泄露。這在異步編程很常見。


image.png

只要按鈕不消失,匿名函數(shù)就不會(huì)消失
為了解決這些問題,讓我們回到 1994 年。1994 年有一本書叫做《設(shè)計(jì)模式》


image.png

這本書講了很多編程套路(編程套路就是設(shè)計(jì)模式)
image.png

書中介紹的所有設(shè)計(jì)模式之間的關(guān)系
這里只關(guān)注其中的兩個(gè)設(shè)計(jì)模式
Iterator 迭代器
Observer 觀察者

迭代器
function makeIterator(array){
var nextIndex = 0;

return {
   next: function(){
       return nextIndex < array.length ?
           {value: array[nextIndex++], done: false} :
           {done: true};
   }
};

}

var it = makeIterator(['a', 'b']);
console.log(it.next().value); // 'a'
console.log(it.next().value); // 'b'
console.log(it.next().done); // true

image.png

ES 6 提供了一個(gè)語法糖來達(dá)成迭代器模式,這個(gè)語法糖叫做生成器(Generator)


image.png

image.png

所謂迭代器模式就是你可以用 .next() API 來「依次」訪問下一項(xiàng)。(next只是一個(gè)函數(shù)名而已,可以隨意約定)
如果有下一項(xiàng),你就會(huì)得到 {value: 下一項(xiàng)的值, done: false}
如果沒有下一項(xiàng),你就會(huì)得到 {value: null, done: true}
觀察者模式
這個(gè)模式則是監(jiān)聽一個(gè)對(duì)象的變化,一旦對(duì)象發(fā)生變化,就調(diào)用你提供的函數(shù)。(JS 已廢棄 Object.observe(),請(qǐng)使用 Proxy API 代替)
var user = {
id: 0,
name: 'Brendan Eich',
title: 'Mr.'
};

// 創(chuàng)建用戶的greeting
function updateGreeting() {
user.greeting = 'Hello, ' + user.title + ' ' + user.name + '!';
}
updateGreeting();

Object.observe(user, function(changes) {
changes.forEach(function(change) {
// 當(dāng)name或title屬性改變時(shí), 更新greeting
if (change.name === 'name' || change.name === 'title') {
updateGreeting();
}
});
});


image.png

兩種模式的區(qū)別
假設(shè) A 是一個(gè)迭代器,那么 B 可以主動(dòng)使用 A.next() 來要求 A 產(chǎn)生變化。(B主動(dòng)要求A變化)
假設(shè) B 是一個(gè)觀察者,在觀察著 A,那么 A 一旦變化,A 就會(huì)主動(dòng)通知 B。(A變化之后B被動(dòng)接收通知)

或者這么說:在觀察者模式里,被觀察的人在迭代觀察者(調(diào)用觀察者的一個(gè)函數(shù))。
再說清楚一點(diǎn):觀察者就是一個(gè)迭代器,被觀察的人一旦有變化,就會(huì)調(diào)用觀察者的一個(gè)函數(shù)。

user .on change
observer.next()
只不過,觀察者永遠(yuǎn)可以 .next(),不會(huì)結(jié)束。而迭代器是會(huì)結(jié)束的,即返回 {done: true}

數(shù)組VS事件
Array: [ {x:1,y:1}, {x:2, y:2}, {x:10,y:10} ]
Event: {x:1,y:1} ... {x:2, y:2} ... {x:10, y:10}
數(shù)組和事件,有啥區(qū)別?

他們都是 collection(數(shù)據(jù)集、集合)。

為了闡述它倆之間的相同點(diǎn),我們來舉兩個(gè)例子。

首先我們介紹 Array 的 4 個(gè)操作:
forEach

[1,2,3].forEach(x=> console.log(x))
1
2
3
map
[1,2,3].map(x=> x+1)
[2,3,4]
filter
[1,2,3].filter(x => x>1)
[2,3]
concatAll(這不是標(biāo)準(zhǔn) API,不過很容易實(shí)現(xiàn)這個(gè) API)
[ [1] , [2,3], [], [4] ].concatAll()
[1,2,3,4]
用這幾個(gè) API 我們可以做一些 amazing 的事情,在 Netflix 我們主要向用戶展示一些好看的劇集:
我們需要展示評(píng)分最高的劇集給用戶。能不能用上面的操作做到呢?
let getTopRatedFilms = user =>
user.videoLists
.map( videoList =>
videoList.videos
.filter( video => video.rating === 5.0)
).concatAll()

getTopRatedFilms(currentUser)
.forEach(film => console.log(film) )
好,如果我現(xiàn)在告訴你,一個(gè)拖曳操作能用類似的代碼實(shí)現(xiàn),你相信嗎?
let getElemenetDrags = el =>
el.mouseDowns
.map( mouseDown =>
document.mouseMoves
.takeUntil(document.mouseUps)
)
.concatAll()

getElementDrags(div)
.forEach(position => img.position = position )
能做到這一切,都是因?yàn)?Observable(大意:可被觀察的對(duì)象)
Observable
Observable = Collections + Time


image.png

用途
Observable 可以表示
事件
數(shù)據(jù)請(qǐng)求
動(dòng)畫
而且可以方便的把這三種東西組合起來,因此,異步操作變得很簡單。

將事件轉(zhuǎn)化為 Observable 的 API 很簡單
var mouseDowns = Observable.fromEvent(element, 'mouseDown')

之前我們是如何操作事件的?——監(jiān)聽(或者叫做訂閱)
// 訂閱或監(jiān)聽
let handler = e => console.log(e)
document.addEventListener('mousemove', handler)
// 取消訂閱或去掉監(jiān)聽
document.removeEventListener('mousemove', handler)

現(xiàn)在我們?cè)趺磳?duì)事件進(jìn)行操作呢?——forEach

// 訂閱
let subscription = mouseMoves.forEach(e => console.log(e) )
// 取消訂閱
subscription.dispose()
將事件包裝成 Observable 對(duì)象,可以方便地使用 forEach/map/filter/takeUntil/concatAll 等 API 來操作,比之前的方式容易很多。

為了處理失敗情況,forEach 還可以接收兩個(gè)額外的參數(shù):


image.png

看起來有點(diǎn)像 Promise 對(duì)吧。

為了跟清楚地闡述如何使用 forEach/map/filter/takeUntil/concatAll 等 API 來操作 Observable 對(duì)象,我現(xiàn)在發(fā)明一種新的語法:


image.png

這個(gè)語法的規(guī)則是

{1...2} 表示這個(gè)對(duì)象會(huì)一開始發(fā)射一個(gè)1,一段時(shí)間后發(fā)射一個(gè)2
{1...2......3}表示發(fā)射1,一段時(shí)間后發(fā)射2,兩段時(shí)間后發(fā)射3(也就是說 ... 表示一段時(shí)間,...... 表示兩段時(shí)間)
forEach

{1......2............3}.forEach(console.log)
1
一段時(shí)間后
一段時(shí)間后
2
一段時(shí)間后
一段時(shí)間后
一段時(shí)間后
一段時(shí)間后
3
map
{1......2............3}.map(x=>x+1)
2
一段時(shí)間后
一段時(shí)間后
3
一段時(shí)間后
一段時(shí)間后
一段時(shí)間后
一段時(shí)間后
4
filter
{1......2............3}.filter(x=>x>1)
一段時(shí)間后
一段時(shí)間后
2
一段時(shí)間后
一段時(shí)間后
一段時(shí)間后
一段時(shí)間后
3


image.png

image.png

image.png

image.png

image.png

自動(dòng)搜索建議


image.png

這個(gè) demo 的難點(diǎn)有兩個(gè):
如果用戶依次輸入 abcdef,請(qǐng)問你應(yīng)該發(fā)送幾個(gè)請(qǐng)求?答案是用函數(shù)防抖,發(fā)一次請(qǐng)求。
如果用戶輸入 a,然后 300 毫秒后輸入 b,那么你會(huì)發(fā)兩個(gè)請(qǐng)求,一個(gè)請(qǐng)求查詢 a 相關(guān)的熱詞,一個(gè)請(qǐng)求查詢 ab 相關(guān)的熱詞,你能保證這兩個(gè)請(qǐng)求響應(yīng)的順序嗎?答案是不能。(競態(tài)問題)

使用 Observable 來思考這個(gè)問題
let search =
keyPresses
.debounce(250) //
.map(key =>
getJSON('/search?q=' + input.value)
.retry(3)
.takeUntil(keyPresses)
)
.concatAll()
search.forEach(
results => updateUI(results),
error => showMessage(error)
)

最開始的回調(diào)地獄
最后我們本文回到最開始的代碼

function play(movieId, cancelButton, callback){
let movieTicket
let playError
let tryFinish = () =>{
if(playError){
callback(playError)
}else if(movieTicket && player.initialized){
callback(null, movieTicket)
}
}
cancelButton.addEventListener('click', ()=>{ playError = 'cancel' })
if(!player.initialized){
player.init((error)=>{
playError = error
tryFinish()
})
}
authorizeMovie(movieId, (error, ticket)=>{
playError = error
movieTicket = ticket
tryFinish()
})
}

通過改變思維方式,你可以寫出這樣的代碼
let authorizations =
player.init()
.map(()=>
playAttempts
.map(movieId=>
player.authorize(movieId)
.retry(3)
.takeUntil(cancels)
)
.concatAll()
)
.concatAll()

authorizations.forEach(
license => player.play(license),
error => showError(error)
)
Rx.js的教程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一.背景介紹 Rx(Reactive Extension -- 響應(yīng)式擴(kuò)展 http://reactivex.io...
    愛上Shu的小刺猬閱讀 2,196評(píng)論 1 3
  • 介紹 RxJS是一個(gè)異步編程的庫,同時(shí)它通過observable序列來實(shí)現(xiàn)基于事件的編程。它提供了一個(gè)核心的類型:...
    泓滎閱讀 16,789評(píng)論 0 12
  • 注:只包含標(biāo)準(zhǔn)包中的操作符,用于個(gè)人學(xué)習(xí)及備忘參考博客:http://blog.csdn.net/maplejaw...
    小白要超神閱讀 1,057評(píng)論 0 3
  • ECMAScript簡稱就是ES,你可以把它看成是一套標(biāo)準(zhǔn),JavaScript就是實(shí)施了這套標(biāo)準(zhǔn)的一門語言 現(xiàn)在...
    最美時(shí)光A閱讀 395評(píng)論 0 0
  • 最近最有成就感的一件事就是本期的心靈寫作基本堅(jiān)持了下來,我非常自豪地宣布我堅(jiān)持了三個(gè)月的寫作,從最開始寫五百字都困...
    雅禪閱讀 533評(píng)論 2 4

友情鏈接更多精彩內(nèi)容