本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-18/
今天我們要講三個非常重要的 operators,這三個 operators 在很多的 RxJS 相關的 library 的使用範例上都會看到。很多初學者在使用這些 library 時,看到這三個 operators 很可能就放棄了,但其實如果有把這個系列的文章完整看過的話,現在應該就能很好接受跟理解。
concatMap 其實就是 map 加上 concatAll 的簡化寫法,我們直接來看一個範例
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source
.map(e => Rx.Observable.interval(1000).take(3))
.concatAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
上面這個範例就可以簡化成
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source
.concatMap(
e => Rx.Observable.interval(100).take(3)
);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
前後兩個行為是一致的,記得 concatMap 也會先處理前一個送出的 observable 在處理下一個 observable,畫成 Marble Diagram 如下
source : -----------c--c------------------...
concatMap(c => Rx.Observable.interval(100).take(3))
example: -------------0-1-2-0-1-2---------...
這樣的行為也很常被用在發送 request 如下
function getPostData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source.concatMap(
e => Rx.Observable.from(getPostData()));
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
這裡我們每點擊一下畫面就會送出一個 HTTP request,如果我們快速的連續點擊,大家可以在開發者工具的 network 看到每個 request 是等到前一個 request 完成才會送出下一個 request,如下圖
這裡建議把網速模擬調到最慢
從 network 的圖形可以看得出來,第二個 request 的發送時間是接在第一個 request 之後的,我們可以確保每一個 request 會等前一個 request 完成才做處理。
concatMap 還有第二個參數是一個 selector callback,這個 callback 會傳入四個參數,分別是
回傳值我們想要的值,範例如下
function getPostData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source.concatMap(
e => Rx.Observable.from(getPostData()),
(e, res, eIndex, resIndex) => res.title);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
這個範例的外部 observable 送出的元素就是 click event 物件,內部 observable 送出的元素就是 response 物件,這裡我們回傳 response 物件的 title 屬性,這樣一來我們就可以直接收到 title,這個方法很適合用在 response 要選取的值跟前一個事件或順位(index)相關時。
switchMap 其實就是 map 加上 switch 簡化的寫法,如下
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source
.map(e => Rx.Observable.interval(1000).take(3))
.switch();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
上面的程式碼可以簡化成
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source
.switchMap(
e => Rx.Observable.interval(100).take(3)
);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
畫成 Marble Diagram 表示如下
source : -----------c--c-----------------...
concatMap(c => Rx.Observable.interval(100).take(3))
example: -------------0--0-1-2-----------...
只要注意一個重點 switchMap 會在下一個 observable 被送出後直接退訂前一個未處理完的 observable,這個部份的細節請看上一篇文章 switch 的部分。
另外我們也可以把 switchMap 用在發送 HTTP request
function getPostData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source.switchMap(
e => Rx.Observable.from(getPostData()));
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
如果我們快速的連續點擊五下,可以在開發者工具的 network 看到每個 request 會在點擊時發送,如下圖
灰色是瀏覽器原生地停頓行為,實際上灰色的一開始就是 fetch 執行送出 request,只是卡在瀏覽器等待發送。
從上圖可以看到,雖然我們發送了多個 request 但最後真正印出來的 log 只會有一個,代表前面發送的 request 已經不會造成任何的 side-effect 了,這個很適合用在只看最後一次 request 的情境,比如說 自動完成(auto complete),我們只需要顯示使用者最後一次打在畫面上的文字,來做建議選項而不用每一次的。
switchMap 跟 concatMap 一樣有第二個參數 selector callback 可用來回傳我們要的值,這部分的行為跟 concatMap 是一樣的,這裡就不再贅述。
mergeMap 其實就是 map 加上 mergeAll 簡化的寫法,如下
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source
.map(e => Rx.Observable.interval(1000).take(3))
.mergeAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
上面的程式碼可以簡化成
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source
.mergeMap(
e => Rx.Observable.interval(100).take(3)
);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
畫成 Marble Diagram 表示
source : -----------c-c------------------...
concatMap(c => Rx.Observable.interval(100).take(3))
example: -------------0-(10)-(21)-2----------...
記得 mergeMap 可以並行處理多個 observable,以這個例子來說當我們快速點按兩下,元素發送的時間點是有機會重疊的,這個部份的細節大家可以看上一篇文章 merge 的部分。
另外我們也可以把 mergeMap 用在發送 HTTP request
function getPostData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source.mergeMap(
e => Rx.Observable.from(getPostData()));
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
如果我們快速的連續點擊五下,大家可以在開發者工具的 network 看到每個 request 會在點擊時發送並且會 log 出五個物件,如下圖
mergeMap 也能傳入第二個參數 selector callback,這個 selector callback 跟 concatMap 第二個參數也是完全一樣的,但 mergeMap 的重點是我們可以傳入第三個參數,來限制並行處理的數量
function getPostData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source.mergeMap(
e => Rx.Observable.from(getPostData()),
(e, res, eIndex, resIndex) => res.title, 3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
這裡我們傳入 3 就能限制,HTTP request 最多只能同時送出 3 個,並且要等其中一個完成在處理下一個,如下圖
大家可以注意看上面這張圖,我連續點按了五下,但第四個 request 是在第一個完成後才送出的,這個很適合用在特殊的需求下,可以限制同時發送的 request 數量。
RxJS 5 還保留了 mergeMap 的別名叫 flatMap,雖然官方文件上沒有,但這兩個方法是完全一樣的。請參考這裡
這三個 operators 還有一個共同的特性,那就是這三個 operators 可以把第一個參數所回傳的 promise 物件直接轉成 observable,這樣我們就不用再用 Rx.Observable.from
轉一次,如下
function getPersonData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var example = source.concatMap(e => getPersonData());
//直接回傳 promise 物件
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
至於在使用上要如何選擇這三個 operators? 其實都還是看使用情境而定,這裡筆者簡單列一下大部分的使用情境
建議初學者不確定選哪一個時,使用 switchMap
在使用 concatAll 或 concatMap 時,請注意內部的 observable 一定要能夠的結束,且外部的 observable 發送元素的速度不能比內部的 observable 結束時間快太多,不然會有 memory issues
今天的文章內容主要講了三個 operators,如果有看完上一篇文章的讀者應該不難吸收,主要還是使用情境上需要思考以及注意一些細節。
不知道今天讀者有沒有收穫呢? 如果有任何問題,歡迎留言給我,謝謝
想額外請問一下,是什麼樣的情況會開始接觸到不同的pattern 或是筆者在什麼情況開始去學習比較不同pattern對於自己應用上的差異?
包含整個系列所提的observable, 或是functional programming vs object oriented 等等等討論,目前處在好像知道在幹嘛,好像說得出來一些優缺點,但其實什麼都不太了解的狀況。
(對於一個從前端開始入門碰到的問題,想尋求意見)
感謝
這裡有兩個問題 問題也都蠻大的 可以寫成兩篇文章來回答 XD
我先回答第一個問題
這是我自己的一些學習歷程跟心得
其實不用在意什麼 Design Pattern,甚至是 Programming Paradigm(FP, OOP) 也不用管,只要能把功能做出來完成需求,會用一些套件就很棒了。
在這個階段,你會一點點的 HTML、CSS、JS,要你修改現有專案的一些小功能是沒有問題的,但其實你常常是搞不清楚真正的運作原理,有時候是瞎貓碰到死耗子解出來的,程式碼也很有可能在一段時間後被你用的很亂。
在這個階段,你已經開始深入的學習 HTML、CSS 跟 JS,這三個基本的核心觀念你都已經具備,雖然沒有到滾瓜爛熟,也沒有非常的全面,但已經能夠理解絕大部分的程式碼,只是有時候還是會卡住,但只要再想想或是查資料就能夠解決,而且絕大多數的 Library 你一看就會知道要如何使用,並且開始對自己寫的程式碼有所要求。
在這個階段,你會使用一些前端工具,並且熟悉某個前端框架,能夠透過工具來建立純前端的專案,有能力獨立完成 SPA 的網站。
在這個時候,你會開始去學習如何讓自己的程式碼更加的簡潔,工作如何更有效率,開始嘗試看某些 Library 或 Framework 的 source code,雖然可能不是每次都能全部看懂,但有已經可以理解 6~7 成。
通常會在這個階段開始去研究各種 Design Pattern 還有 Programming Paradigm,但都只是剛開始應用,不會有很深的理解。
在這個階段,你很可能已經會了兩個以上的前端框架,並且有能力自己從無到有的建立前端專案,包含工作流程的自動化、各種前端工具的設定,以及自動化測試的撰寫。也對各個前端框架有所掌握,就算自己沒有用過,但也能透過文章或教學,快速理解其運作的原理,並且你同時也熟悉一個後端語言。
你也會在這個階段對各種 Design Pattern 有比較深入的體會,對不同的 Programming Paradigm 也能用程式碼清楚的表達。
這個階段,筆者也還沒到達只是自己的想像跟目標
在這個階段,你能用白話的方式清楚地闡述各種觀念,能很輕鬆的教別人學習前端甚至跟完全不懂技術的人有良好的溝通,並且對前端各種技術都有一定深度的認識,像是 SVG, Canvas, IndexDB, WebGL, Web Socket, Service Worker... 等。
這是我自己在學習前端過程區分的五個階段,大概會在第三到第四個階段開始對 Design Pattern 有所研究,如果你還沒到第三個階段,Design Pattern 請看看就好,讓自己先打好 HTML, CSS, JS 的基礎,並且找一個 framework 來學,並且實際用在專案上,在這個過程當中很自然的就會碰到 Design Pattern。
第二個問題應該是 不太清楚 FP 跟 OOP 還有 Observable
這邊要先釐清
Observable 可以說是一種 Design Pattern (只是沒有出現在四人幫的書裡)
FP 跟 OOP 則是 Programming Paradigm
其實 FP 跟 OOP 這兩個並不相斥,他們是可以同時使用並且能夠很好的搭配在一起,所以不用擔心說是不是用了一個就要小心不要參雜了另一個。
而 FP 跟 OOP 重要的是觀念跟思想,他們主要做的是告訴我們應該如何撰寫程式,但這並沒有任何強制的規定,不是說我們一定要如何寫程式才能夠稱為 OOP 或 FP;而是我們要能夠用 OOP 或 FP 的思想來思考如何撰寫程式碼才能讓程式碼更好。
舉個例子 像是 FP 所說的 pure function。
平常我們在寫 function 的時候就可以想,要如何盡可能的不要有 side-effect,運算結果不要受外部變數影響,不要修改傳入變數的值。
利用這些想法來組織我們的程式碼,但不是說每個 function 你都要嚴格遵守,像是 side-effect 就是一定會存在在某些 function 裡頭,但你可以試試把具有 side-effect 的 function 跟其他 function 分開。
永遠記得 他們的存在只是讓你在撰寫程式時,能夠利用他們的想法來寫出更好的程式碼。
至於 Observable 如果覺得沒有很懂,可以把前面的文章再看一下,但最重要的是動手寫寫看,完成幾個小需求,然後去睡覺過幾天就會理解了。 (去睡覺不是我亂說的,是認知心理學講的,人類大腦中有很多神經元,理解一個知識的過程其實就是神經元長出新的觸手,而長出新的觸手是需要時間跟休息的)
如果還是有困惑歡迎留言給我喔!
哇感謝超棒的回覆,有比較理解了也解惑了
超用心希望你得獎哈哈哈
阿然後關於這篇的問題:
selector callback,這個 callback 會傳入四個參數
1,2了解了 想問3,4所指的index 是什麼意思 ?
index 是指 送出的第幾個元素 從 0 開始
跟陣列是一樣的概念
ok
記得 mergeMap 可以並行處理多個 observable,以這個例子來說當我們快速點按兩下,元素發送的時間點是有機會重疊的,這個部份的細節大家可以看上一篇文章 merge 的部分。
另外我們也可以把 switchMap 用在發送 HTTP request
這邊應該是 「另外我們也可以把 mergeMap 用在發送 HTTP request」
感謝
被發現用 copy&paste 了 XD
本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-18/