任务

任务用于在组件初始化或组件状态更改时运行异步操作。

注意:任务类似于 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 导出的 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() 函数。它仅在触发新任务或删除组件时调用。
  • 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
  }
}

贡献者

感谢所有帮助改进此文档的贡献者!

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