Svelte `bind:` 指令详解:双向数据绑定的完整语法、内置绑定类型与源码级实现
本篇基于 Svelte 官方模板语法文档 12-bind.md 展开,系统讲解 bind: 指令的通用语法、函数式绑定、各类内置元素绑定(表单、媒体、尺寸、contenteditable、bind:this)以及组件 prop 绑定($bindable)。读完本文,你不仅能掌握所有 bind: 变体的写法与边界条件(数值强制转换、表单重置行为、FileList 限制等),还能理解编译器如何校验绑定、运行时如何生成 $.bind_* 调用,从而在生产代码中正确编写表单与媒体绑定。
基本语法:数据从子流向父
Svelte 中数据通常自上而下流动(父组件 → 子组件)。bind: 指令让数据可以反向流动(子 → 父)。
通用语法为 bind:property={expression},其中 expression 必须是一个 lvalue(可赋值表达式,即变量或对象属性)。当表达式是与属性同名的标识符时,可以省略表达式——以下两种写法等价:
<input bind:value={value} />
<input bind:value />
Svelte 会为该元素创建一个事件监听器来更新被绑定的值。如果元素上已经存在同一事件的监听器,那个监听器会在绑定值更新之前先被触发。
大多数绑定是双向的(two-way):改变绑定的值会反映到元素上,反之亦然。少数绑定是只读的(readonly):改变绑定的值不会对元素产生任何效果(例如尺寸、媒体进度类属性)。
函数绑定:bind:property={get, set}
除了普通表达式,还可以使用 bind:property={get, set} 的形式,其中 get、set 都是函数,从而在读写时执行校验或转换:
<input bind:value={
() => value,
(v) => value = v.toLowerCase()}
/>
对于只读绑定(例如尺寸绑定),get 必须写成 null:
<div
bind:clientWidth={null, redraw}
bind:clientHeight={null, redraw}
>...</div>
注意:函数绑定从 Svelte 5.9.0 起可用。
从源码结构看,编译器把 bind:property={get, set} 解析为 JS 的 SequenceExpression。分析阶段在 BindDirective.js 中处理:要求表达式恰好包含两个子表达式,禁止出现 await,并且 bind:group 不支持函数绑定形式(会直接报 bind_group_invalid_expression 错误)。
<input bind:value>
<input> 上的 bind:value 绑定其 value 属性:
<script>
let message = $state('hello');
</script>
<input bind:value={message} />
<p>{message}</p>
对于数值型输入(type="number" 或 type="range"),值会被强制转换为数字:
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
</script>
<label>
<input type="number" bind:value={a} min="0" max="10" />
<input type="range" bind:value={a} min="0" max="10" />
</label>
<label>
<input type="number" bind:value={b} min="0" max="10" />
<input type="range" bind:value={b} min="0" max="10" />
</label>
<p>{a} + {b} = {a + b}</p>
输入为空或非法(type="number" 的情况下)时,绑定值为 undefined。这一点在运行时源码中可以直接印证:input.js 中的 bind_value 会监听 input 事件,对 numberlike 输入执行 to_number 转换,随后通过 render_effect 把状态值写回 input.value。
自 5.6.0 起,如果 <input> 带有 defaultValue 且位于表单内,表单重置时会恢复到该值而不是空字符串。初始渲染时,只要绑定值不是 null/undefined,绑定值优先于 defaultValue:
<script>
let value = $state('');
</script>
<form>
<input bind:value defaultValue="not the empty string">
<input type="reset" value="Reset">
</form>
这个行为的运行时依据在 bind_value 的事件回调中:var value = is_reset ? input.defaultValue : input.value(见 input.js)。表单重置与 defaultValue 的行为有专门测试覆盖,可参考 bindings-form-reset 测试 与 form-default-value 测试。
注意:请谨慎使用 reset 按钮,并确保用户不会在尝试提交表单时误点它。
<input bind:checked>
复选框可用 bind:checked 绑定:
<label>
<input type="checkbox" bind:checked={accepted} />
Accept terms and conditions
</label>
自 5.6.0 起,带 defaultChecked 属性且位于表单内的 <input> 在表单重置时会恢复到该值而不是 false。初始渲染时,只要绑定值不是 null/undefined,绑定值优先:
<script>
let checked = $state(true);
</script>
<form>
<input type="checkbox" bind:checked defaultChecked={true}>
<input type="reset" value="Reset">
</form>
注意:单选按钮(radio)应使用
bind:group而不是bind:checked。这一点在编译器分析阶段就会被强制校验:在 BindDirective.js 中,<input type="radio">上使用bind:checked会直接抛出编译错误并提示改用bind:group。
<input bind:indeterminate>
复选框可以处于独立于勾选/未勾选的**半选(indeterminate)**状态:
<script>
let checked = $state(false);
let indeterminate = $state(true);
</script>
<form>
<input type="checkbox" bind:checked bind:indeterminate>
{#if indeterminate}
waiting...
{:else if checked}
checked
{:else}
unchecked
{/if}
</form>
在编译器绑定注册表 bindings.js 中,indeterminate 被标记为 bidirectional: true、仅适用于 input,且监听的事件是 change(而非 input),这解释了为什么半选状态切换与勾选状态更新节奏一致。
<input bind:group>
一组协同工作的输入可以使用 bind:group:
<!--- file: App.svelte --->
<script>
let tortilla = $state('Plain');
/** @type {string[]} */
let fillings = $state([]);
</script>
<h1>Customize your burrito</h1>
<!-- grouped radio inputs are mutually exclusive -->
<label><input type="radio" bind:group={tortilla} value="Plain" /> Plain</label>
<label><input type="radio" bind:group={tortilla} value="Whole wheat" /> Whole wheat</label>
<label><input type="radio" bind:group={tortilla} value="Spinach" /> Spinach</label>
<!-- grouped checkbox inputs populate an array -->
<label><input type="checkbox" bind:group={fillings} value="Rice" /> Rice</label>
<label><input type="checkbox" bind:group={fillings} value="Beans" /> Beans</label>
<label><input type="checkbox" bind:group={fillings} value="Cheese" /> Cheese</label>
<label><input type="checkbox" bind:group={fillings} value="Guac (extra)" /> Guac (extra)</label>
<p>Tortilla: {tortilla}</p>
<p>Fillings: {fillings.join(', ') || 'None'}</p>
<style>
label {
display: block;
}
</style>
注意:
bind:group只在输入位于同一个 Svelte 组件内时有效。
源码层面,bind:group 的分组逻辑在 BindDirective.js 中实现:编译器向上遍历 AST 找出引用了绑定标识符的 EachBlock,并按“相同标识符序列”把多个输入划入同一个 binding_group;运行时由 input.js 中的 bind_group 函数把每个 <input> 推入组内数组,单选输入取 input.__value,多选(checkbox)则通过 get_binding_group_value 生成数组。相关测试可见 state-bind-group 测试。
<input bind:files>
对于 type="file" 的 <input>,可以用 bind:files 获取所选文件的 FileList。若要编程式更新文件,必须传入 FileList 对象;由于 FileList 目前无法直接构造,需要创建一个新的 DataTransfer 对象并取其 files:
<script>
let files = $state();
function clear() {
files = new DataTransfer().files; // null or undefined does not work
}
</script>
<label for="avatar">Upload a picture:</label>
<input accept="image/png, image/jpeg" bind:files id="avatar" name="avatar" type="file" />
<button onclick={clear}>clear</button>
FileList 对象也不可变。例如要删除列表中的单个文件,需要新建 DataTransfer 并只添加你想保留的文件。
注意:
DataTransfer在某些服务端 JS 运行时中不可用。把绑定files的状态保持未初始化,可以防止组件在服务端渲染(SSR)时产生潜在错误。这与编译器注册表中files绑定带有omit_in_ssr: true标记(见 bindings.js)相一致。
<select bind:value>
<select> 的 value 绑定对应于被选中 <option> 的 value 属性,它可以是任意值(不像常规 DOM 只能是字符串):
<select bind:value={selected}>
<option value={a}>a</option>
<option value={b}>b</option>
<option value={c}>c</option>
</select>
<select multiple> 的行为类似一组复选框:绑定变量是一个数组,每个被选中 <option> 的 value 对应一个条目:
<select multiple bind:value={fillings}>
<option value="Rice">Rice</option>
<option value="Beans">Beans</option>
<option value="Cheese">Cheese</option>
<option value="Guac (extra)">Guac (extra)</option>
</select>
当 <option> 的 value 与其文本内容一致时,value 属性可以省略:
<select multiple bind:value={fillings}>
<option>Rice</option>
<option>Beans</option>
<option>Cheese</option>
<option>Guac (extra)</option>
</select>
可以给 <select> 设置默认值:在应当被初始选中的 <option>(multiple 时可以是多个)上加 selected 属性。如果 <select> 位于表单内,表单重置时会恢复到该选中状态。初始渲染时,只要绑定值不是 undefined,绑定值优先:
<select bind:value={selected}>
<option value={a}>a</option>
<option value={b} selected>b</option>
<option value={c}>c</option>
</select>
自 5.57.0 起,带 defaultValue 且位于表单内的 <select> 在表单重置时会恢复到该值而不是空字符串。初始渲染时,只要绑定值不是 null/undefined,绑定值优先:
<form>
<select bind:value defaultValue="b">
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<input type="reset" value="Reset">
</form>
<select> 的绑定由 select.js 中的 bind_select_value 实现,它专门处理“非字符串 value”这类原生 <select> 无法表达的场景。
<audio> 与 <video>
<audio> 元素有一组专属绑定——五个双向绑定:
currentTimeplaybackRatepausedvolumemuted
以及一组只读绑定:
durationbufferedseekableseekingendedreadyStateplayed
<audio src={clip} bind:duration bind:currentTime bind:paused></audio>
<video> 元素拥有与 <audio> 完全相同的绑定,另加只读的 videoWidth 与 videoHeight 两个绑定。
这些属性在 bindings.js 中均有注册:双向项带 bidirectional: true,媒体事件(durationchange、resize 等)由 event 字段声明,统一由编译器生成形如 $.bind_current_time、$.bind_paused、$.bind_volume 的运行时调用(见 3-transform/client/visitors/BindDirective.js),并全部标记 omit_in_ssr: true,即服务端渲染时不生效。
<img>
<img> 元素有两个只读绑定:
naturalWidthnaturalHeight
从注册表可见(bindings.js),二者绑定到 load 事件上——即图片加载完成后触发更新,仅适用于 img 元素。
<details bind:open>
<details> 元素支持对 open 属性绑定:
<details bind:open={isOpen}>
<summary>How do you comfort a JavaScript bug?</summary>
<p>You console it.</p>
</details>
注册表中 open 对应的事件是 toggle(bindings.js),且为双向绑定,只适用于 details 元素。
window 与 document
要绑定 window 和 document 的属性,请使用 <svelte:window> 与 <svelte:document> 组件(例如 scrollX、innerWidth、online、activeElement、visibilityState 等均在 bindings.js 中限定为仅适用于这两个特殊元素)。
Contenteditable 绑定
带 contenteditable 属性的元素支持以下绑定:
innerHTMLinnerTexttextContent
注意:
innerText与textContent之间存在细微差别(前者参与可见性/渲染模型,后者直接反映 DOM 文本)。
<div contenteditable="true" bind:innerHTML={html}></div>
这类绑定受严格校验:若元素没有 contenteditable 属性,分析阶段会抛出 attribute_contenteditable_missing 错误;若 contenteditable 是动态表达式,则抛出 attribute_contenteditable_dynamic(见 BindDirective.js)。运行时则由 $.bind_content_editable 统一处理三种属性。
尺寸绑定(Dimensions)
所有可见元素都支持以下只读绑定,底层使用 ResizeObserver 测量:
clientWidthclientHeightoffsetWidthoffsetHeightcontentRectcontentBoxSizeborderBoxSizedevicePixelContentBoxSize
<div bind:offsetWidth={width} bind:offsetHeight={height}>
<Chart {width} {height} />
</div>
注意:
display: inline元素没有宽高(“固有尺寸”元素如<img>、<canvas>除外),无法被ResizeObserver观察。需要将其display改为inline-block等。另外 CSS 变换(transform)不会触发ResizeObserver回调。
从源码结构看,尺寸类绑定分两条路径生成:contentRect 等 ResizeObserverEntry 字段走 $.bind_resize_observer,而 clientWidth/offsetHeight 等 CSSOM 数值走 $.bind_element_size(见 3-transform/client/visitors/BindDirective.js)。另外编译器还有一条针对性规则:SVG 元素不允许使用 bind:offsetWidth,会提示改用 bind:clientWidth(BindDirective.js)。
bind:this:获取 DOM 节点或组件实例引用
<!--- copy: false --->
bind:this={dom_node}
要获取 DOM 节点引用,使用 bind:this。组件挂载之前该值为 undefined——也就是说,你应该在effect 或事件处理器中读取它,而不是在组件初始化期间读取:
<script>
/** @type {HTMLCanvasElement} */
let canvas;
$effect(() => {
const ctx = canvas.getContext('2d');
drawStuff(ctx);
});
</script>
<canvas bind:this={canvas}></canvas>
组件同样支持 bind:this,允许你以编程方式操作组件实例:
<!--- file: App.svelte --->
<ShoppingCart bind:this={cart} />
<button onclick={() => cart.empty()}> Empty shopping cart </button>
<!--- file: ShoppingCart.svelte --->
<script>
// All instance exports are available on the instance object
export function empty() {
// ...
}
</script>
注意:当结合函数绑定使用时,getter 是必需的,这样才能保证组件或元素销毁时正确的值被置空。
在编译器转换阶段,bind:this 是特殊路径:它走 build_bind_this,且语句被推入 init 阶段(先于渲染),因为它是一向绑定、可能影响渲染 effect;其余绑定则推入 after_update 阶段,确保绑定发生在属性更新之后、与事件/action 保持顺序(见 3-transform/client/visitors/BindDirective.js)。运行时实现位于 this.js。
组件的 bind:property:借助 $bindable 的 prop 绑定
bind:property={variable}
组件的 prop 可以用与元素完全相同的语法进行绑定:
<Keypad bind:value={pin} />
虽然 Svelte 的 prop 本身是响应式的,但默认情况下这种响应性只向下流入组件。使用 bind:property 可以让组件内部对该属性的修改回流到父组件。
要声明一个属性可绑定,使用 $bindable rune:
<script>
let { readonlyProperty, bindableProperty = $bindable() } = $props();
</script>
声明为 bindable 表示该属性可以用 bind: 绑定,而不是必须用 bind: 绑定。
可绑定属性可以带回退值(fallback value):
<script>
let { bindableProperty = $bindable('fallback value') } = $props();
</script>
这个回退值只在属性未被绑定时生效。当属性被绑定且存在回退值时,父组件必须提供非 undefined 的值,否则会抛出运行时错误——这避免了“到底该用哪个值”这类难以推理的场景。
分析阶段同样会校验被绑定标识符的“可写性”:只有 state、raw_state、prop、bindable_prop、each、store_sub 等类型的绑定(或已被更新过的绑定)才能作为绑定目标,否则报 bind_invalid_value(见 BindDirective.js)。
小结:从语法到代码生成的一条链路
综合文档与源码,Svelte 的 bind: 机制可以概括为三层:
- 注册表层:packages/svelte/src/compiler/phases/bindings.js 以
binding_properties表集中声明每个绑定名的适用元素(valid_elements/invalid_elements)、驱动事件(event)、是否双向(bidirectional)、是否在 SSR 中省略(omit_in_ssr)。文档中所有“只读/双向”“仅某元素可用”的描述都可以在此表中一一对应。 - 分析与转换层:2-analyze/visitors/BindDirective.js 负责校验(元素与绑定名的匹配、
type="file"检查、contenteditable 要求、bind:group分组、函数绑定语法);3-transform/client/visitors/BindDirective.js 负责把bind:*翻译为$.bind_value、$.bind_group、$.bind_select_value等运行时调用,并按init/after_update阶段安排执行顺序。 - 运行时层:packages/svelte/src/internal/client/dom/elements/bindings/ 目录下的
input.js、select.js、media.js、size.js、window.js、document.js等文件实现了各类绑定的双向同步逻辑(如bind_value中“聚焦中的输入不被重写”“数值输入的 0/00 相等处理”“表单重置时回落到defaultValue”等细节),并有 runtime-runes 测试集 中的bindings-form-reset、form-default-value、state-bind-group等样例持续验证这些行为。
掌握这张“文档 → 编译器 → 运行时”的对应关系后,遇到绑定不生效、SSR 报 DataTransfer 不存在、或表单重置行为不符合预期等问题时,就能快速定位到具体的实现与测试代码进行排查。
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 StartedRust0623
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