任务

任务用于作为组件初始化或组件状态更改的一部分运行异步操作。

注意:任务类似于 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 导出的 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() // 不要在服务器上延迟渲染值作为 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 by isServer. The track() 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 the useTask$() once the text 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(); // <-- ❌ 不起作用
  }
}

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • mhevery
  • manucorporat
  • wtlin1228
  • AnthonyPAlicea
  • the-r3aper7
  • sreeisalso
  • brunocrosier
  • harishkrishnan24