Svelte 5 事件处理
新版事件语法(Svelte 5)
Svelte 5 对事件处理进行了重大更新:废弃了旧版的 on: 指令(如 on:click),改为使用标准的 HTML 事件处理属性语法(如 onclick)。这使 Svelte 的事件处理与标准 HTML/JS 更加一致,也让代码更易于理解。
<!-- Svelte 4 旧版 -->
<button on:click={handler}>
点击
</button>
<input
on:input={handleInput}
on:keydown|escape={close}
/>
<!-- Svelte 5 新版 -->
<button onclick={handler}>
点击
</button>
<input
oninput={handleInput}
onkeydown={(e) => {
if (e.key === 'Escape') close();
}}
/>
内联事件处理器
<script lang="ts">
let count = $state(0);
let log = $state<string[]>([]);
function handleClick(event: MouseEvent) {
count++;
log.push(`点击于 (${event.clientX}, ${event.clientY})`);
}
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
// 处理表单
}
</script>
<!-- 引用函数 -->
<button onclick={handleClick}>点击次数:{count}</button>
<!-- 内联箭头函数 -->
<button onclick={() => count++}>简短形式</button>
<!-- 带参数 -->
<button onclick={(e) => log.push(e.type)}>记录事件类型</button>
<!-- 表单提交 -->
<form onsubmit={handleSubmit}>
<button type="submit">提交</button>
</form>
手动实现事件修饰符
Svelte 5 移除了旧版的管道修饰符(on:click|preventDefault),改为在处理函数中显式调用。这样代码意图更明确:
<script lang="ts">
function preventDefault<T extends Event>(fn: (e: T) => void) {
return (e: T) => { e.preventDefault(); fn(e); };
}
function stopPropagation<T extends Event>(fn: (e: T) => void) {
return (e: T) => { e.stopPropagation(); fn(e); };
}
function handleSubmit() {
console.log('提交!');
}
</script>
<!-- 阻止默认行为 -->
<form onsubmit={preventDefault(handleSubmit)}>...</form>
<!-- 阻止冒泡 -->
<div onclick={stopPropagation(() => console.log('div'))}>
<button onclick={() => console.log('button')}>内部按钮</button>
</div>
<!-- once:只触发一次(原生 addEventListener 的 once 选项)-->
<button onclick={(e) => {
console.log('只执行一次');
e.target.replaceWith(e.target.cloneNode(true));
}}>只触发一次</button>
双向绑定 bind:
表单元素绑定
bind: 指令用于在响应式变量和 HTML 元素的属性之间建立双向绑定:变量变化时元素属性更新,元素属性变化时变量也自动更新。
<script lang="ts">
let name = $state('');
let age = $state(18);
let subscribed = $state(false);
let role = $state('user');
let bio = $state('');
let file = $state<File | null>(null);
</script>
<!-- 文本输入 -->
<input type="text" bind:value={name} placeholder="输入姓名" />
<p>Hello, {name || '陌生人'}!</p>
<!-- 数字输入(自动转换为 number 类型)-->
<input type="number" bind:value={age} min="0" max="120" />
<!-- 复选框 -->
<input type="checkbox" bind:checked={subscribed} />
<label>{subscribed ? '已订阅' : '未订阅'}</label>
<!-- 单选框 -->
<label><input type="radio" bind:group={role} value="user" /> 用户</label>
<label><input type="radio" bind:group={role} value="admin" /> 管理员</label>
<p>当前角色:{role}</p>
<!-- 下拉选择 -->
<select bind:value={role}>
<option value="user">普通用户</option>
<option value="editor">编辑者</option>
<option value="admin">管理员</option>
</select>
<!-- 多行文本 -->
<textarea bind:value={bio} rows="4"></textarea>
<!-- 文件输入 -->
<input type="file" bind:files={files}
onchange={() => console.log(files[0]?.name)} />
bind:this — 获取 DOM 引用
bind:this 允许你获取原生 DOM 元素或子组件实例的引用,类似于 React 的 useRef:
<script lang="ts">
let inputEl = $state<HTMLInputElement>();
let canvasEl = $state<HTMLCanvasElement>();
// 组件挂载后聚焦输入框
$effect(() => {
inputEl?.focus();
});
function drawOnCanvas() {
const ctx = canvasEl?.getContext('2d');
if (!ctx) return;
ctx.fillStyle = '#FF3E00';
ctx.fillRect(10, 10, 100, 100);
}
</script>
<input bind:this={inputEl} type="text" placeholder="自动聚焦" />
<canvas bind:this={canvasEl} width="200" height="200"></canvas>
<button onclick={drawOnCanvas}>画图</button>
bind:this 的注意事项
bind:this 绑定的引用在组件挂载(DOM 创建)后才有值,在 $effect() 内或用户交互时访问是安全的。不要在组件 script 顶层直接访问,那时 DOM 还未创建。
bind:this 高级应用:拖拽排序
<script lang="ts">
let containerEl = $state<HTMLElement>();
let items = $state([
{ id: 1, label: 'Item A' },
{ id: 2, label: 'Item B' },
{ id: 3, label: 'Item C' }
]);
let draggedId = $state<number | null>(null);
function handleDragStart(id: number) {
draggedId = id;
}
function handleDrop(targetId: number) {
if (draggedId === null || draggedId === targetId) return;
const fromIdx = items.findIndex(i => i.id === draggedId);
const toIdx = items.findIndex(i => i.id === targetId);
// 重排数组
const newItems = [...items];
const [moved] = newItems.splice(fromIdx, 1);
newItems.splice(toIdx, 0, moved);
items = newItems;
draggedId = null;
}
</script>
<ul bind:this={containerEl} class="sortable-list">
{#each items as item (item.id)}
<li
draggable="true"
class:dragging={draggedId === item.id}
ondragstart={() => handleDragStart(item.id)}
ondragover={(e) => e.preventDefault()}
ondrop={() => handleDrop(item.id)}
>
{item.label}
</li>
{/each}
</ul>
键盘事件与可访问性
处理键盘事件时需要同时照顾到鼠标用户和键盘用户,这是无障碍访问(Accessibility / a11y)的基本要求。
<script lang="ts">
let isOpen = $state(false);
function toggle() {
isOpen = !isOpen;
}
// 键盘处理:Enter 和空格键触发点击(模拟按钮行为)
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggle();
} else if (e.key === 'Escape' && isOpen) {
isOpen = false;
}
}
</script>
<!-- 可访问的自定义切换组件 -->
<div
role="button"
tabindex="0"
aria-expanded={isOpen}
aria-label="切换内容"
onclick={toggle}
onkeydown={handleKeydown}
>
{isOpen ? '▲ 收起' : '▼ 展开'}
</div>
{#if isOpen}
<div role="region">
折叠内容区域...
</div>
{/if}
<!-- 全局键盘监听:按 Escape 关闭弹窗 -->
<svelte:window
onkeydown={(e) => {
if (e.key === 'Escape') isOpen = false;
}}
/>
Svelte 的 a11y 警告
Svelte 编译器内置了无障碍访问检查:如果你在非交互元素(div、span)上添加 onclick 但没有 role 和 tabindex,编译器会发出 a11y-click-events-have-key-events 警告。遵循这些警告可以让你的应用对键盘用户和屏幕阅读器用户更友好。
Svelte 特殊元素事件
<!-- svelte:window:监听窗口事件(如滚动、大小变化)-->
<script lang="ts">
let scrollY = $state(0);
let windowWidth = $state(0);
let windowHeight = $state(0);
</script>
<!-- bind: 直接绑定窗口属性 -->
<svelte:window
bind:scrollY
bind:innerWidth={windowWidth}
bind:innerHeight={windowHeight}
onresize={() => console.log('窗口大小改变')}
/>
{#if scrollY > 200}
<button class="back-to-top" onclick={() => window.scrollTo(0, 0)}>↑ 回顶部</button>
{/if}
<!-- svelte:document:监听 document 事件 -->
<svelte:document
onvisibilitychange={() => {
if (document.hidden) console.log('页面不可见,暂停动画');
else console.log('页面可见,恢复动画');
}}
/>
<!-- svelte:body:监听 body 上的事件 -->
<svelte:body
onpointerenter={() => console.log('鼠标进入文档')}
/>
常见绑定场景汇总
| 元素/场景 | 绑定语法 | 说明 |
|---|---|---|
| 文本输入 | bind:value={str} | 绑定字符串值 |
| 数字输入 | bind:value={num} | 自动类型转换为 number |
| 复选框 | bind:checked={bool} | 绑定布尔值 |
| 多选复选框 | bind:group={arr} | 选中项加入数组 |
| 单选按钮 | bind:group={str} | 绑定选中的 value |
| 下拉选择 | bind:value={selected} | 绑定选中 option 的 value |
| 文件选择 | bind:files={fileList} | 绑定 FileList |
| contenteditable | bind:innerHTML={html} | 绑定富文本 HTML |
| 元素尺寸 | bind:clientWidth={w} | 响应式读取元素宽度 |
| 滚动位置 | bind:scrollY(svelte:window) | 监听页面滚动 |
| DOM 引用 | bind:this={el} | 获取 DOM 元素实例 |
本章小结
本章核心要点
- Svelte 5 改用原生 HTML 事件属性:
onclick、oninput、onkeydown取代了旧版on:click指令,与标准 HTML 完全一致,IDE 自动补全更好。 - 事件修饰符由函数实现:Svelte 5 移除了
on:click|preventDefault管道语法,改为在函数体内显式调用e.preventDefault()、e.stopPropagation(),逻辑更清晰。 - bind: 指令实现双向绑定:
bind:value绑定输入值、bind:checked绑定复选框、bind:group绑定单选/多选组、bind:files绑定文件选择。 - bind:this 获取 DOM 引用:在
$effect()内访问,因为 DOM 在组件挂载后才可用;可以操作 canvas、video、audio 等元素的原生 API。 - svelte:window/document/body 监听全局事件:可以用 bind: 直接绑定窗口属性(scrollY、innerWidth),无需手动 addEventListener/removeEventListener。
- 键盘事件须兼顾无障碍访问:非交互元素上的 onclick 需配合
role、tabindex和onkeydown;Svelte 编译器会发出 a11y 警告提醒。
组件级双向绑定
<!-- NumberInput.svelte -->
<script lang="ts">
// $bindable() 声明可被父组件双向绑定的 prop
let { value = $bindable(0), min = 0, max = 100 } = $props<{
value?: number;
min?: number;
max?: number;
}>();
</script>
<input type="number" bind:value {min} {max} />
<!-- 父组件:使用 bind: 双向绑定子组件 prop -->
<script>
import NumberInput from './NumberInput.svelte';
let quantity = $state(5);
</script>
<NumberInput bind:value={quantity} min={1} max={99} />
<p>数量:{quantity}</p>