Svelte 的 `<svelte:window>` 完全指南:窗口事件监听与全局状态双向绑定
<svelte:window> 是 Svelte 提供的特殊元素,它让你免写 addEventListener/removeEventListener 样板代码,直接对浏览器 window 对象挂载事件监听器,并通过 bind: 指令与窗口尺寸、滚动位置、网络状态等全局属性保持响应式同步。本文以 Svelte 官方文档 05-special-elements/02-svelte-window 为主体内容,结合编译器分析与客户端运行时源码,完整覆盖该元素的两种用法、可绑定属性清单、SSR 行为以及滚动绑定的无障碍设计细节。读完本篇,你可以直接在组件中使用 <svelte:window> 处理按键、窗口尺寸、滚动与在线状态,并理解其编译产物与运行时机制。
基本语法与定位
文档给出的基本形态有两种:
<svelte:window onevent={handler} />
<svelte:window bind:prop={value} />
它的核心价值在于:为 window 对象添加事件监听器时,不需要你手动在组件销毁时移除监听器,也不需要为服务端渲染(SSR)场景检查 window 是否存在——这些工作都由 Svelte 编译器与运行时接管。
与 <svelte:document>、<svelte:body>、<svelte:head> 等兄弟特殊元素一样,<svelte:window> 只能出现在组件的最顶层,不能放在任何块(block)或元素(element)内部。这一约束在编译器阶段就被强制校验:SvelteWindow 分析器 中调用 disallow_children(node) 禁止子节点,仓库中的 window-inside-block、window-inside-element 等测试样例正是针对“放进块/元素内部会报编译错误”这一行为做的固化验证。
事件监听:免清理的全局监听器
文档中的标准示例——监听全局按键并提示按下的键名:
<script>
function handleKeydown(event) {
alert(`pressed the ${event.key} key`);
}
</script>
<svelte:window onkeydown={handleKeydown} />
这里只需声明 onkeydown={handler},组件卸载时监听器自动移除。从源码看这一机制分两层:
- 编译期校验:SvelteWindow 分析器 遍历节点属性——凡
is_event_attribute识别出的事件属性交给check_global_event_reference做全局事件引用检查;而展开属性(SpreadAttribute)或普通属性则会直接抛出illegal_element_attribute编译错误。也就是说<svelte:window>只接受事件属性与窗口绑定,传任何其它自定义属性都会被编译器拒绝,这是防止误用的一道护栏。 - 代码生成:客户端转换阶段,SvelteWindow 转换器 将其交给通用特殊元素处理函数
visit_special_element(node, '$.window', context)——参数$.window表明该元素最终编译到内部命名空间下的window对象上,事件监听与绑定都以它为挂载目标。通用逻辑见 special_element.js:OnDirective(legacy 语法)与事件属性会被逐一访问并推入初始化语句。
可绑定属性一览
文档明确列出可以通过 bind: 绑定的 8 个属性:
| 属性 | 对应 window 属性 | 可写性 |
|---|---|---|
innerWidth |
window.innerWidth |
只读 |
innerHeight |
window.innerHeight |
只读 |
outerWidth |
window.outerWidth |
只读 |
outerHeight |
window.outerHeight |
只读 |
scrollX |
window.scrollX |
可写(双向) |
scrollY |
window.scrollY |
可写(双向) |
online |
window.navigator.onLine 的别名 |
只读 |
devicePixelRatio |
window.devicePixelRatio |
只读 |
除 scrollX 与 scrollY 外,其余均为只读绑定(单向同步)。
这些属性并非随意声明,而是在 bindings.js 的 binding_properties 表中逐项注册的。与 svelte:window 相关的条目均带有 valid_elements: ['svelte:window'] 与 omit_in_ssr: true 标记——前者限定这些绑定只能在 <svelte:window> 上生效,后者则解释了文档提到的“无需担心 SSR”:所有窗口绑定在服务端渲染输出中直接被省略,因为 window 在 Node 环境并不存在。
几个值得注意的注册细节:
scrollX/scrollY是唯一带有bidirectional: true的窗口绑定,这正对应文档中“可双向绑定”的说法;devicePixelRatio注册了event: 'resize',意味着它通过 resize 事件感知变化(例如用户缩放页面触发像素比变化);- 尺寸类绑定(
innerWidth等)在运行时监听resize,对应 window 绑定运行时 中的bind_window_size:listen(window, ['resize'], () => without_reactive_context(() => set(window[type])))——在resize回调中跳出响应式上下文再写入状态,避免监听器自身产生副作用回环。
滚动绑定:scrollX 与 scrollY 的特殊语义
scrollX/scrollY 是双向绑定,但 Svelte 对其做了刻意克制的实现。文档给出的用法:
<svelte:window bind:scrollY={y} />
并在文末特别提示(原文 NOTE):
页面不会在初始时滚动到绑定变量的初始值,以避免无障碍问题。只有后续对
scrollX/scrollY绑定变量的修改才会触发滚动。如果你有正当理由需要在组件渲染时滚动页面,请在$effect中调用scrollTo()。
这条提示并非文档空谈,window.js 运行时 中的 bind_window_scroll 函数逐行实现了这一约定:
- 初始值不回滚:
render_effect内用first标志跳过第一次执行(源码注释明确写着// Don't scroll to the initial value for accessibility reasons)。组件挂载时只“读取”当前滚动位置,不“写入”。 - 100ms 滚动防抖窗口:用户滚动触发
scroll事件后,scrolling标志被置真,并setTimeout(clear, 100)后清除。在该窗口内程序不会与用户滚动抢控制权;源码中也留了一条 TODO,考虑在scrollend事件得到普遍支持后替换该定时器。 - 监听器为 passive:
addEventListener('scroll', target_handler, { passive: true }),保证滚动绑定不阻塞滚动主线程。 - 初始位置补偿:由于浏览器在初始滚动位置不触发
scroll事件,实现末尾用effect(target_handler)主动执行一次同步,保证挂载后绑定变量立即拿到真实滚动值。 - 自动清理:
teardown(() => removeEventListener('scroll', target_handler))在组件销毁时移除监听器,兑现了“无需手动清理”的承诺。
因此当你程序化地修改 y(非滚动期间的 100ms 窗口内),render_effect 会调用 scrollTo(window.scrollX, latest_value) 完成向 DOM 的写回,形成真正的双向绑定。
典型应用场景与写法
结合上述机制,以下是可直接落地的常见用法。
窗口尺寸自适应布局:
<script>
let width = 0;
</script>
<svelte:window bind:innerWidth={width} />
<p>当前视口宽度:{width}px</p>
网络状态指示(online 为 navigator.onLine 别名):
<svelte:window bind:online />
{#if online}在线{:else}离线{/if}
需要初始滚动的场景:按文档建议,把滚动放进 $effect:
<script>
import { $effect } from 'svelte';
let y = 0;
$effect(() => {
scrollTo(0, y); // 组件渲染后主动滚动,符合无障碍预期
});
</script>
<svelte:window bind:scrollY={y} />
使用限制小结
- 位置:只能位于组件最顶层,不能嵌套在块或元素中(编译器强制校验);
- 属性:仅接受事件属性与窗口
bind:,其它属性/展开语法会触发illegal_element_attribute编译错误; - SSR:所有窗口绑定标记
omit_in_ssr,服务端渲染时自动省略,不会因window不存在而报错; - 只读 vs 双向:仅
scrollX/scrollY可写回,且首帧不回写滚动位置; - 与相邻元素的关系:
<svelte:document>(03-svelte-document)、<svelte:body>(04-svelte-body)、<svelte:head>(05-svelte-head)共享同一套“顶层专属”约束与特殊元素编译路径,可参照本篇的源码脉络类推。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00