今天我们用实际的例子来练习各种 RxJS operators 的组合运用!在一般的应用程序里面,资料查询应该算是非常常见的情境了,我们就实际使用资料查询的功能做范例,加上各种变化,来练习一些 operators 的实际使用吧!
由於我们主要的练习目标是 RxJS,因此许多关於画面操作的部分,都已经先设计好了,可以到以下网址以起始专案开始练习:
https://stackblitz.com/edit/mastering-rxjs-practice-search-starter
如果想要跟着练习,建议可以 fork 一份到自己的帐号下,以後可以随时回来看成果。
练习过程中,也可以参考完整版程序码:
https://stackblitz.com/edit/mastering-rxjs-practice-search-finished
在这个专案中,我们要练习使用 GitHub Search API 来依照指定的名称搜寻 repositories,预期完成画面如下:
包含一些基本功能:
另外还包含了搜寻时的「自动完成」功能:
当输入文字时,可以提供符合文字的 repositories 建议。
在起始专案的 index.html
内已经把基本的 HTML 都写好了,另外在 index.ts
内也预先 import 已经写好的操作逻辑
// 画面上的 DOM 物件操作程序
import * as domUtils from './dom-utils';
// 存取 API 资料的程序码
import * as dataUtils from './data-utils';
dom-utils.ts
内都是操作画面的逻辑,但这不是主要练习的部分,现在前端 SPA 框架的盛行,不同框架会有不同的画面操作方式,单纯操作 DOM 物件其实是脏活,所以大概知道有些什麽功能就好罗:
fillAutoSuggestions
:显示自动完成的建议内容fillSearchResult
:显示搜寻结果loading
:当开始搜寻资料时,呼叫此方法将画面遮罩,避免多余的操作loaded
:当完成搜寻後,呼叫次方法隐藏画面遮罩,以便进行其他操作updatePageNumber
:更新页码的画面资讯updateStarsSort
:更新依照 stars 排序的画面资讯updateForksSort
:更新依照 forks 排序的画面资讯而在 data-utils.ts
内,则是 GitHub API 的呼叫,这里使用 RxJS 内提供的 ajax
来抓取呼叫 API 的内容,并使用 map
operator 来将需要的内容抓出来,例如「取得建议清单」的程序码如下:
const baseUrl = `https://api.github.com/search/repositories`
export const getSuggestions = (keyword: string) => {
const searchUrl = `${baseUrl}?q=${keyword}&per_page=10&page=1`;
return ajax(searchUrl).pipe(
map(response => response.response.items),
map(repositories =>
repositories.map(repository => repository.full_name))
);
};
上述程序码的第 7 行使用的是 RxJS 内的 map
operator,而第 8 行使用的是 JavaScript 内建阵列的 map
,有些时候在阅读上会不小心造成误解,这时候可以把里面的实作逻辑也抽出来,会变得更好阅读:
// 将阵列的处理抽成 function
const toSuggestionList = (repositories) => {
return repositories.map(repository => repository.full_name)
};
export const getSuggestions = (keyword: string) => {
const searchUrl = `${baseUrl}?q=${keyword}&per_page=10&page=1`;
return ajax(searchUrl).pipe(
map(response => response.response.items),
map(toSuggestionList)
);
};
此时原来程序就可以阅读成「将回传结果转换成建议清单 (map to suggestion list)」,在实作程序码比较复杂,或是阅读上比较不好理解时,适当的将程序码抽成好读好理解的 function 对於未来开发和维护都会有加分效果喔!
data-utils.ts
内还有一个 getSearchResult
方法,是用来取得搜寻结果的,实作基本上大同小异,只是带入更多的参数而已,就不浪费篇幅在这说明了,有兴趣的话可以看一下罗。
接着我们来实际做出自动完成的功能,自动完成要做的事情非常简单,就是在打字时,呼叫 dataUtils.getSuggestions()
方法来取得要显示的清单,并且呼叫 domUtils.fillAutoSuggestions()
即可。
我们可以使用 fromEvent
来监听输入框的 input
事件:
const keyword$ = fromEvent(document.querySelector('#keyword'), 'input');
此时订阅的话回得到 input
相关的事件,然而我们实际上需要关注的是输入的内容,因此可以使用 map
来进行转换:
const keyword$ = fromEvent(document.querySelector('#keyword'), 'input').pipe(
map(event => (event.target as HTMLInputElement).value)
);
这边使用了 event.target
来取得事件来源的 DOM 物件,由於是一个 input
输入框,因此可以使用 .value
来取得相关的值,as HTMLInputElement
是 TypeScript 的型别转换,让我们可以明确的知道 input
输入框有哪些属性可用。
此时 keyword$
就是一个「关键字内容的资料流」,我们可以再将此资料流搭配各种其他的 operators 来产生出不同的变化。
现在已经可以拿到输入的关键字了,接下来要把关键字带入 domUtils.getSuggestions()
方法来查询,最简单的方法如下:
keyword$.subscribe(keyword => {
domUtils.getSuggestions(keyword).subscribe(suggestions => {
domUtils.fillAutoSuggestions(suggestions);
});
});
不用多说,这种巢状的 subscribe
是应该尽力避免的,因此我们改用 switchMap
operator 来协助我们「将某个事件值换成另一个 Observable」:
keyword$
.pipe(
switchMap(keyword => dataUtils.getSuggestions(keyword))
)
.subscribe(suggestions => {
domUtils.fillAutoSuggestions(suggestions);
});
使用 switchMap
可以在来源资料变更时,退订上一次的 Observable 订阅,因此永远会以最新的来源资料及转换後的 Observable 为主,如此可以确保我们拿到的资料一定是使用最新的 keyword
关键字查询的结果。
这边说明一下为何不使用其他 xxxMap
系列的 operators:
concatMap
:虽然会拿到最後的资料,但因为不会退订上一次 Observable 订阅的关系,需要等之前 keyword 变更的查询都结束,会花费比较多时间。mergeMap
:每次 keyword 变更都会立刻查询,不退订之前的 Observable,加上 API 呼叫是非同步的关系,我们没办法确保最後一次会来的结果一定就是最新的查询结果,因此查询结果会不稳定。exhaustMap
:在之前 Observable 查询完成之前,有任何新的事件都会被忽略掉,因此只要事件更新前转换的 Observable 没结束,就不会拿到新事件的查询结果。如果不明白这几个 operators 的差别,请先看过之前写的文章:
[RxJS] 转换类型 Operators (2) - switchMap / concatMap / mergeMap / exhaustMap
此时已经可以在每次输入关键字就进行查询罗,但还有很多需要加强的地方!
GitHub API 在未验证时有限制每分钟只能进行 10 次查询(有验证时限制每分钟 30 次查询),这是为了保护服务器被大量搜寻的机制(称为 Rate Limits),在实际应用上,我们也应该要尽可能避免使用者产生大量的查询,以免前端的疏失操成後端的效能低落,因此在这里我们可以再加上一个 debounceTime
operator,来避免一有新事件就查询的问题:
keyword$
.pipe(
// 避免一有新事件就查询
debounceTime(700),
switchMap(keyword => dataUtils.getSuggestions(keyword)),
)
.subscribe(suggestions => {
domUtils.fillAutoSuggestions(suggestions);
});
debounceTime
可以在指定的时间内都没有新事件发生後,才以最後一次的事件值进入下一个步骤,透过这种方式,就可以避免一直打字一直查询的问题罗。
想像一下,假设目前输入的内容是「rxjs」,并且已经完成一次建议清单的查询,接着我们继续输入变成「rxjsdemo」,但想了一下又删掉变回「rxjs」,然後 debounceTime
的控制时间才过去,新的事件资料跟上一次的事件资料一样都是「rxjs」,如果资料其实并没有改变,还需要再进行一次查询吗?这时候当然就可以省略不查询了,因此可以使用 distinctUntilChanged
operator,只有当新的事件值与上一次的事件值不同时,才会继续让事件发生:
keyword$
.pipe(
debounceTime(700),
// 避免重复的查询
disintctUntilChanged(),
switchMap(keyword => dataUtils.getSuggestions(keyword)),
)
.subscribe(suggestions => {
domUtils.fillAutoSuggestions(suggestions);
});
如果要避免资料重复,为何不使用 distinct
呢?以这边的例子来说,当我把资料变更成「rxjsdemo」时,如果有产生新的查询,下一次变更回「rxjs」时,由於 distinct
的设计是「整个资料流的事件值不会重复」,因此「rxjs」事件值在整个资料流过程已经发生过了会被忽略掉,导致没有正确查询,画面显示错误的资料。
而使用 distinctUntilChanged
,则只有跟「上一次」事件值相同才会忽略,因此可以避免掉 distinct
的问题。
distinct
和 distinctUntilChanged
的差别,可以参考之前写的文章:
[RxJS] 过滤类型 Operators (4) - distinct / distinctUntilChanged / distinctUntilKeyChanged
最後来微调一下,如果不管资料长度都会进行搜寻,就会导致一开始只输入一个「r」这样的资料时就会开始进行搜寻,而得到比较不精准的内容,因此可以再做个调整,让查询内容大於某个长度时才进行搜寻,例如至少要 3 个字,此时使用最基本的 filter
operator 就可以了:
keyword$
.pipe(
debounceTime(700),
disintctUntilChanged(),
// 避免内容太少查出不精准的结果
filter(keyword => keyword.length >= 3),
switchMap(keyword => dataUtils.getSuggestions(keyword)),
)
.subscribe(suggestions => {
domUtils.fillAutoSuggestions(suggestions);
});
透过这些 operators 的控制,就能设计出兼顾效能及准度的自动建议功能啦。
像这样的程序码,可以想想看没有各种 operators 加持的情况下,要判断多少状态、条件,而有了 RxJS 及各种 operators,真的可以帮助我们大幅减少许多程序码的撰写!
关键字的建议及自动完成功能搞定了,接着先来单纯的针对关键字进行搜寻,这里我们希望按下「search」按钮时,才针对我们要的关键字进行查询:
一样的,我们可以使用 fromEvent
来将按钮事件包装成 Observable
const search$ = fromEvent(document.querySelector('#search'), 'click');
接着我们就需要在事件发生时,依照输入的关键字进行搜寻。
我们一样可以透过 switchMap
operators 来将按钮事件转换成查询资料的 Observable:
search$.pipe(
switchMap(() => dataUtils.getSearchResult(
(document.querySelector('#keyword') as HTMLInputElement).value)
)
).subscribe(result => {
domUtils.fillSearchResult(result);
});
第 2 行程序使用 switchMap
将事件转换成查询的 Observable,里面的参数在第 3 行直接取得输入框 DOM 物件资料。
在关键字内容部分,我们已经有一个 keyword$
的资料流了,不多加运用实在很可惜:
const searchByKeyword$ = search$.pipe(
switchMap(() => keyword$),
switchMap(keyword => dataUtils.getSearchResult(keyword))
);
searchByKeyword$.pipe(
switchMap(keyword => dataUtils.getSearchResult(keyword))
).subscribe(result => {
domUtils.fillSearchResult(result);
});
我们建立了一个 searchByKeyword$
的 Observable,它会:
keyword$
Observablekeyword$
Observable 有新事件时,转换查询用的 Observable但这样会有两个问题:
keyword$
後,还没有新的事件发生,因此不会进行查询,需要等 keyword$
有新事件才会查询keyword$
资料流不会结束(因为随时可能输入新的文字),因此搜寻按钮事件後,每次 keyword$
有新事件都会变成查询,这不是我们要的,我们希望拿到最新的关键字後直接进行查询,然後结束。接着我们来解决这两个问题
由於转换成 keyword$
後,需要等待新事件才会查询,我们希望能立刻得到最後一次事件资料,因此我们可以使用 shareReplay()
将 keyword$
转换成 Hot Observable 并将最後 N 次事件资料共享出来:
const keyword$ = fromEvent(document.querySelector('#keyword'), 'input').pipe(
map(event => (event.target as HTMLInputElement).value),
// 共享最後一次事件资料
shareReplay(1)
);
在这里我们建立了 keywordForSearch$
的 Observable,专门给 search$
转换使用,keywordForSearch$
内使用 shareReplay(1)
确保每次订阅时会先拿到事件的最後一笔资料。
为何不使用 share()
呢?由於 share()
实际上是建立一个 Subject
,而 Subject
的特性是订阅後需要等 Subject
有新事件才会得到资料,因此使用 shareReplay()
来建立 ReplaySubject
,ReplaySubject
会依照设定纪录最近 N 次事件资料。
关於 share()
和 shareReplay()
背後默默做的事情及差异,可以参考之前的文章:
[RxJS] Multicast 类 Operator (1) - multicast / publish / refCount / share / shareReplay
确定可以拿到 keyword$
最後一次事件资料後,接着来处理事件不会结束的问题,这个问题很好解决,使用 take
就好,take
operator 可以在订阅後取得前 N 次事件资料,然後结束资料流,因此只需要设定成 1,就可以在拿到第一次资料後结束。
const keywordForSearch$ = keyword$.pipe(
// 取得一次资料流事件後结束
take(1)
);
const searchByKeyword$ = search$.pipe(switchMap(() => keywordForSearch$));
整体流程就会变成:
search$
发生新事件,转换成 keywordForSearch$
ObservablekeywordForSearch$
Observable 使用 keyword$
作为来源,并且有 shareReplay(1)
的关系,会立刻得到最近一次 keyword$
的事件资料take(1)
,因此在取得最近一次事件资料的同时,结束 Observable最後,还有一个问题:当搜寻按钮按下时,如果 keyword$
还没有事件资料,那麽转换後的 keywordForSearch$
就不会立刻结束,此时会变成按下按钮後,再变更搜寻文字才会进行查询,这是不合理的,因此我们要给 keyword$
资料流一个初始资料:
const keyword$ = fromEvent(document.querySelector('#keyword'), 'input').pipe(
map(event => (event.target as HTMLInputElement).value),
// 让资料流有初始值
startWith(''),
// 共享最後一次事件资料
shareReplay(1)
);
接着我们必须避免空字串的查询,因此加上 filter
不让空字串的事件发生:
const searchByKeyword$ = search$.pipe(
switchMap(() => keywordForSearch$),
// 排除空字串查询
filter(keyword => !!keyword)
);
searchByKeyword$.pipe(
switchMap(keyword => dataUtils.getSearchResult(keyword))
).subscribe(...);
如此一来,就将「关键字变更的资料流 (keyword$
) 」和「按钮事件的资料流 (search$
)」整合成「依照关键字进行搜寻(searchByKeyword$
)」的资料流罗。
最後我们将分页与排序都整合进搜寻功能,整个画面就会很完整啦!
之前已经有 searchByKeyword$
依照关键字查询的 Observable 了,接着我们要处理排序、分页等条件的 Observables。
在排序部分,我们希望预设能以 stars 数量降幂排序,也就是预设 stars 越多的越前面,同时能针对 stars 和 forks 进行升幂/降幂排序,stars 和 forks 是两个不同的栏位,但排序是「一个资讯」,因此我们可以使用 Subject
类别,并分别订阅两个栏位的事件,来改变 Subject
资讯。
我们需要有一个预设的排序条件,同时在改变排序时也会需要这个排序资讯来决定下一次的排序方式,因此可以使用 BehaviorSubject
。
// 建立 BehaviorSubject,预设使用 stars 进行降幂排序
const sortBy$ = new BehaviorSubject({ sort: 'stars', order: 'desc' });
接着订阅画面上 stars 和 forks 的点击事件,来改变这个 sortBy$
的事件值:
const sortBy$ = new BehaviorSubject({ sort: 'stars', order: 'desc' });
const changeSort = (sortField: string) => {
if (sortField === sortBy$.value.sort) {
sortBy$.next({
sort: sortField,
order: sortBy$.value.order === 'asc' ? 'desc' : 'asc'
});
} else {
sortBy$.next({
sort: sortField,
order: 'desc'
});
}
};
fromEvent(document.querySelector('#sort-stars'), 'click').subscribe(() => {
changeSort('stars');
});
fromEvent(document.querySelector('#sort-forks'), 'click').subscribe(() => {
changeSort('forks');
});
在 changeSort
方法里面,我们可以使用 sortBy$.value
得到 BehaviorSubject
最近的事件值,并依此判断接下来排序的规则。
接着我们先来处理「每页显示几笔」的下拉选单,我们可以很容易的使用 fromEvent
来将 select
的事件资料进行转换,这部分跟 keyword$
非常类似,差别只在处理的来源和事件不同而已:
const perPage$ = fromEvent(document.querySelector('#per-page'), 'change').pipe(
map(event => +(event.target as HTMLSelectElement).value)
);
最後是切换页码,实际上是两个按钮分别代表「上一页」和「下一页」,页数分别会「减 1」和「加 1」,因此我们可以分别把按钮事件变成 1 和 -1,以便用来计算下一页的页码:
const previousPage$ = fromEvent(
document.querySelector('#previous-page'),
'click'
).pipe(
mapTo(-1)
);
const nextPage$ = fromEvent(
document.querySelector('#next-page'),
'click'
).pipe(
mapTo(1)
);
因为转换的是一个固定不动的常数,因此可以直接使用 mapTo
operator 进行转换。
接着我们可以把这两个 Observables 使用 merge
组合成一个 Observable,并使用 scan
来变更页码资讯:
const page$ = merge(previousPage$, nextPage$).pipe(
scan((currentPageIndex, value) => {
const nextPage = currentPageIndex + value;
return nextPage < 1 ? 1 : nextPage;
}, 1)
);
透过这种方式,也可以轻松完成「下 5 页」、「下 10 页」的功能罗。
所有查询相关的资料来源都准备完毕後,最後我们只需要把这些条件组合在一起就可以了,我们需要每个事件最後一次的资讯,因此可以使用 combinteLatest
来组合每个资料流最後的事件值,再将这些资料丢给 dataUtils.getSearchResult()
查询:
// 组合搜寻条件
const startSearch$ = combineLatest([
searchByKeyword$,
sortBy$,
page$,
perPage$
]);
// 将搜寻条件转换成查询 Observable
const searchResult$ = startSearch$.pipe(
switchMap(([keyword, sort, page, perPage]) =>
dataUtils.getSearchResult(keyword, sort.sort, sort.order, page, perPage)
)
);
searchResult$.subscribe(result => {
domUtils.fillSearchResult(result);
});
由於 combineLatest
是将里面 Observables 的「最後一次事件」组合起来,因此若某个 Observable 还没发生过事件,整个 combineLatest
组合的 Observable 都还不会有事件值,此时 page$
和 perPage$
都没有启始资料,因此就算按下搜寻,还不会有任何反应,所以最後针对 page$
和 perPage$
再使用 startWith
给予初始资料:
const startSearch$ = combineLatest([
searchByKeyword$,
sortBy$,
// 给予 page$ 初始资料
page$.pipe(startWith(1)),
// 给予 perPage$ 初始资料
perPage$.pipe(startWith(10))
]);
基本的查询、分页和排序功能就完成啦!!
最後让我们再来针对一些细节来做调整。
这部分很简单,订阅原来的事件,然後把事件资讯更新到画面上就好了:
page$.subscribe(page => {
domUtils.updatePageNumber(page);
});
sortBy$.pipe(filter(sort => sort.sort === 'stars')).subscribe(sort => {
domUtils.updateStarsSort(sort);
});
sortBy$.pipe(filter(sort => sort.sort === 'forks')).subscribe(sort => {
domUtils.updateForksSort(sort);
});
以上程序直接订阅 page$
,并使用 domUtils.updatePageNumber()
更新页码资讯,而排序资讯则依照排序类型有两个不同的方法呼叫,因此使用 filter
将不同的的排序栏位分成两个 Observable,并各自订阅然後呼叫各自对应的方法来更新排序栏位资讯。
接下来我们需要在查询资料时呼叫 domUtils.loading()
遮罩画面,并在查询结束时呼叫 domUtils.loaded()
隐藏遮罩,在查询条件变更时就需要遮罩住画面,因此订阅 startSearch$
Observable 即可:
startSearch$.subscribe(() => {
domUtils.loading();
});
接着在查询完毕後,除了画面更新外,也要将此遮罩隐藏:
searchResult$
.subscribe(result => {
domUtils.fillSearchResult(result);
domUtils.loaded();
});
当查询过程发生错误时,整条订阅的 Observable 会完全中断,这也代表如果中途产生无法处理的错误,会造成之後无法继续进行查询作业,为了避免这个问题,我们可以使用 catchError()
来拦截并处理错误:
searchResult$
.pipe(
// 处理搜寻事件的错误,以避免整个资料流从此中断
catchError(() => of([]))
)
.subscribe(result => {
domUtils.fillSearchResult(result);
domUtils.loaded();
});
乍看之下没什麽问题,当 searchResult$
发生错误时,拦截错误并给予空阵列,这样便可以确保 subscribe
的 next()
可以收到资料,但 catchError()
回传的 Observable 会让目前订阅的 Observable 剩下一个空阵列的资料然後结束,因此依然会让整个订阅结束,而导致无法继续查询。
这时候就要朝错误的源头下手,也就是 startSearch$.pipe(switchMap(...))
内的 Observable,在这里面进行错误处理,才不会让整个 Observable 订阅被结束,所以我们把查询的程序拉出来,并加上错误处理机制:
const getSearchResult = (
keyword: string,
sort: string,
order: string,
page: number,
perPage: number
) =>
dataUtils
.getSearchResult(keyword, sort, order, page, perPage)
.pipe(
// 从查询开始处理错误
catchError(() => of([]))
);
如此原来的 searchResult$
就不会有其他需要处理的错误,整个订阅就不会因此而结束。
上个阶段我们已经能处理错误了,但目前只是当错误发生时查不到资料而已,使用者感觉不出有错误发生,因此我们需要提示错误讯息,最简单的方式是在 catchError()
内进行提示:
const getSearchResult = (
keyword: string,
sort: string,
order: string,
page: number,
perPage: number
) =>
dataUtils
.getSearchResult(keyword, sort, order, page, perPage)
.pipe(
// 从查询开始处理错误
catchError((error) => {
alert(error.response.message);
return of([]);
})
);
这样依然会有 side effect 的问题,所以可以换个方式,把资料包装起来,当错误发生时,加上一个错误的 flag:
const getSearchResult = (
keyword: string,
sort: string,
order: string,
page: number,
perPage: number
) =>
dataUtils
.getSearchResult(keyword, sort, order, page, perPage)
.pipe(
// 正常收到资料时,将资料包装起来且 success 设成 true
map(result => ({ success: true, message: null, data: result })),
catchError((error) => {
// 发生错误时,将资料包装起来且 success 设成 false
// 同时传递错误资讯,让後续订阅可以处理提示
return of({
success: false,
message: error.response.message,
data: []
})
}));
这麽一来就可以保留整个流程,直到 subscribe
时在进行处理,原来订阅是直接拿 result
去更新,现在 result
变成包含是否成功资讯的物件,因此只要取其中的 data
来更新就好:
searchResult$.subscribe(result => {
// 原来的 result 改变了,因此取其中的 data 就好
// domUtils.fillSearchResult(result);
domUtils.fillSearchResult(result.data);
domUtils.loaded();
});
而处理错误的部分,我们可以拉出另外一条 Observable 来处理:
// 处理错误提示
searchResult$
.pipe(
filter(result => !result.success)
).subscribe(result => {
alert(result.message);
});
把「显示资料」和「错误处理」当作两个不同的资料来源处理,可以让我们在阅读程序时更加专注在原本的意图上。
这里最後需要注意的是,searchResult$
因为针对不同情境处理而被订阅了两次,而原来的 searchResult$
是 Cold Observable,且其中有 ajax
的呼叫,代表每次订阅都会重跑一次 ajax
,这麽一来 API 呼叫就会重复,造成不必要的浪费,所以最後再 searchResult$
补上 share()
:
const searchResult$ = startSearch$.pipe(
switchMap(([keyword, sort, page, perPage]) =>
getSearchResult(keyword, sort.sort, sort.order, page, perPage)
),
// searchResult$ 有多次订阅
// 因此使用 share 避免重复请求资料
share()
);
在前面 keyword$
示范时使用 shareReplay(1)
是因为订阅时机会随着按钮事件的 switchMap()
订阅时间而不同,且需要最後一次事件的资讯。
在这里使用 share()
则是因为订阅会立刻发生,且没有使用最後一次事件资讯需要的关系;当然,要在这里使用 shareReplay(1)
在逻辑上也是完全没问题的。
今天我们透过实际在网页程序上常常遇到的查询、分页和排序等功能,来说明实务上 RxJS 的应用,以及各种 operators 的组合技巧。
当所有事件都使用资料流及 Observable 的观点来思考时,整个思路会变得更加单纯,我们只需要习惯应用各种已经学会的 operators 来组合这些资料流即可,而且每个资料流、每个订阅都只处理自己该做的事情,让每段程序码都变得更简短好阅读,且发生问题时,也可以很容易从问题发生点循序找到每个执行过程,更容易找到错误发生的地方!
刚开始在学习 RxJS 时一定会对於何时该使用哪些 operators 的问题觉得不知所措,建议可以多加练习,把上述的程序码反覆写过,了解每个资料的流向,接着更可以尝试组合不同的 operators,用不同的写法达到一样的功能,久而久之习惯後,RxJS 功力就会大增罗!!
<<: JavaScript基本功修练:Day31 - 完赛了,然後呢?
>>: 强型闯入DenoLand[30] - Web API 实作篇
这个得上一篇:https://ithelp.ithome.com.tw/articles/10258...
Hello 大家, 上三天就放假了~~ 但还是觉得好累= = 周休二日真的不够, 至少每周可以在家上...
1. Action 与 Side Effect 昨天,我们确认了状态能被储存起来,然而我们这个开门,...
上一篇我们完成了资料库的建置,那麽我们现在就可以来处理如何应用啦! 开始实作 首先我们要先开启之前要...
今天我们来更深入的探讨Excel中,首先先来介绍「日期」这个东西,并且我相信你也有遇过再Excel中...