任務

任務是用於在元件初始化或元件狀態變更時執行非同步操作。

注意:任務類似於 React 中的 useEffect(),但兩者之間有足夠的差異,我們不想將它們命名為相同的名稱,以免造成對其工作方式的先入為主的期望。主要差異在於:

  • 任務是非同步的。
  • 任務在伺服器和瀏覽器上執行。
  • 任務在渲染之前執行,並且可以阻止渲染。

useTask$() 應該是你執行同步或異步工作的預設 API,無論是在組件初始化時還是狀態改變時。只有當你無法使用 useTask$() 達成目標時,才應該考慮使用 useVisibleTask$()useResource$()

useTask$() 的基本用例是在組件初始化時執行工作。useTask$() 具有以下特性:

  • 它可以在伺服器端或瀏覽器端運行。
  • 它在渲染之前運行並會阻塞渲染。
  • 如果有多個任務正在運行,則它們會按照註冊的順序依次運行。異步任務會阻塞下一個任務的運行,直到它完成為止。

當組件狀態改變時,也可以使用任務來執行工作。在這種情況下,每次追蹤的狀態改變時,任務都會重新運行。請參閱:track()

有時,任務只需要在瀏覽器端運行,並且在渲染之後運行,在這種情況下,你應該使用 useVisibleTask$()

注意:如果你需要異步獲取數據並且不阻塞渲染,則應該使用 useResource$()。在資源解析過程中,useResource$() 不會阻塞渲染。

生命週期

可恢復性是指「懶惰執行」,它能夠在伺服器端構建「框架狀態」(組件邊界等),並讓它存在於客戶端,而無需重新執行框架。

應用程序環境(無論是客戶端還是伺服器端)是由用戶交互決定的。在伺服器端渲染中,應用程序最初會在伺服器端渲染。當用戶與應用程序交互時,它會在客戶端恢復,從伺服器留下的狀態繼續執行。這種方法通過根據交互利用兩種環境,確保了高效且響應迅速的用戶體驗。

注意:對於使用激活的系統,應用程序的執行會發生兩次。一次在伺服器端(SSR/SSG),一次在瀏覽器端(激活)。這就是為什麼許多框架都有「效果」,這些效果只在瀏覽器端執行。這意味著在伺服器端運行的代碼與在瀏覽器端運行的代碼不同。Qwik 執行是統一的,這意味著如果代碼已經在伺服器端執行過,則它不會在瀏覽器端重新執行。

在 Qwik 中,只有 3 個生命週期階段:

  • Task - 在渲染之前以及追蹤的狀態改變時運行。Tasks 按順序運行,並阻塞渲染。
  • Render - 在 TASK 之後、VisibleTask 之前運行。
  • VisibleTask - 在 Render 之後以及組件變得可見時運行。
      useTask$ -------> RENDER ---> useVisibleTask$
                            |
| --- SERVER or BROWSER --- | ----- BROWSER ----- |
                            |
                       pause|resume

伺服器端:通常,組件的生命週期始於伺服器端(在 SSR 或 SSG 期間),在這種情況下,useTask$RENDER 將在伺服器端運行,然後 VisibleTask 將在瀏覽器端運行,在組件可見之後。

請注意,由於組件是在伺服器端掛載的,因此只有 useVisibleTask$() 會在瀏覽器端運行。這是因為瀏覽器會繼續執行在伺服器端暫停的相同生命週期,並在渲染後在瀏覽器端恢復。

瀏覽器端:當組件首次在瀏覽器端掛載或渲染時,例如當用戶導航到單頁應用程序 (SPA) 中的新頁面時,或者當「模態」組件最初出現在頁面上時,生命週期將按以下步驟進行:

  useTask$ --> RENDER --> useVisibleTask$
 
| -------------- BROWSER --------------- |

請注意,生命週期完全相同,但這次所有鉤子都在瀏覽器端運行,而不是在伺服器端運行。

useTask$()

  • 時間點: 組件首次渲染之前,以及追蹤的狀態改變時
  • 次數: 至少一次
  • 平台: 伺服器和瀏覽器

useTask$() 註冊一個在組件創建時執行的鉤子,它將至少在伺服器或瀏覽器中運行一次,具體取決於組件最初渲染的位置。

此外,此任務可以是反應式的,並且在追蹤的 狀態 發生變化時會重新執行。

請注意,任何後續的任務重新執行都將始終在瀏覽器中進行,因為反應性是僅限瀏覽器的功能。

                      (state change) -> (re-execute)
                                  ^            |
                                  |            v
 useTask$(track) -> RENDER ->  CLICK  -> useTask$(track)
                        |
  | ----- SERVER ------ | ----------- BROWSER ----------- |
                        |
                   pause|resume

如果 useTask$() 沒有追蹤任何狀態,它將僅運行一次,在伺服器瀏覽器中(不是兩者),具體取決於組件最初渲染的位置。實際上就像一個「掛載時」的鉤子。

useTask$() 會阻止組件的渲染,直到其異步回調解析完成,換句話說,即使任務是異步的,它們也會按順序執行。(一次只執行一個任務)。

讓我們看一下任務的最簡單用例,即在組件初始化時運行一些異步工作

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const fibonacci = useSignal<number[]>();
 
  useTask$(async () => {
    const size = 40;
    const array = [];
    array.push(0, 1);
    for (let i = array.length; i < size; i++) {
      array.push(array[i - 1] + array[i - 2]);
      await delay(100);
    }
    fibonacci.value = array;
  });
 
  return <p>{fibonacci.value?.join(', ')}</p>;
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

在此示例中

  • useTask$() 每 100 毫秒計算一次費氏數列。因此,渲染 40 個條目需要 4 秒。
    • useTask$() 在伺服器上作為 SSR 的一部分執行(結果可能會緩存在 CDN 中。)
    • 由於 useTask$() 會阻止渲染,因此渲染 HTML 頁面需要 4 秒鐘。
  • 因為此任務沒有 track(),所以它永遠不會重新運行,使其有效地成為初始化代碼。
    • 因為此組件僅在伺服器上渲染,所以 useTask$() 永遠不會在瀏覽器上下載或運行。

請注意,useTask$()實際渲染之前在伺服器上運行。因此,如果您需要進行 DOM 操作,請改用 useVisibleTask$(),它會在渲染後在瀏覽器上運行。

當您需要時,請使用 useTask$()

  • 在渲染之前運行異步任務
  • 在組件首次渲染之前僅運行一次代碼
  • 在狀態更改時以編程方式運行副作用代碼

注意,如果您正在考慮使用 fetch()useTask$ 內部加載數據,請考慮改用 useResource$()。此 API 在利用 SSR 流式傳輸和並行數據獲取方面效率更高。

掛載時

在 Qwik 中,沒有一個像其他一些框架那樣的特定「掛載」步驟。相反,組件直接在需要它們的地方啟動,無論是在 Web 伺服器上還是在您的瀏覽器中。這是沒有內部追蹤函數的情況,該函數用於監控特定的數據片段。

useTask$ 在組件首次掛載時始終至少運行一次。

import { component$, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
 
  useTask$(async () => {
    // A task without `track` any state effectively behaves like a `on mount` hook.
    console.log('Runs once when the component mounts in the server OR client.');
  });
 
  return <div>Hello</div>;
});

Qwik 的一個獨特之處是組件在伺服器和客戶端上只掛載一次。這是可恢復性的一個特性。這意味著如果在伺服器端渲染 (SSR) 期間執行了 useTask$,則它不會在瀏覽器中再次運行,因為 Qwik 不會執行水合作用。

track()

有時,當組件狀態發生變化時,重新運行任務是可取的。這是通過使用 track() 函數來完成的。track() 函數允許您在最初在伺服器上渲染時設置對組件狀態的依賴關係,然後在瀏覽器中狀態更改時重新執行任務。相同的任務永遠不會在伺服器上執行兩次。

注意:如果您只想從現有狀態同步計算新狀態,則應改用 useComputed$()

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const text = useSignal('Initial text');
  const delayText = useSignal('');
 
  useTask$(({ track }) => {
    track(() => text.value);
    const value = text.value;
    const update = () => (delayText.value = value);
    isServer
      ? update() // don't delay on server render value as part of SSR
      : delay(500).then(update); // Delay in browser
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p>Delayed text: {delayText}</p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

在伺服器上

  • useTask$() 在伺服器上運行,track() 函數在 text 信號上設置訂閱。
  • 頁面已渲染。

在瀏覽器上

  • useTask$() 不必急切地運行或下載,因為 Qwik 知道該任務已訂閱伺服器執行中的 text 信號。
  • 當用戶在輸入框中輸入時,text 信號會發生變化。 Qwik 知道 useTask$() 已訂閱 text 信號,並且此時會將 useTask$() 閉包帶入 JavaScript VM 中執行。

useTask$()

  • useTask$() 會阻止渲染,直到它完成。如果您不想阻止渲染,請確保任務已解決,並在單獨的、未連接的 promise 上運行延遲工作。在此示例中,我們不等待 delay(),因為它會阻止渲染。

有時只需要在伺服器或客戶端中運行代碼。這可以使用從 @builder.io/qwik/build 導出的 isServerisBrowser 布林值來實現,如上所示。

作為函數的 track()

在上面的示例中,track() 用於追蹤特定信號。但是,track() 也可用作函數來一次追蹤多個信號。

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const isUppercase = useSignal(false);
  const text = useSignal('');
  const delayText = useSignal('');
 
  useTask$(({ track }) => {
    const value = track(() =>
      isUppercase.value ? text.value.toUpperCase() : text.value.toLowerCase()
    );
    const update = () => (delayText.value = value);
    isServer
      ? update() // don't delay on server render value as part of SSR
      : delay(500).then(update); // Delay in browser
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <label>
        Is uppercase? <input type="checkbox" bind:checked={isUppercase} />
      </label>
      <p>Delay text: {delayText}</p>
    </section>
  );
});
 
function delay(time: number) {
  return new Promise((resolve) => setTimeout(resolve, time));
}

在此示例中,track() 接受一個函數,該函數不僅讀取信號,還將其值轉換為大寫/小寫。 track() 訂閱多個信號並計算它們的值。

cleanup()

有時在運行任務時,需要執行清理工作。觸發新任務時,將調用先前任務的 cleanup() 回調。從 DOM 中移除組件時,也會調用此回調。

  • 任務完成時,不會調用 cleanup() 函數。僅在觸發新任務或移除組件時才會調用它。
  • 將應用程式序列化為 HTML 後,將在伺服器上調用 cleanup() 函數。
  • cleanup() 函數不可從伺服器轉移到瀏覽器。清理旨在釋放運行它的 VM 上的資源。它不打算被轉移到瀏覽器。

此示例顯示瞭如何使用 cleanup() 函數實現去抖動功能。

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const text = useSignal('');
  const debounceText = useSignal('');
 
  useTask$(({ track, cleanup }) => {
    const value = track(() => text.value);
    const id = setTimeout(() => (debounceText.value = value), 500);
    cleanup(() => clearTimeout(id));
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p>Debounced text: {debounceText}</p>
    </section>
  );
});

useVisibleTask$()

有時任務只需要在瀏覽器上且在渲染後運行,在這種情況下,您應該使用 useVisibleTask$()useVisibleTask$() 類似於 useTask$(),但它僅在瀏覽器上且在初始渲染後運行。 useVisibleTask$() 註冊一個鉤子,以便在組件在視口中可見時執行,它至少會在瀏覽器中運行一次,並且它可以是響應式的,並在某些已追蹤狀態 更改時重新執行。

useVisibleTask$() 具有以下屬性

  • 僅在客戶端上運行。
  • 當組件可見時,在客戶端上急切地執行代碼。
  • 在初始渲染後運行。
  • 不阻止渲染。

注意useVisibleTask$() 應該作為最後的手段使用,因為它會在客戶端上急切地執行代碼。 Qwik 通過 可恢復性 竭盡全力延遲在客戶端上執行代碼,而 useVisibleTask$() 是一個應謹慎使用的逃生艙口。有關更多詳細信息,請參閱 最佳實務。如果您需要在客戶端上運行任務,請考慮使用帶有伺服器防護的 useTask$()

import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { isServer } from '@builder.io/qwik/build';
 
export default component$(() => {
  const text = useSignal('Initial text');
  const isBold = useSignal(false);
 
  useTask$(({ track }) => {
    track(() => text.value);
    if (isServer) {
      return; // Server guard
    }
    isBold.value = true;
    delay(1000).then(() => (isBold.value = false));
  });
 
  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p style={{ fontWeight: isBold.value ? 'bold' : 'normal' }}>
        Text: {text}
      </p>
    </section>
  );
});
 
const delay = (time: number) => new Promise((res) => setTimeout(res, time));

在上面的例子中,`useTask$()` 被 `isServer` 保護。`track()` 函數放置在保護之前,這允許服務器設置訂閱,而無需在服務器上執行任何程式碼。然後,一旦 `text` 信號發生變化,客戶端就會執行 `useTask$()`。

這個例子展示了如何使用 `useVisibleTask$()` 僅在時鐘組件可見時,才在瀏覽器上初始化時鐘。

import {
  component$,
  useSignal,
  useVisibleTask$,
  type Signal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const isClockRunning = useSignal(false);
 
  return (
    <>
      <div style="position: sticky; top:0">
        Scroll to see clock. (Currently clock is
        {isClockRunning.value ? ' running' : ' not running'}.)
      </div>
      <div style="height: 200vh" />
      <Clock isRunning={isClockRunning} />
    </>
  );
});
 
const Clock = component$<{ isRunning: Signal<boolean> }>(({ isRunning }) => {
  const time = useSignal('paused');
  useVisibleTask$(({ cleanup }) => {
    isRunning.value = true;
    const update = () => (time.value = new Date().toLocaleTimeString());
    const id = setInterval(update, 1000);
    cleanup(() => clearInterval(id));
  });
  return <div>{time}</div>;
});

請注意,時鐘的 `useVisibleTask$()` 在 `<Clock>` 組件可見之前並不會運行。`useVisibleTask$()` 的預設行為是在組件可見時運行任務。此行為是通過 交叉觀察器 實現的。

**注意**:交叉觀察器 不會在不被視為可見的組件上運行,例如 `<audio />`。

選項 `eagerness`

有時,希望在應用程序加載到瀏覽器後立即急切地運行 `useVisibleTask$()`。在這種情況下,`useVisibleTask$()` 需要以急切模式運行。這是通過使用 `{ strategy: 'document-ready' }` 來完成的。

import {
  component$,
  useSignal,
  useVisibleTask$,
  type Signal,
} from '@builder.io/qwik';
 
export default component$(() => {
  const isClockRunning = useSignal(false);
 
  return (
    <>
      <div style="position: sticky; top:0">
        Scroll to see clock. (Currently clock is
        {isClockRunning.value ? ' running' : ' not running'}.)
      </div>
      <div style="height: 200vh" />
      <Clock isRunning={isClockRunning} />
    </>
  );
});
 
const Clock = component$<{ isRunning: Signal<boolean> }>(({ isRunning }) => {
  const time = useSignal('paused');
  useVisibleTask$(
    ({ cleanup }) => {
      isRunning.value = true;
      const update = () => (time.value = new Date().toLocaleTimeString());
      const id = setInterval(update, 1000);
      cleanup(() => clearInterval(id));
    },
    { strategy: 'document-ready' }
  );
  return <div>{time}</div>;
});

在這個例子中,時鐘會立即在瀏覽器上開始運行,而不管它是否可見。

進階:運行時間以及使用 CSS 管理可見性

在內部,`useVisibleTask$` 是通過向第一個渲染的組件(返回的組件,或者在 Fragment 的情況下,它的第一個子組件)添加一個屬性來實現的。使用標準的 `eagerness`,這意味著如果第一個渲染的組件是隱藏的,則任務將不會運行。

這意味著您可以使用 CSS 來影響任務何時運行。例如,如果任務應該只在行動裝置上運行,則可以返回一個 `<div class="md:invisible" />`(在 Tailwind CSS 的情況下)。

這也意味著您不能使用可見任務來取消隱藏組件;為此,您可以返回一個 Fragment。

return (<>
  <div />
  <MyHiddenComponent hidden={!showSignal.value} />
</>)

使用鉤子規則

使用生命週期鉤子時,您必須遵守以下規則

  • 它們只能在 `component$` 的根級別被調用(不能在條件塊內部)。
  • 它們只能在另一個 `use*` 方法的根級別被調用,允許組合。
useHook(); // <-- ❌ does not work
 
export default component$(() => {
  useCustomHook(); // <-- ✅ does work
  if (condition) {
    useHook(); // <-- ❌ does not work
  }
  useTask$(() => {
    useNavigate(); // <-- ❌ does not work
  });
  const myQrl = $(() => useHook()); // <-- ❌ does not work
  return <button onClick$={() => useHook()}></button>; // <-- ❌ does not work
});
 
function useCustomHook() {
  useHook(); // <-- ✅ does work
  if (condition) {
    useHook(); // <-- ❌ does not work
  }
}

貢獻者

感謝所有幫助改進此文檔的貢獻者!

  • mhevery
  • manucorporat
  • wtlin1228
  • AnthonyPAlicea
  • the-r3aper7
  • sreeisalso
  • brunocrosier
  • harishkrishnan24
  • gioboa
  • bodhicodes
  • zanettin
  • blackpr
  • mrhoodz
  • ehrencrona
  • julianobrasil
  • adamdbradley
  • aendel
  • jemsco