Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ModernWeb 2018 - 輕鬆應付複雜的非同步操作:RxJS + Redux Obse...

Huli
January 29, 2023

ModernWeb 2018 - 輕鬆應付複雜的非同步操作:RxJS + Redux Observable

Reactive Programming 近幾年在處理非同步事件上成為顯學,無論是 JavaScript、Java 或是 Swift,都能看到它的蹤影。所以演說的第一部份會介紹 RxJS 的基本概念跟常用的 operator,藉由幾個小範例讓大家看見 RxJS 在處理非同步上的厲害之處。

而 React 作為一套 UI library,在處理 API call 時往往需要依靠 Redux 來做狀態的管理,而搭配的解決方案又有好多種,像是 redux-thunk 或 redux-saga 等等。這次要介紹的 redux-observable 是一套利用 RxJS 來處理非同步 Action 的解決方案,因此第二部分會講到 redux-observable 的基本使用以及核心概念,最後講到如何用 RxJS 處理複雜的 API call。

Huli

January 29, 2023
Tweet

More Decks by Huli

Other Decks in Programming

Transcript

  1. 胡⽴立 Hu Li • 東南亞新創公司 Eatigo Frontend Team Lead •

    TechBridge Weekly 技術週刊編輯 
 致⼒力於淺顯易懂的程式教學,搞不好你看過我的⽂文章 • [⼼心得] ⼗十年程式⾃自學之路 • 零基礎的⼩小明要如何成為前端⼯工程師? • 跟著⼩小明⼀一起搞懂技術名詞:MVC、SPA 與 SSR • 資料庫的好夥伴:Redis • 讓我們來談談 CSRF • 希望是最淺顯易懂的 RxJS 教學 [email protected]
  2. 開始之前 4⽉月底 RxJS 5 => RxJS 6
 6⽉月底 redux-observable v0.x

    => v1.0.0 由於概念不變,簡報中舉的例⼦子都是
 RxJS5 + redux-observable v0.x
  3. const solution = arr => {
 let ans = 0


    for(let i=0; i<arr.length; i++){
 if (i%2) {
 ans += i*i
 }
 }
 return ans
 }
  4. const solution = arr => {
 return arr
 .filter(val =>

    val % 2)
 .map(val => val * val)
 .reduce(
 (total, val) => total + val
 )
 }
  5. const solution = arr => {
 let ans = 0


    for(let i=0; i<arr.length; i++){
 if (i%2) {
 ans += i*i
 }
 }
 return ans
 } const solution = arr => {
 return arr
 .filter(val => val % 2)
 .map(val => val * val)
 .reduce(
 (total, val) => total + val
 )
 }
  6. const solution = arr => {
 let ans = 0


    for(let i=0; i<arr.length; i++){
 if (i%2) {
 ans += i*i
 }
 }
 return ans
 } const solution = arr => {
 return arr
 .filter(val => val % 2)
 .map(val => val * val)
 .reduce(
 (total, val) => total + val
 )
 } Imperative 命令式 什麼東⻄西都⾃自⼰己來,⼼心 hen 累
  7. const solution = arr => {
 let ans = 0


    for(let i=0; i<arr.length; i++){
 if (i%2) {
 ans += i*i
 }
 }
 return ans
 } const solution = arr => {
 return arr
 .filter(val => val % 2)
 .map(val => val * val)
 .reduce(
 (total, val) => total + val
 )
 } Imperative 命令式 什麼東⻄西都⾃自⼰己來,⼼心 hen 累 Declarative 聲明式 告訴電腦:我想要什麼
  8. const solution = (orders, members) => {
 const ans =

    []
 for(let i=0; i<orders.length; i++){
 if (orders[i].amount > 100 && orders[i].id > 10) {
 ans.push({
 order: orders[i],
 member: members[orders.memberId]
 })
 }
 }
 return ans
 } Imperative 命令式 什麼東⻄西都⾃自⼰己來,⼼心 hen 累
  9. const solution = (orders, members) => {
 const ans =

    []
 for(let i=0; i<orders.length; i++){
 if (orders[i].amount > 100 && orders[i].id > 10) {
 ans.push({
 order: orders[i],
 member: members[orders.memberId]
 })
 }
 }
 return ans
 } const solution = (orders, members) => {
 return orders
 .filter(o => o.amount > 100)
 .filter(o => o.id > 10)
 .map(o => ({
 order: o,
 member: members[o.memberId] 
 }))
 } Imperative 命令式 什麼東⻄西都⾃自⼰己來,⼼心 hen 累 Declarative 聲明式 告訴電腦:我想要什麼
  10. const solution = (orders, members) => {
 const ans =

    []
 for(let i=0; i<orders.length; i++){
 if (orders[i].amount > 100 && orders[i].id > 10) {
 ans.push({
 order: orders[i],
 member: members[orders.memberId]
 })
 }
 }
 return ans
 } SELECT * FROM orders
 WHERE orders.amount > 100 AND orders.id > 10
 JOIN members ON members.id = orders.memberId Imperative 命令式 什麼東⻄西都⾃自⼰己來,⼼心 hen 累 Declarative 聲明式 告訴電腦:我想要什麼
  11. 1 2 3 4 5 6 filter (取奇數) 1 3

    5 map (平⽅方) 1 9 25
  12. 1 2 3 4 5 6 filter (取奇數) 1 3

    5 map (平⽅方) 1 9 25 reduce (加總)
  13. 1 2 3 4 5 6 filter (取奇數) 1 3

    5 map (平⽅方) 1 9 25 reduce (加總) 35
  14. Rx.Observable.fromEvent(window, 'click')
 .map(e => 1)
 .scan((total, val) => total +

    val)
 .subscribe(value => { console.log(‘click!’, value) }
 )
  15. [1, 2, 3] 跟 [4, 5, 6]
 
 串接:[1, 2,

    3, 4, 5, 6]
 相加:[5, 7, 9]
  16. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 merge +1 -1
  17. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 merge +1 +1 -1
  18. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 merge +1 +1 -1
  19. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 merge +1 +1 +1 -1
  20. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 -1 merge +1 +1 +1 -1
  21. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 -1 merge +1 +1 -1 +1 -1
  22. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 +1 -1 merge +1 +1 -1 +1 -1
  23. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 +1 -1 merge +1 +1 +1 -1 +1 -1
  24. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 +1 -1 -1 merge +1 +1 +1 -1 +1 -1
  25. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) +1 +1 +1 -1 -1 merge +1 +1 +1 -1 -1 +1 -1
  26. Rx.Observable.fromEvent(plus, 'click')
 .mapTo(1)
 .merge(
 Rx.Observable.fromEvent(minus, 'click')
 .mapTo(-1)
 )
 .scan((total, val)

    => total + val)
 .subscribe(value => { console.log(‘click!’, value) }
 ) var total = 0 plus.addEventListener('click', () => {
 total++
 console.log(‘click!’, total)
 })
 
 minus.addEventListener('click', () => {
 total—
 console.log(‘click!’, total)
 })
  27. function getData() {
 return fetch('http://example.com/api')
 .then(res => res.json()) 
 }


    
 Rx.Observable.fromPromise(getData())
 .subscribe(response => { console.log(response) }
 )
  28. ⾮非同步的問題 假設 A 的 response 花了 3 秒
 B 的

    response 只花了 1 秒 mergeMap:1 秒後顯⽰示 B,3 秒後顯⽰示 A
  29. ⾮非同步的問題 假設 A 的 response 花了 3 秒
 B 的

    response 只花了 1 秒 mergeMap:1 秒後顯⽰示 B,3 秒後顯⽰示 A
 concatMap:3 秒後顯⽰示 A,4 秒後顯⽰示 B

  30. ⾮非同步的問題 假設 A 的 response 花了 3 秒
 B 的

    response 只花了 1 秒 mergeMap:1 秒後顯⽰示 B,3 秒後顯⽰示 A
 concatMap:3 秒後顯⽰示 A,4 秒後顯⽰示 B
 switchMap:1 秒後顯⽰示 B
  31. ⾮非同步的問題 假設 A 的 response 花了 3 秒
 B 的

    response 只花了 1 秒 mergeMap:1 秒後顯⽰示 B,3 秒後顯⽰示 A
 concatMap:3 秒後顯⽰示 A,4 秒後顯⽰示 B
 switchMap:1 秒後顯⽰示 B 很常⽤用的原因:只要管最新的 request
  32. const getUserEpic = action$ => action$.ofType(actionTypes.GET_USER)
 .switchMap(action => 
 Rx.Observable.fromPromise(API.getUser(


    action.payload.id
 ))
 .map(user => setUser(user))
 .catch(err => Observable.of(getUserError(err)))
 )
 Epic
  33. const getUserEpic = action$ => action$.ofType(actionTypes.GET_USER)
 .switchMap(action => 
 Rx.Observable.fromPromise(API.getUser(


    action.payload.id
 ))
 .map(user => setUser(user))
 .catch(err => Observable.of(getUserError(err)))
 )
 Action in, Action out
  34. dispatch ⼀一個 promise,middleware 幫你執⾏行 redux-promise const getUser = id =>

    ({
 type:'GET_USER',
 payload:API.getUser(id)
 }) middleware redux-promise GET_USER_PENDING
 GET_USER_FULFILLED
 GET_USER_REJECTED API Server
  35. dispatch ⼀一個 function,middleware 幫你執⾏行 redux-thunk const getUser = id =>

    { return dispatch => { return API.getUser(id).then( user => dispatch(setUser(user)) ).catch( err => dispatch(getUserError(err)) ) } }
  36. 監聽指定的 action,並且丟給 worker 處理 watcher function* watcherSaga() { yield takeLatest("GET_USER",

    fetchUser) } takeLatest:只拿最後⼀一個(switchMap)
 takeEvery:每個都拿(mergeMap)
  37. call API 並且 dispatch 結果 worker function* fetchUser(action) { try

    { const user = yield call(
 API.getUser, action.payload.id
 ) yield put(setUser(user)) } catch (err) { yield put(getUserError(err)) } }
  38. 簡單⽐比較 promise thunk saga observable 學習難度 ☆ ☆☆ ☆☆☆☆☆ ☆☆☆☆☆

    Action 型態 Promise Function Object Object ⾮非同步
 流程管理 X X O O ⽅方便測試 X X O O
  39. 情境⼀一:巢狀 API /api/posts/:id
 /api/posts/:id/translations const epic = action$ => action$.ofType(actionTypes.GET_POST)


    .switchMap(action =>
 Observable.forkJoin( Observable.from(API.getPost(id)),
 Observable.from(API.getTrans(id))
 )
 .map(data => Actions.setPost({
 post: data[0],
 translations: data[1]
 })
 )
 )
  40. 情境⼀一:巢狀 API /api/posts/:id
 /api/posts/:id/translations const epic = action$ => action$.ofType(actionTypes.GET_POST)


    .switchMap(action =>
 Observable.forkJoin( Observable.from(API.getPost(id)),
 Observable.from(API.getTrans(id))
 )
 .map(data => Actions.setPost({
 post: data[0],
 translations: data[1]
 })
 )
 )
  41. 情境⼆二:call 兩個 API /api/posts/:id
 /api/users/:id action$.ofType(GET_POST).switchMap(action => Observable.from(API.getPost(action.id))
 .mergeMap(post =>

    Observable.from(API.getUser(post.authorId))
 .map(user => Actions.setPost({
 post,
 user
 })
 )
 ) 
 )
  42. 情境⼆二:call 兩個 API /api/posts/:id
 /api/users/:id action$.ofType(GET_POST).switchMap(action => Observable.from(API.getPost(action.id))
 .mergeMap(post =>

    Observable.from(API.getUser(post.authorId))
 .map(user => Actions.setPost({
 post,
 user
 })
 )
 ) 
 )
  43. 情境三:call ⼀一⼤大堆 API action$.ofType(GET_POSTS).switchMap(action => Observable.from(API.getPosts())
 .switchMap(posts =>
 Observable.of(...posts)
 .mergeMap(item

    =>
 Observable.from(API.getUser(item.authorId))
 .map(user => ({ ...item,
 user
 }))
 , 10)
 )
 )
 .toArray()
 .map(posts => Actions.setPost(posts)
 )
  44. 情境三:call ⼀一⼤大堆 API action$.ofType(GET_POSTS).switchMap(action => Observable.from(API.getPosts())
 .switchMap(posts =>
 Observable.of(...posts)
 .mergeMap(item

    =>
 Observable.from(API.getUser(item.authorId))
 .map(user => ({ ...item,
 user
 }))
 , 10)
 )
 )
 .toArray()
 .map(posts => Actions.setPost(posts)
 )
  45. 情境三:call ⼀一⼤大堆 API action$.ofType(GET_POSTS).switchMap(action => Observable.from(API.getPosts())
 .switchMap(posts =>
 Observable.of(...posts)
 .mergeMap(item

    =>
 Observable.from(API.getUser(item.authorId))
 .map(user => ({ ...item,
 user
 }))
 , 10)
 )
 )
 .toArray()
 .map(posts => Actions.setPost(posts)
 )
  46. 情境三:call ⼀一⼤大堆 API action$.ofType(GET_POSTS).switchMap(action => Observable.from(API.getPosts())
 .switchMap(posts =>
 Observable.of(...posts)
 .mergeMap(item

    =>
 Observable.from(API.getUser(item.authorId))
 .map(user => ({ ...item,
 user
 }))
 , 10)
 )
 )
 .toArray()
 .map(posts => Actions.setPost(posts)
 )
  47. 情境三:call ⼀一⼤大堆 API action$.ofType(GET_POSTS).switchMap(action => Observable.from(API.getPosts())
 .switchMap(posts =>
 Observable.of(...posts)
 .mergeMap(item

    =>
 Observable.from(API.getUser(item.authorId))
 .map(user => ({ ...item,
 user
 }))
 , 10)
 )
 )
 .toArray()
 .map(posts => Actions.setPost(posts)
 )