任务
任务用于在组件初始化或组件状态更改时运行异步操作。
注意:任务类似于 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$()
不跟踪任何状态,它将只运行一次,无论是在服务器还是在浏览器中(不会同时运行),具体取决于组件最初在何处渲染。有效地表现得像一个“on-mount”钩子。
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$()
:
- 在渲染之前运行异步任务
- 在组件首次渲染之前仅运行一次代码
- 在状态更改时以编程方式运行副作用代码
注意,如果您考虑在
useTask$
中使用fetch()
加载数据,请考虑改用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 的一个独特之处在于,组件在服务器端和客户端仅挂载一次。这是可恢复性的一个属性。这意味着,如果 useTask$
在服务器端渲染 (SSR) 期间执行,它将不会在浏览器中再次运行,因为 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()
函数。它仅在触发新任务或删除组件时调用。cleanup()
函数在应用程序被序列化为 HTML 后在服务器端调用。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
}
}