iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0

這篇要來稍微看一下 Effect 到底是如何實作的,同時我們會先來看到,如何在「背景」執行 Effect

Effect.fork

先來看今天的主角, Effect.forkEffect.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
)

(playground link)

你應該會看到 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
)

(playground link)

你可以實際執行看看,你會發現, 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 是怎麼排程的

Reference


上一篇
15. Effect 實戰分享 3: 資料遷移
下一篇
17. Effect 的 concurrency 調度器: Fiber 簡介
系列文
Effect 魔法:打造堅不可摧的應用程式22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言