任务
任务用于作为组件初始化或组件状态更改的一部分运行异步操作。
注意:任务类似于 React 中的
useEffect()
,但是它们之间有足够的差异,我们不希望将它们称为相同的名称,以免带来关于它们如何工作的预期。主要的区别是:
- 任务是异步的。
- 任务在服务器和浏览器上运行。
- 任务在渲染之前运行,并且可以阻塞渲染。
useTask$()
应该是你默认的 API,用于在组件初始化或组件状态更改时运行异步(或同步)工作。只有当你无法使用 useTask$()
实现所需功能时,才应该考虑使用 useVisibleTask$()
或 useResource$()
。
useTask$()
的基本用例是在组件初始化时执行工作。useTask$()
具有以下特性:
- 它可以在服务器或浏览器上运行。
- 它在渲染之前运行并阻塞渲染。
- 如果有多个任务正在运行,则它们按照注册的顺序依次运行。异步任务将阻塞下一个任务的运行,直到它完成。
任务还可以用于在组件状态更改时执行工作。在这种情况下,每次跟踪的状态更改时,任务都会重新运行。参见:track()
。
有时,任务只需要在浏览器上运行并在渲染后运行,这种情况下,你应该使用 useVisibleTask$()
。
有时,任务应该异步获取数据并生成一个信号(而不阻塞渲染),这种情况下,你应该使用 useResource$()
。
生命周期
由于 可恢复性 的存在,组件的生命周期跨越服务器和浏览器运行时。有时组件首先在服务器上呈现,而其他时候可能在浏览器上呈现。然而,在这两种情况下,生命周期(顺序)是相同的,只是执行位置在不同的环境中(服务器 vs 浏览器)。
注意:对于使用 hydration 的系统,应用程序的执行会发生两次。一次在服务器上(SSR/SSG),一次在浏览器上(hydration)。因此,许多框架具有仅在浏览器上执行的“效果”。这意味着在服务器上运行的代码与在浏览器上运行的代码不同。Qwik 的执行是统一的,这意味着如果代码已经在服务器上执行过,则不会在浏览器上重新执行。
在 Qwik 中,只有 3 个生命周期阶段:
Task
- 在渲染之前运行,也在跟踪的状态更改时运行。(任务按顺序运行,并阻塞渲染。)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$()
注册一个钩子,在组件创建时执行,它将至少在服务器或浏览器上运行一次,具体取决于组件最初的渲染位置。
此外,此任务可以是响应式的,并在跟踪的状态更改时重新执行。
请注意,任务的任何后续重新执行都将始终在浏览器中进行,因为响应性是仅在浏览器中存在的事物。
(状态更改) -> (重新执行)
^ |
| v
useTask$(track) -> RENDER -> CLICK -> useTask$(track)
|
| ----- 服务器 ------ | ----------- 浏览器 ----------- |
|
暂停|恢复
如果
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()
),请考虑使用useResource$()
。这个 API 在利用 SSR 流式传输和并行数据获取方面更高效。
在挂载时
Qwik 没有特定的“挂载”钩子,因为没有跟踪任何状态的 useTask$()
实际上就像一个挂载钩子。
这是因为 useTask$
总是在组件首次挂载时运行至少一次。
import { component$, useTask$ } from '@builder.io/qwik';
export default component$(() => {
useTask$(async () => {
// 没有跟踪任何状态的任务实际上就像一个“挂载”钩子。
console.log('当组件在服务器或客户端挂载时运行一次。');
});
return <div>Hello</div>;
});
Qwik 的一个独特之处是,组件只在服务器和客户端上挂载一次。这是可恢复性的一个特性。这意味着如果 useTask$
在 SSR 期间运行,它将不会在浏览器中再次运行,因为 Qwik 不会进行 hydration。
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() // 不要在服务器上延迟渲染值作为 SSR 的一部分
: delay(500).then(update); // 在浏览器中延迟
});
return (
<section>
<label>
输入文本:<input bind:value={text} />
</label>
<p>延迟文本:{delayText}</p>
</section>
);
});
const delay = (time: number) => new Promise((res) => setTimeout(res, time));
在服务器上:
useTask$()
在服务器上运行,track()
函数在text
信号上设置了一个订阅。- 页面被渲染。
在浏览器上:
useTask$()
不必运行(或下载),因为 Qwik 知道该任务订阅了来自服务器执行的text
信号。- 当用户在输入框中键入时,
text
信号发生变化。Qwik 知道useTask$()
订阅了text
信号,此时将执行useTask$()
闭包。
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() // 不要在服务器上延迟渲染值作为 SSR 的一部分
: delay(500).then(update); // 在浏览器中延迟
});
return (
<section>
<label>
输入文本:<input bind:value={text} />
</label>
<label>
是否大写?<input type="checkbox" bind:checked={isUppercase} />
</label>
<p>延迟文本:{delayText}</p>
</section>
);
});
function delay(time: number) {
return new Promise((resolve) => setTimeout(resolve, time));
}
在此示例中,
track()
接受一个函数,该函数不仅读取信号,还将其值转换为大写/小写。track()
在多个信号上进行订阅,并计算它们的值。
cleanup()
有时,在运行任务时需要执行清理工作。当触发新任务时,将调用上一个任务的 cleanup()
回调函数。(当组件从 DOM 中删除时,也会调用 cleanup()
回调函数。)
- 当任务完成时,不会调用
cleanup()
函数。只有在触发新任务或从 DOM 中删除组件时才会调用它。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>
输入文本:<input bind:value={text} />
</label>
<p>防抖文本:{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));
In the above example the
useTask$()
is guarded byisServer
. Thetrack()
is before the guard which allows the server to set up the subscription but does not execute any code on the server. The client then executes theuseTask$()
once thetext
signal changes.
这个示例展示了如何使用 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$()
的默认行为是在组件可见时运行任务。此行为是通过 intersection observers 实现的。
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(); // <-- ❌ 不起作用
export default component$(() => {
useCustomHook(); // <-- ✅ 起作用
if (condition) {
useHook(); // <-- ❌ 不起作用
}
useTask$(() => {
useNavigate(); // <-- ❌ 不起作用
});
const myQrl = $(() => useHook()); // <-- ❌ 不起作用
return <button onClick$={() => useHook()}></button>; // <-- ❌ 不起作用
});
function useCustomHook() {
useHook(); // <-- ✅ 起作用
if (condition) {
useHook(); // <-- ❌ 不起作用
}
}