事件

为了使 Web 应用程序具有交互性,需要一种方法来响应用户事件。这可以通过在 JSX 模板中注册回调函数来实现。事件处理程序使用 on{EventName}$ 属性注册。例如,onClick$ 属性用于监听 click 事件。

<button onClick$={() => alert('CLICKED!')}>click me!</button>

内联处理程序

在以下示例中,<button> 元素的 onClick$ 属性用于让 Qwik 知道,每当 <button> 触发 click 事件时,应该执行回调 () => store.count++

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Increment {count.value}
    </button>
  );
});

您还可以使用 bind:propertyName 方便地在信号和输入元素之间进行 双向绑定

请注意,onClick$$ 结尾。这既是 优化器 的提示,也是开发人员的提示,表明在此位置会发生特殊转换。$ 后缀的存在意味着此处存在一个延迟加载边界。与 click 处理程序相关的代码不会加载到 JavaScript 虚拟机 (VM) 中,直到用户激活点击事件。但是,为了避免在第一次交互期间出现延迟,它会被急切地加载到浏览器缓存中。

在实际应用中,监听器可能引用复杂代码。通过创建延迟加载边界(使用 $),Qwik 可以对点击监听器后面的所有代码进行树状抖动,并延迟其加载,直到用户点击按钮。

重用事件处理程序

要将同一个事件处理程序用于多个元素或事件,您必须将事件处理程序包装在 @builder.io/qwik 导出的 $() 函数中。这会将其转换为 QRL

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const increment = $(() => count.value++);
  return (
    <>
      <button onClick$={increment}>Increment</button>
      <p>Count: {count.value}</p>
    </>
  );
});

注意:如果您提取了事件处理程序,则必须手动将其包装在 $(...handler...) 中。这确保了它被延迟附加。

多个事件处理程序

要为同一个事件注册多个事件处理程序,您可以将事件处理程序数组传递给 on{EventName}$ 属性。

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const print = $((ev) => console.log('CLICKED!', ev));
  const increment = $(() => count.value++);
 
  // The button when clicked will print "CLICKED!" to the console, increment the count and send an event to Google Analytics.
  return (
    <button
      onClick$={[print, increment, $(() => {
        ga.send('click', { label: 'increment' });
      })]}
    >
      Count: {count.value}
    </button>
  );
});

事件对象

事件处理程序的第一个参数是 Event 对象。此对象包含有关触发处理程序的事件的信息。例如,click 事件的 Event 对象包含有关鼠标位置和被点击元素的信息。您可以查看 MDN 文档以了解有关每个 DOM 事件的更多详细信息

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const position = useSignal<{ x: number; y: number }>();
  return (
    <div
      onClick$={(event) => (position.value = { x: event.x, y: event.y })}
      style="height: 100vh"
    >
      <p>
        Clicked at: ({position.value?.x}, {position.value?.y})
      </p>
    </div>
  );
});

异步事件

由于 Qwik 的异步特性,如果事件处理程序的实现尚未加载到 JavaScript VM 中,则其执行可能会延迟。因此,Event 对象上的以下 API 将不起作用

  • event.preventDefault()
  • event.currentTarget

阻止默认行为

由于事件处理是异步的,因此您无法使用 event.preventDefault()。为了解决这个问题,Qwik 引入了一种声明性的方法,通过 preventdefault:{eventName} 属性来阻止默认行为。

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <a
      href="/docs"
      preventdefault:click // This will prevent the default behavior of the "click" event.
      onClick$={() => {
        // event.PreventDefault() will not work here, because handler is dispatched asynchronously.
        alert('Do something else to simulate navigation...');
      }}
    >
      Go to docs page
    </a>
  );
});

事件目标

由于事件处理是异步的,因此您无法使用 event.currentTarget。为了解决这个问题,Qwik 处理程序提供了一个 currentTarget 作为第二个参数。

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const currentElm = useSignal<HTMLElement|null>(null);
  const targetElm = useSignal<HTMLElement|null>(null);
 
  return (
    <section onClick$={(event, currentTarget) => {
      currentElm.value = currentTarget;
      targetElm.value = event.target as HTMLElement;
    }}>
      Click on any text <code>target</code> and <code>currentElm</code> of the event.
      <hr/>
      <p>Hello <b>World</b>!</p>
      <hr/>
      <ul>
        <li>currentElm: {currentElm.value?.tagName}</li>
        <li>target: {targetElm.value?.tagName}</li>
      </ul>
    </section>
  );
});

注意:DOM 中的 currentTarget 指向附加事件监听器的元素。在上面的示例中,它始终是 <SECTION> 元素。

同步事件处理

在某些情况下,有必要以传统方式处理事件,因为某些 API 需要同步使用。例如,dragstart 事件必须同步处理,因此它不能与 Qwik 的延迟代码执行相结合。

要做到这一点,您可以利用 useVisibleTask 以编程方式使用 DOM API 直接添加事件监听器。

import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const draggableRef = useSignal<HTMLElement>();
  const dragStatus = useSignal('');
 
  useVisibleTask$(({ cleanup }) => {
    if (draggableRef.value) {
      // Use the DOM API to add an event listener.
      const dragstart = () => (dragStatus.value = 'dragstart');
      const dragend = () => (dragStatus.value = 'dragend');
 
      draggableRef.value!.addEventListener('dragstart', dragstart);
      draggableRef.value!.addEventListener('dragend', dragend);
      cleanup(() => {
        draggableRef.value!.removeEventListener('dragstart', dragstart);
        draggableRef.value!.removeEventListener('dragend', dragend);
      });
    }
  });
 
  return (
    <div>
      <div draggable ref={draggableRef}>
        Drag me!
      </div>
      <p>{dragStatus.value}</p>
    </div>
  );
});

注意 使用 VisibleTask 监听事件在 Qwik 中是一种反模式,因为它会导致浏览器中急切地执行代码,从而破坏了 可恢复性。只有在别无选择的情况下才使用它。您应该使用 JSX 来监听事件,例如:<div onClick$={...}>。或者,如果您需要以编程方式监听事件,请考虑使用 useOn(...) 事件方法。

自定义事件属性

在创建组件时,传递类似于事件处理程序的自定义事件属性通常很有用,即使它们只是回调而不是实际的 DOM 事件。Qwik 中的组件边界必须可序列化,以便优化器可以将其拆分为单独的块。函数不可序列化,除非它们被转换为 QRL。

例如,监听默认情况下 HTML 无法执行的三击事件,将需要创建一个 onTripleClick$ 自定义事件属性。

import { component$, Slot, useStore } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <Button onTripleClick$={() => alert('TRIPLE CLICKED!')}>
      Triple Click me!
    </Button>
  );
});
 
type ButtonProps = {
  onTripleClick$: QRL<() => void>;
};
 
export const Button = component$<ButtonProps>(({ onTripleClick$ }) => {
  const state = useStore({
    clicks: 0,
    lastClickTime: 0,
  });
  return (
    <button
      onClick$={() => {
        // triple click logic
        const now = Date.now();
        const timeBetweenClicks = now - state.lastClickTime;
        state.lastClickTime = now;
        if (timeBetweenClicks > 500) {
          state.clicks = 0;
        }
        state.clicks++;
        if (state.clicks === 3) {
          // handle custom event
          onTripleClick$();
          state.clicks = 0;
        }
      }}
    >
      <Slot />
    </button>
  );
});

请注意在 onTripleClick$: QRL<() => void>; 中使用 QRL 类型。它类似于在类型级别将函数包装在 $() 中。如果你有 const greet = $(() => "hi 👋"); 并在 'greet' 上悬停,你会看到 'greet' 的类型为 QRL<() => "hi 👋">

窗口和文档事件

到目前为止,讨论的重点是监听来自元素的事件。有一些事件,例如 scrollmousemove,需要在 windowdocument 上监听。Qwik 通过在监听事件时提供 document:onwindow:on 前缀来实现这一点。

window:on/document: 前缀用于在组件的当前 DOM 位置注册事件,同时允许它接收来自 window/document 的事件。这样做有两个优点:

  1. 事件可以在你的 JSX 中声明式地注册。
  2. 当组件被销毁时,事件会自动清理(不需要显式簿记和清理)。

useOn[window|document] 钩子

  • useOn():监听当前组件根元素上的事件。
  • useOnWindow():监听 window 对象上的事件。
  • useOnDocument():监听 document 对象上的事件。

useOn[window|document]() 钩子将在组件级别以编程方式添加一个基于 DOM 的事件监听器。这在你想创建自己的使用钩子或在编译时不知道事件名称时很有用。

import { $, component$, useOnDocument, useStore } from '@builder.io/qwik';
 
// Assume reusable use method that does not have access to JSX
// but needs to register event handlers.
function useMousePosition() {
  const position = useStore({ x: 0, y: 0 });
  useOnDocument(
    'mousemove',
    $((event) => {
      const { x, y } = event as MouseEvent;
      position.x = x;
      position.y = y;
    })
  );
  return position;
}
 
export default component$(() => {
  const pos = useMousePosition();
  return (
    <div>
      MousePosition: ({pos.x}, {pos.y})
    </div>
  );
});

贡献者

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

  • voluntadpear
  • the-r3aper7
  • RATIU5
  • manucorporat
  • nnelgxorz
  • adamdbradley
  • hamatoyogi
  • fleish80
  • cunzaizhuyi
  • Pika-Pool
  • mhevery
  • AnthonyPAlicea
  • amatiash
  • harishkrishnan24
  • fabian-hiller
  • igorbabko
  • mrhoodz
  • julianobrasil
  • maiieul
  • Balastrong
  • Jemsco