任務
任務是用於在元件初始化或元件狀態變更時執行非同步操作。
注意:任務類似於 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
導出的isServer
和isBrowser
布林值來實現,如上所示。
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
}
}