Events

一个应用想要交互,就要有响应用户事件的方式。 在QWIK里,通过 在JSX模板里注册回调函数 的方式添加事件处理。

export const Counter = component$(() => {
  const store = useStore({ count: 0 });

  return <button onClick$={() => store.count++}>{store.count}</button>;
});

在上面的例子里,<button>元素的onClick$属性用于注册事件处理程序。 当用户点击按钮时,() => store.count++应该被执行。

注意onClick$$结尾。 这是一个既给开发者也给QWIK Optimizer的提示,表面这个位置要有特殊的转换发生。 $后缀的出现意味着这里有一个懒加载的边界。 click的事件处理程序代码在用户触发click事件前 是不会下载的。 更多细节请参考 Optimizer Rules

上面例子里,click事件处理程序实现的太简单了。在真实的应用里,事件监听器可能是很复杂的一段代码。 通过创建懒加载边界,QWIK可以tree-shake掉很大一段事件处理代码,直到用户点击按钮时再懒加载这段代码。

除了回调函数的方式,你也可以在onClick$后面传QRLs。 即上面的例子也可以用下面的方式写:

import { component$, useStore, $ } from "@builder.io/qwik";

export const Counter = component$(() => {
  const store = useStore({ count: 0 });
  const incrementCount = $(() => store.count++)

  return <button onClick$={incrementCount}>{store.count}</button>;
});

Prevent default

由于QWIK的异步特性,事件处理程序的执行可能稍微有点延时,因为代码可能还没下载。 这就引发了一个问题,当事件需要阻止事件默认行为时,传统的event.preventDefault()是不生效的, 所以在QWIK里我们使用preventdefault:{eventName}属性。

export const PreventDefaultExample = component$(() => {
  return (
    <a
      href="/about"
      preventdefault:click // This will prevent the default behavior of the "click" event.
      onClick$={(event) => {
        // PreventDefault will not work here, because handle is dispatched asynchronously.
        // event.preventDefault();
        singlePageNavigate('/about');
      }}
    >
      Go to about page
    </a>
  );
});

Window 和 Document 事件

目前为止,我们讨论了怎么在元素自身上添加事件。 有些事件(比如scrollmousemove)需要挂在window 或者 document上进行监听。 QWIK提供了document:onwindow:on前缀来应对这种情况。

export const EventExample = component$(() => {
  const store = useStore({
    scroll: 0,
    mouse: { x: 0, y: 0 },
    clickCount: 0,
  });

  return (
    <button
      window:onScroll$={(e) => (store.scroll = window.scrollY)}
      document:onMouseMove$={(e) => {
        store.mouse.x = e.x;
        store.mouse.y = e.y;
      }}
      onClick$={() => store.clickCount++}
    >
      scroll: {store.scroll}
      mouseMove: {store.mouse.x}, {store.mouse.y}
      click: {store.clickCount}
    </button>
  );
});

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

  1. 事件可以写在 JSX 里
  2. 当组件销毁时事件自动清理掉。(不需要显示地去清除事件监听器)

Events 和 Components

组件是函数,不是元素。 因此可以通过传递props的方式传递自定义事件。

现在让我们看看怎么声明一个 带事件的子组件。

import { PropFunction } from '@builder.io/qwik';

interface CmpButtonProps {
  onClick$?: PropFunction<() => void>;
}

export const CmpButton = component$((props: CmpButtonProps) => {
  return (
    <button onDblclick$={props.onClick$}>
      <Slot />
    </button>
  );
});

As far as Qwik is concerned, passing events to a component is equivalent to passing as props. In our example, we declare all props in CmpButtonProps interface. Specifically, notice onClick$: PropFunction<() => void> declaration.

当需要调用这个组件时,这么写:

<CmpButton onClick$={() => store.cmpCount++}>{store.cmpCount}</CmpButton>

Working with QRLs

让我们看一下上面<CmpButton>实现的变种。 这个例子里, Let's look at a variation of the above <CmpButton> implementation. In this example, we would like to demonstrate the passing of callbacks to components. For this reason, we have created an additional listener onClick$

interface CmpButtonProps {
  onClick$?: PropFunction<() => number>;
}

export const CmpButton = component$((props: CmpButtonProps) => {
  return (
    <button
      onDblclick$={props.onClick$}
      onClick$={async () => {
        const nu = await props?.onClick$();
        console.log('clicked', nu);
      }}
    >
      <Slot />
    </button>
  );
});

Notice that we can pass the props.onClick$ method directly to the onDblclick$ attribute as seen on <button> element. (see attribute onDblclick$={props.onClick$}) This is because both the inputting prop onClick$ as well as JSX prop onDblclick are of type PropFunction<?> (and both have $ suffix).

However, it is not possible to pass props.onClick$ to onClick$ because the types don't match. (This would result in the type error: onClick$={props.onClick$}) Instead, the $ is reserved for inlined closures. In our example, we would like to console.log("clicked") after we process the props.onClick callback. We can do so with the props.onClick$() method. This method will:

  1. Lazy load the code
  2. Restore the closure state
  3. Invoke the closure

The operation is asynchronous and therefore returns a promise, which we can resolve using the await statement.

State 恢复

export const Counter = component$(() => {
  const store = useStore({ count: 0 });

  return <button onClick$={() => store.count++}>{store.count}</button>;
});

乍一看,QWIK仅仅是懒加载了这个onClick$函数。 但是仔细观察,实际上懒加载的是一个闭包而非函数。 (A closure is a function that lexically captures the state inside its variables. 换句话说, 闭包携带 state, 而函数不携带) The capturing of the state is what allows the Qwik application to simply resume where the server left off because the recovered closure carries the state of the application with it.

在我们的例子里, onClick$ 闭包 捕获了 store。 捕获的 store 让应用 在不需要re-run整个应用的情况下,就能在点击时 增加 count 属性的值。 让我们看看QWIK里的闭包捕获是如何工作的。

上述代码生成的HTML基本上如下面展示的这样:

<div>
  <button q:obj="1" on:click="./chunk-a.js#Counter_button_onClick[0]">0</button>
</div>

请注意 on:click 属性 包含三部分信息:

  1. ./chunk-a.js: 需要去懒加载的文件。
  2. Counter_button_onClick: 需要从懒加载的chunk里获取的symbol。
  3. [0]: 一个闭包捕获到的变量引用的数组。(闭包里的state).

在这个例子里, () => store.count++ 仅仅捕获了 store, 因此只包含一个引用 0. 0q:obj 属性所引用的对象的索引。q:obj是对实际序列化的对象的引用。 在这里指的是 store。 (更准确的机制和语法是可能随时改变的实现细节。)

Qwikloader

为了让浏览器理解on:click语法,就有了仅包含一点点JS的Qwikloader。 Qwikloader很小(大约1KB),执行很快(大约5ms)。 Qwikloader的代码是内联到HTML里面的,所以可以很快执行到。 当用户和应用交互时,浏览器会在相应的DOM上触发相关事件。 在根DOM元素上,Qwikloader监听事件然后尝试定位on:<event>属性。 如果找到了这个属性,那属性值就能解析到 待下载的chunk的位置,然后执行它。

更多细节请参考 Qwikloader

Made with ❤️ by