事件

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

<button onClick$={() => alert('点击了!')}>点击我!</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++}>
      增加 {count.value}
    </button>
  );
});

请注意,onClick$$ 结尾。这是对 Optimizer 和开发人员的提示,表示在此位置发生了特殊的转换。$ 后缀的存在意味着此处存在延迟加载边界。与 click 处理程序相关的代码直到用户触发 click 事件之前不会加载到 JavaScript VM 中,但它将急切地 加载到浏览器缓存 中,以免在首次交互时造成延迟。

在实际应用中,监听器可能引用复杂的代码。通过创建延迟加载边界(使用 $),Qwik 可以树摇所有与点击监听器相关的代码,并延迟其加载,直到用户点击按钮。

重用事件处理程序

如果我们想要为多个元素或事件重用相同的事件处理程序,我们需要从 @builder.io/qwik 导入 $,并将事件处理程序包装在其中。

这样,我们必须将事件处理程序提取到 QRLs 中,并将其传递给事件监听器。

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++);
 
  // 当按钮被点击时,将在控制台打印 "CLICKED!",增加计数,并向 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>
        点击位置:({position.value?.x}, {position.value?.y})
      </p>
    </div>
  );
});

异步事件

由于 Qwik 的异步特性,事件处理程序的执行可能会延迟,因为实现尚未加载到 JavaScript VM 中。由于 Qwik 中事件处理的异步性质,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 // 这将阻止 "click" 事件的默认行为。
      onClick$={() => {
        // 这里不能使用 event.PreventDefault(),因为处理程序是异步调度的。
        alert('Do something else to simulate navigation...');
      }}
    >
      前往文档页面
    </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;
    }}>
      点击任意文本 <code>target</code><code>currentElm</code> 事件。
      <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) {
      // 使用 DOM API 添加事件监听器。
      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}>
        拖动我!
      </div>
      <p>{dragStatus.value}</p>
    </div>
  );
});

注意: 使用 VisibleTask 监听事件是 Qwik 中的反模式,因为它会导致在浏览器中急切执行代码,从而破坏了可恢复性。只有在没有其他选择时才使用它。大多数情况下,你应该使用 JSX 来监听事件:<div onClick$={...}> 或者使用 useOn(...) 事件方法来进行编程式监听。

自定义事件属性

在创建组件时,传递自定义事件属性通常是很有用的,这些属性看起来像事件处理程序(尽管它们不是 DOM 事件,只是回调函数)。Qwik 中的组件边界必须是可序列化的,以便优化器将它们拆分为单独的块,而函数是不可序列化的,除非它们使用 $ 符号转换为 QRL。

在使用泛型语法的 component$ 时,Qwik 将自动处理类型转换。

import { component$, Slot, useStore } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <Button onTripleClick$={() => alert('TRIPLE CLICKED!')}>
      Triple click me!
    </Button>
  );
});
 
type ButtonProps = {
  onTripleClick$: () => void;
};
 
export const Button = component$<ButtonProps>(({ onTripleClick$ }) => {
  const state = useStore({
    clicks: 0,
    lastClickTime: 0,
  });
  return (
    <button
      onClick$={() => {
        // 三击逻辑
        const now = Date.now();
        const timeBetweenClicks = now - state.lastClickTime;
        state.lastClickTime = now;
        if (timeBetweenClicks > 500) {
          state.clicks = 0;
        }
        state.clicks++;
        if (state.clicks === 3) {
          // 处理自定义事件
          onTripleClick$();
          state.clicks = 0;
        }
      }}
    >
      <Slot />
    </button>
  );
});
 

⚠️ 当使用类型注解时,我们需要使用 PropFunction 将事件类型包装起来,以告诉 TypeScript 该函数不能同步调用。

component$(({ onTripleClick$ } : { onTripleClick$?: PropFunction<() => void> }) => {
  ...
});

窗口和文档事件

到目前为止,我们已经讨论了如何监听来自元素的事件。有些事件(例如 scrollmousemove)需要我们在 windowdocument 上监听它们。因此,Qwik 允许在监听事件时使用 document:onwindow:on 前缀。

window:on/document: 的目的是在组件的当前 DOM 位置注册事件,但从 window/document 接收事件。这样做有两个优点:

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

useOn[window|document] Hook

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

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

import { $, component$, useOnDocument, useStore } from '@builder.io/qwik';
 
// 假设可重用的 use 方法没有访问 JSX,但需要注册事件处理程序。
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>
  );
});

Contributors

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

  • voluntadpear
  • the-r3aper7
  • RATIU5
  • manucorporat
  • nnelgxorz
  • adamdbradley
  • hamatoyogi
  • fleish80
  • cunzaizhuyi
  • Pika-Pool
  • mhevery
  • AnthonyPAlicea
  • amatiash
  • harishkrishnan24
  • maiieul