$effect
效果是让你的应用执行操作的原因。当 Svelte 运行效果函数时,它会跟踪访问了哪些状态(和派生状态)(除非在 untrack
内部访问),并在该状态稍后发生变化时重新运行该函数。
¥Effects are what make your application do things. When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside untrack
), and re-runs the function when that state later changes.
Svelte 应用中的大多数效果都是由 Svelte 本身创建的 - 例如,它们是当 name
更改时更新 <h1>hello {name}!</h1>
中的文本的位。
¥Most of the effects in a Svelte app are created by Svelte itself — they’re the bits that update the text in <h1>hello {name}!</h1>
when name
changes, for example.
但你也可以使用 $effect
符文创建自己的效果,这在你需要将外部系统(无论是库、<canvas>
元素还是网络上的某些东西)与 Svelte 应用内部的状态同步时非常有用。
¥But you can also create your own effects with the $effect
rune, which is useful when you need to synchronize an external system (whether that’s a library, or a <canvas>
element, or something across a network) with state inside your Svelte app.
避免过度使用
$effect
!当你在效果中做太多工作时,代码通常会变得难以理解和维护。有关替代方法,请参阅 何时不使用$effect
。¥[!NOTE] Avoid overusing
$effect
! When you do too much work in effects, code often becomes difficult to understand and maintain. See when not to use$effect
to learn about alternative approaches.
你的效果在组件安装到 DOM 后运行,并在状态更改(demo)后的 microtask 中运行:
¥Your effects run after the component has been mounted to the DOM, and in a microtask after state changes (demo):
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// this will re-run whenever `color` or `size` change
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
重新运行是分批的(即在同一时刻更改 color
和 size
不会导致两次单独的运行),并且在应用任何 DOM 更新后发生。
¥Re-runs are batched (i.e. changing color
and size
in the same moment won’t cause two separate runs), and happen after any DOM updates have been applied.
你可以将 $effect
放置在任何地方,而不仅仅是组件的顶层,只要它在组件初始化期间调用(或在父效果处于活动状态时)。然后,它与组件(或父效果)的生命周期绑定在一起,因此当组件卸载(或父效果被销毁)时,它将自行销毁。
¥You can place $effect
anywhere, not just at the top level of a component, as long as it is called during component initialization (or while a parent effect is active). It is then tied to the lifecycle of the component (or parent effect) and will therefore destroy itself when the component unmounts (or the parent effect is destroyed).
你可以从 $effect
返回一个函数,该函数将在效果重新运行之前以及在被销毁之前立即运行(demo)。
¥You can return a function from $effect
, which will run immediately before the effect re-runs, and before it is destroyed (demo).
<script>
let count = $state(0);
let milliseconds = $state(1000);
$effect(() => {
// This will be recreated whenever `milliseconds` changes
const interval = setInterval(() => {
count += 1;
}, milliseconds);
return () => {
// if a callback is provided, it will run
// a) immediately before the effect re-runs
// b) when the component is destroyed
clearInterval(interval);
};
});
</script>
<h1>{count}</h1>
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>
了解依赖(Understanding dependencies)
¥Understanding dependencies
$effect
会自动获取在其函数主体内同步读取的任何反应值($state
、$derived
、$props
)并将它们注册为依赖。当这些依赖发生变化时,$effect
会安排重新运行。
¥$effect
automatically picks up any reactive values ($state
, $derived
, $props
) that are synchronously read inside its function body and registers them as dependencies. When those dependencies change, the $effect
schedules a rerun.
异步读取的值(例如在 await
之后或在 setTimeout
内部)将不会被跟踪。在这里,当 color
更改时,画布将被重新绘制,但当 size
更改(demo)时则不会:
¥Values that are read asynchronously — after an await
or inside a setTimeout
, for example — will not be tracked. Here, the canvas will be repainted when color
changes, but not when size
changes (demo):
function $effect(fn: () => void | (() => void)): void
namespace $effect
Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. $state
or $derived
values.
The timing of the execution is after the DOM has been updated.
Example:
$effect(() => console.log('The count is now ' + count));
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
Does not run during server side rendering.
$effect(() => {
const const context: CanvasRenderingContext2D
context = let canvas: {
width: number;
height: number;
getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
}
canvas.function getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D
getContext('2d');
const context: CanvasRenderingContext2D
context.CanvasRect.clearRect(x: number, y: number, w: number, h: number): void
clearRect(0, 0, let canvas: {
width: number;
height: number;
getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
}
canvas.width: number
width, let canvas: {
width: number;
height: number;
getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
}
canvas.height: number
height);
// this will re-run whenever `color` changes...
const context: CanvasRenderingContext2D
context.CanvasFillStrokeStyles.fillStyle: string | CanvasGradient | CanvasPattern
fillStyle = let color: string
color;
function setTimeout<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+2 overloads)
Schedules execution of a one-time callback
after delay
milliseconds.
The callback
will likely not be invoked in precisely delay
milliseconds.
Node.js makes no guarantees about the exact timing of when callbacks will fire,
nor of their ordering. The callback will be called as close as possible to the
time specified.
When delay
is larger than 2147483647
or less than 1
, the delay
will be set to 1
. Non-integer delays are truncated to an integer.
If callback
is not a function, a TypeError
will be thrown.
This method has a custom variant for promises that is available using timersPromises.setTimeout()
.
setTimeout(() => {
// ...but not when `size` changes
const context: CanvasRenderingContext2D
context.CanvasRect.fillRect(x: number, y: number, w: number, h: number): void
fillRect(0, 0, let size: number
size, let size: number
size);
}, 0);
});
效果仅在其读取的对象发生变化时重新运行,而不是在其内部的属性发生变化时重新运行。(如果你想在开发时观察对象内部的变化,你可以使用 $inspect
。)
¥An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes inside an object at dev time, you can use $inspect
.)
<script>
let state = $state({ value: 0 });
let derived = $derived({ value: state.value * 2 });
// this will run once, because `state` is never reassigned (only mutated)
$effect(() => {
state;
});
// this will run whenever `state.value` changes...
$effect(() => {
state.value;
});
// ...and so will this, because `derived` is a new object each time
$effect(() => {
derived;
});
</script>
<button onclick={() => (state.value += 1)}>
{state.value}
</button>
<p>{state.value} doubled is {derived.value}</p>
效果仅取决于它上次运行时读取的值。这对于具有条件代码的效果具有有趣的含义。
¥An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code.
例如,如果下面代码片段中的 a
是 true
,则 if
块内的代码将运行并评估 b
。因此,更改为 a
或 b
将导致效果重新运行。
¥For instance, if a
is true
in the code snippet below, the code inside the if
block will run and b
will be evaluated. As such, changes to either a
or b
will cause the effect to re-run.
相反,如果 a
是 false
,则不会评估 b
,并且只有当 a
更改时才会重新运行效果。
¥Conversely, if a
is false
, b
will not be evaluated, and the effect will only re-run when a
changes.
function $effect(fn: () => void | (() => void)): void
namespace $effect
Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. $state
or $derived
values.
The timing of the execution is after the DOM has been updated.
Example:
$effect(() => console.log('The count is now ' + count));
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
Does not run during server side rendering.
$effect(() => {
var console: Console
The console
module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.
- A global
console
instance configured to write to process.stdout
and
process.stderr
. The global console
can be used without calling require('console')
.
Warning: The global console object’s methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O
for
more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout
with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
log('running');
if (let a: false
a) {
var console: Console
The console
module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.
- A global
console
instance configured to write to process.stdout
and
process.stderr
. The global console
can be used without calling require('console')
.
Warning: The global console object’s methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O
for
more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout
with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
log('b:', let b: false
b);
}
});
$effect.pre
在极少数情况下,你可能需要在 DOM 更新之前运行代码。为此,我们可以使用 $effect.pre
符文:
¥In rare cases, you may need to run code before the DOM updates. For this we can use the $effect.pre
rune:
<script>
import { tick } from 'svelte';
let div = $state();
let messages = $state([]);
// ...
$effect.pre(() => {
if (!div) return; // not yet mounted
// reference `messages` array length so that this code re-runs whenever it changes
messages.length;
// autoscroll when new messages are added
if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) {
tick().then(() => {
div.scrollTo(0, div.scrollHeight);
});
}
});
</script>
<div bind:this={div}>
{#each messages as message}
<p>{message}</p>
{/each}
</div>
除了时间之外,$effect.pre
的工作原理与 $effect
完全相同。
¥Apart from the timing, $effect.pre
works exactly like $effect
.
$effect.tracking
$effect.tracking
符文是一项高级功能,可告诉你代码是否在跟踪上下文中运行,例如效果或模板内 (demo):
¥The $effect.tracking
rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template (demo):
<script>
console.log('in component setup:', $effect.tracking()); // false
$effect(() => {
console.log('in effect:', $effect.tracking()); // true
});
</script>
<p>in template: {$effect.tracking()}</p> <!-- true -->
它用于实现像 createSubscriber
这样的抽象,它将创建监听器来更新反应值,但前提是这些值正在被跟踪(而不是例如在事件处理程序中读取)。
¥It is used to implement abstractions like createSubscriber
, which will create listeners to update reactive values but only if those values are being tracked (rather than, for example, read inside an event handler).
$effect.root
$effect.root
符文是一项高级功能,可创建不会自动清理的非跟踪范围。这对于你想要手动控制的嵌套效果很有用。此符文还允许在组件初始化阶段之外创建效果。
¥The $effect.root
rune is an advanced feature that creates a non-tracked scope that doesn’t auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase.
<script>
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
console.log(count);
});
return () => {
console.log('effect root cleanup');
};
});
</script>
何时不使用 $effect(When not to use $effect)
¥When not to use $effect
一般来说,$effect
最好被视为一种应急方案 - 对于分析和直接 DOM 操作等有用 - 而不是你应该经常使用的工具。特别是,避免使用它来同步状态。而不是这样...
¥In general, $effect
is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this...
<script>
let count = $state(0);
let doubled = $state();
// don't do this!
$effect(() => {
doubled = count * 2;
});
</script>
...执行以下操作:
¥...do this:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
对于比
count * 2
等简单表达式更复杂的事情,你也可以使用$derived.by
。¥[!NOTE] For things that are more complicated than a simple expression like
count * 2
, you can also use$derived.by
.
你可能想做一些复杂的效果来将一个值链接到另一个值。以下示例显示了 “多页应用” 和 “花费金额” 的两个相互连接的输入。如果你更新一个,另一个应该相应更新。不要为此(demo)使用效果:
¥You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for “money spent” and “money left” that are connected to each other. If you update one, the other should update accordingly. Don’t use effects for this (demo):
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
$effect(() => {
left = total - spent;
});
$effect(() => {
spent = total - left;
});
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
相反,尽可能使用回调(demo):
¥Instead, use callbacks where possible (demo):
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(e) {
spent = +e.target.value;
left = total - spent;
}
function updateLeft(e) {
left = +e.target.value;
spent = total - left;
}
</script>
<label>
<input type="range" value={spent} oninput={updateSpent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" value={left} oninput={updateLeft} max={total} />
{left}/{total} left
</label>
如果你出于某种原因需要使用绑定(例如当你想要某种“可写 $derived
”时),请考虑使用 getter 和 setter 来同步状态(demo):
¥If you need to use bindings, for whatever reason (for example when you want some kind of “writable $derived
”), consider using getters and setters to synchronise state (demo):
<script>
let total = 100;
let spent = $state(0);
let left = {
get value() {
return total - spent;
},
set value(v) {
spent = total - v;
}
};
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left.value} max={total} />
{left.value}/{total} left
</label>
如果你绝对必须在效果内更新 $state
并因为你读取和写入相同的 $state
而陷入无限循环,请使用 untrack。
¥If you absolutely have to update $state
within an effect and run into an infinite loop because you read and write to the same $state
, use untrack.