通過「換一種思路」來解決「異步」問題
Rx.js比async還要好
我們的所有網(wǎng)頁應(yīng)用都是異步的:
腳本加載
播放器
數(shù)據(jù)訪問
動(dòng)畫
DOM事件綁定、數(shù)據(jù)事件綁定
異步編程

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

三處用到了movieTicket變量
當(dāng)項(xiàng)目變復(fù)雜時(shí),你很難理解某個(gè)狀態(tài)是如何變化的。
另一方面,使用回調(diào)時(shí),try...catch 語法基本是沒用的

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

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

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

書中介紹的所有設(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

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


所謂迭代器模式就是你可以用 .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();
}
});
});

兩種模式的區(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

用途
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ù):

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

這個(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)搜索建議

這個(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的教程
- 英文教程:http://reactivex.io/learnrx/ (演講者自己寫的教程)
- 中文教程:https://www.google.com/search?q=site%3Azhihu.com+太狼+rxjs (我推薦太狼在知乎上寫的 Rx.js 教程)




