學習Rxjs是兩個月前的事了,但沒有用到在一個實際需求上。近日收到一個需求,正好是一個可以抽象為由資料流驅動的應用,於是我欣然用Rxjs結合React寫之。
需求說明
需求原始說明為:
60個英文單詞被分為三個組系,每個組系包括2-6個單詞一組的五組單詞。在測試中,測試單詞以1詞/秒的速率依次呈現在電腦螢幕上,被試大聲朗讀螢幕上的單詞,當一組單詞呈現結束時,螢幕上出現與該組數目相等的問號,提示被試用剛才呈現的單詞造句。 兩個一組時,螢幕上以1詞/秒的速率依次出現“price”,“week”,兩個單詞後會出現??,受試用price造句,造完句後點下滑鼠,螢幕上出現一個問號,受試用week造句,之後點滑鼠進入三個一組,螢幕上以1詞/秒的速率依次出現“bird”,“game”,“ star”,三個單詞後會出現???,受試用bird造句,造完句後點下滑鼠, 螢幕上出現兩個問號,受試用game造句,再點下滑鼠, 螢幕上出現一個問號,受試用star造句,之後點滑鼠進入四個一組…以此類推。
全程錄音,計算機記錄受試每個單詞造句所用時間。
需求原始說明可能有些不易看明,經過討論,我歸總如下:
-
資料格式為:(
word.組系序號.組單詞數.組內序號
)Test1 Test2 Test3 word.1.2.1 word.2.2.1 word.3.2.1 word.1.2.2 word.2.2.2 word.3.2.2 word.1.3.1 word.2.3.1 word.3.3.1 word.1.3.2 word.2.3.2 word.3.3.2 word.1.3.3 word.2.3.3 word.3.3.3 ... ... ... word.1.6.6 word.2.6.6 word.3.6.6 由此,抽象為以下資料型別:
// 代表一個組系 interface TestItem { name: string; // 組系名,如:Test1, Test2, Test3 /* 二維陣列表示的組系內資料項 [word.1.2.1, word.1.2.2] [word.1.3.1, word.1.3.2, word.1.3.3] ... */ wordGroups: string[][]; } 複製程式碼
並將資料放入全域性store:
class Store { // 組系 items: TestItem[] = []; // 每組最小單詞數 minWordsCount = 2; // 每組最大單詞數 maxWordsCount = 6; // 實驗開始前倒數計時秒數 countdown = 5; } 複製程式碼
-
流程:
-
倒數計時
-
對每個組系:
-
對每一組:
- 依次顯示單詞,每個單詞顯示一秒
- 對每個單詞:
- 顯示
此組單詞數 - i
個問號 - 如果
點選
事件發生,子流程結束
- 顯示
- 子流程結束
-
子流程結束
-
-
流程結束
-
讀者有意,可以試想一下普通做法如何實現。
資料流抽象
為了方便一些操作,我封裝了一個
immediateInterval
,因為interval不是即時開始發射資料的:
function immediateInterval( period?: number | undefined, scheduler?: SchedulerLike | undefined ): Observable<number> { return new Observable(subscriber => { subscriber.next(0); const interval$ = interval(period, scheduler); const next = subscriber.next; subscriber.next = function(this: Subscriber<number>, i: number) { next.bind(this)(i + 1); }.bind(subscriber); interval$.subscribe(subscriber); }); } 複製程式碼
由上節的流程,得到如下資料流:
-
倒數計時:使用
immediateInterval
發出store.countdown
個值,每個值攜帶當前倒數計時數,然後 -
迭代
item of store.items
:-
迭代
group of item.wordGroups
:-
顯示單詞:使用
immediateInterval
發出group.length
個值,每個值攜帶當前單詞,然後 -
迭代
for(let i = 0; i < group.length; i++)
:- 發出值,攜帶
group.length - i
(問號數) - 訂閱全域性
event bus
的click
事件,當click
激發時,continue
此迭代
- 發出值,攜帶
-
-
資料流實現
以下程式碼的一些設計點:
- 使用
event bus
的emit
實現流程鉤子,用來向外報告進行到了哪一步 - 使用
async await
語法實現非同步迭代:async () => { for(...){ await new Promise(async (resolve) => { someAsyncCode( // 結束此Promise,以continue該for迴圈 resolve(); ) }) } } 複製程式碼
// event bus
const bus = new EventEmitter();
const stream$ = new Observable<
| { type: "倒數計時"; count: number }
| {
type: "顯示單詞";
word: string;
}
| {
type: "顯示問號";
count: number;
}
>(subscriber => {
immediateInterval(1000)
.pipe(take(store.countdown + 1))
.subscribe(
i => {
if (i !== store.countdown)
subscriber.next({ type: "倒數計時", count: store.countdown - i });
},
console.error,
async () => {
bus.emit("start", { type: "full" }); // 全程開始
for (let item of store.items) {
await new Promise(async (resolve, reject) => {
bus.emit("start", { type: "item", item }); // 一組系開始
for (
let groupIndex = 0;
groupIndex < item.wordGroups.length;
groupIndex++
) {
const group = item.wordGroups[groupIndex];
// 一組開始
bus.emit("start", { type: "group", item, groupIndex }); // 一組開始
await new Promise(async (resolve, reject) => {
// 顯示單詞
immediateInterval(1000)
.pipe(take(group.length))
.subscribe(
i => {
subscriber.next({ type: "顯示單詞", word: group[i] });
},
console.error,
() => {
// 顯示問號
rxjs.timer(1000).subscribe(async () => {
for (
let i = group.length, wordIndex = 0;
i > 0;
i--, wordIndex++
) {
// 開始造句
bus.emit("start", {
type: "sentence",
index: wordIndex,
item,
groupIndex
});
await new Promise(async (resolve, reject) => {
subscriber.next({ type: "顯示問號", count: i });
const onClick = () => {
resolve(); // next 問號
bus.removeListener("click", onClick);
};
bus.addListener("click", onClick);
});
// 造句結束
bus.emit("end", {
type: "sentence",
index: wordIndex,
item,
groupIndex
});
}
resolve(); // next group
});
}
);
});
bus.emit("end", { type: "group", item, groupIndex }); // 一組結束
}
bus.emit("end", { type: "item", item }); // 一組繫結束
resolve(); // next test
});
}
bus.emit("end", { type: "full" }); // 全程結束
subscriber.complete();
}
);
});
複製程式碼
連線到React元件(Hooks):
const Play = () => {
const action = useObservable(() => stream$);
let comp = null;
if (!action) {
comp = null;
} else if (action.type === "倒數計時") {
comp = ...;
} else if (action.type === "顯示單詞") {
comp = ...;
} else if (action.type === "顯示問號") {
comp = ...;
}
return ...;
};
複製程式碼
總結
Rxjs使得一些事情更容易(:
涉及到的一些庫
rxjs
rxjs-hooks(rxjs與react hooks的結合庫)
wolfy87-eventemitter(瀏覽器端的高效的EventEmitter)