這篇要來稍微看一下 Effect 到底是如何實作的,同時我們會先來看到,如何在「背景」執行 Effect
Effect.fork
先來看今天的主角, Effect.fork
,Effect.fork
可以幫助你在背景執行 Effect
import { Effect, Console, pipe, Schedule } from "effect"
pipe(
Effect.gen(function * () {
yield * pipe(
Console.log("run in background"),
Effect.repeat({
schedule: Schedule.fixed('100 millis'),
times: 2
}),
Effect.fork
)
yield * pipe(
Console.log('run in foreground'),
Effect.repeat({
schedule: Schedule.fixed('200 millis'),
times: 2,
})
)
}),
Effect.runPromise
)
你應該會看到 background 跟 foreground 兩個輪流輸出,不過你可能會想,為什麼我們需要這邊複雜的範例來示範在背景執行的概念嗎?我們換看一個簡單的版本
import { Effect, Console, pipe } from "effect"
pipe(
Effect.gen(function * () {
yield * pipe(
Console.log("run in background"),
Effect.fork
)
yield * Console.log('run in foreground')
}),
Effect.runPromise
)
你可以實際執行看看,你會發現, background
的輸出不見了,這是為什麼呢?你可能會想到,奇怪, js 明明就是單執行緒的語言,同時間只能執行一段程式碼,怎麼可能真的有什麼背景執行這回事
如果你對瀏覽器或是 Node.js 熟的話,你可能會知道這兩個環境中都有 worker (Node.js 是 worker_threads) ,使用 worker 的話確實可以同時執行多個程式,但我們這邊沒有使用, Effect 也沒有幫你建立 worker ,因此確實是單執行緒的
結合前兩個範例和 JavaScript 單執行緒的特性,你是否意識到什麼?第一個範例能看到 background
輸出,是因為我們設定了重複和等待時間。而第二個範例,由於沒有等待,主執行流程在背景任務有機會執行前就已結束
還記得我們曾提過 Effect 程式碼就像是執行的藍圖嗎?它定義了如何處理平行、重試等邏輯,然後交由 Effect 運行。Effect 能夠做到這一點,是因為它在背後安排這些任務的執行順序
我們再來看一個範例,這次我們來等待背景的任務完成,才結束整個流程,要做到這點,就會需要使用到 Fiber
,我們會再下一篇再來詳細討論 Fiber
,現在我們先來看怎麼做
import { Effect, Console, Fiber, pipe } from "effect"
pipe(
Effect.gen(function * () {
const fiber = yield * pipe(
Console.log("run in background"),
Effect.fork
)
yield * Console.log('run in foreground')
// 等待背景的任務執行完
yield * Fiber.join(fiber)
}),
Effect.runPromise
)
這次背景的任務有執行完了
還記得在「14. dependency injection 與測試」中,我們有提到 TestClock
,但那時候提到我們目前還沒辦法實際拿它來測試有使用到 Effect.sleep
或是 Schedule
的程式嗎?現在我們可以做到了,因為我們可以透過 Effect.fork
讓我們想測試的程式在背景執行,讓我們的測試流程可以順利的完成測試,我們實際看個範例吧
這次的測試目標是一個會在 1 秒後呼叫 console 的 effect
const delayEffect = Effect.gen(function* () {
yield* Effect.sleep("1 second");
yield* Console.log("after 1s");
});
接下來我們實際用 TestClock
測試看看
it.effect("can test schedule", () =>
Effect.gen(function* () {
const log = vi.fn(() => Effect.void);
const fiber = yield* pipe(
Effect.fork(delayEffect),
Effect.withConsole({
log,
} as unknown as Console.Console)
);
// 一開始 log 沒有被呼叫過
expect(log).not.toBeCalled();
// 經過 500ms
yield* TestClock.adjust("500 millis");
// 還是沒呼叫
expect(log).not.toBeCalled();
// 再加 500ms ,這樣共前進了 1s
yield* TestClock.adjust("500 millis");
// 終於被呼叫了
expect(log).toBeCalled();
// 等待任務結束,這可以確保如果背景任務發生了錯誤可以被測試捕捉到
yield* Fiber.join(fiber);
})
);
像這樣,我們可以使用 TestClock
讓測試的時間前進,以測試我們的 Effect.sleep
造成的延遲
這次簡單的介紹了如何用 Effect.fork
在背景執行,下一篇要來比較進一步的看 Effect 是怎麼排程的