事件
为了使 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> }) => {
...
});
窗口和文档事件
到目前为止,我们已经讨论了如何监听来自元素的事件。有些事件(例如 scroll
和 mousemove
)需要我们在 window
或 document
上监听它们。因此,Qwik 允许在监听事件时使用 document:on
和 window:on
前缀。
window:on
/document:
的目的是在组件的当前 DOM 位置注册事件,但从 window
/document
接收事件。这样做有两个优点:
- 事件可以在 JSX 中声明式地注册。
- 当组件被销毁时,事件会自动清理(无需显式的清理和管理)。
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>
);
});