响应式语句 #
一、副作用概述 #
副作用是指在状态变化时需要执行的额外操作,例如:
- DOM 操作
- 数据获取
- 订阅管理
- 定时器
- 日志记录
1.1 副作用类型 #
text
┌─────────────────────────────────────────────────────────────┐
│ 副作用类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 同步副作用 异步副作用 │
│ ├── DOM 操作 ├── 数据获取 │
│ ├── 日志记录 ├── 定时器 │
│ ├── 标题更新 ├── 事件订阅 │
│ └── 计算缓存 └── WebSocket │
│ │
└─────────────────────────────────────────────────────────────┘
二、Svelte 4 响应式语句 #
2.1 基本语法 #
svelte
<script>
let count = 0;
$: console.log('count changed:', count);
</script>
2.2 语句块 #
svelte
<script>
let count = 0;
$: {
console.log('count is', count);
document.title = `Count: ${count}`;
}
</script>
2.3 条件响应式 #
svelte
<script>
let count = 0;
$: if (count > 10) {
console.log('count exceeded 10');
}
</script>
2.4 多依赖追踪 #
svelte
<script>
let a = 1;
let b = 2;
$: sum = a + b;
$: console.log(`a=${a}, b=${b}, sum=${sum}`);
</script>
三、Svelte 5 $effect #
3.1 基本用法 #
svelte
<script>
let count = $state(0);
$effect(() => {
console.log('count changed:', count);
});
</script>
3.2 DOM 操作 #
svelte
<script>
let text = $state('');
$effect(() => {
document.title = text || 'My App';
});
</script>
<input bind:value={text} placeholder="输入标题" />
3.3 清理函数 #
svelte
<script>
let count = $state(0);
$effect(() => {
const timer = setInterval(() => {
console.log('tick', count);
}, 1000);
return () => {
clearInterval(timer);
console.log('cleanup');
};
});
</script>
3.4 依赖追踪 #
svelte
<script>
let a = $state(1);
let b = $state(2);
$effect(() => {
console.log(`a=${a}, b=${b}`);
});
</script>
四、$effect 高级用法 #
4.1 $effect.pre - DOM 更新前 #
svelte
<script>
let items = $state([]);
let listRef;
$effect.pre(() => {
if (listRef) {
console.log('DOM will update, items:', items.length);
}
});
</script>
<ul bind:this={listRef}>
{#each items as item}
<li>{item}</li>
{/each}
</ul>
4.2 控制依赖 #
svelte
<script>
let count = $state(0);
let logEnabled = $state(true);
$effect(() => {
if (logEnabled) {
console.log('count:', count);
}
});
</script>
4.3 避免无限循环 #
svelte
<script>
let count = $state(0);
$effect(() => {
if (count < 10) {
count += 1;
}
});
</script>
五、异步副作用 #
5.1 数据获取 #
svelte
<script>
let userId = $state(1);
let user = $state(null);
let loading = $state(false);
let error = $state(null);
$effect(() => {
let cancelled = false;
async function fetchUser() {
loading = true;
error = null;
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
user = data;
}
} catch (e) {
if (!cancelled) {
error = e.message;
}
} finally {
if (!cancelled) {
loading = false;
}
}
}
fetchUser();
return () => {
cancelled = true;
};
});
</script>
{#if loading}
<p>加载中...</p>
{:else if error}
<p class="error">{error}</p>
{:else if user}
<p>{user.name}</p>
{/if}
5.2 搜索防抖 #
svelte
<script>
let query = $state('');
let results = $state([]);
$effect(() => {
if (!query.trim()) {
results = [];
return;
}
const timer = setTimeout(async () => {
const response = await fetch(`/api/search?q=${query}`);
results = await response.json();
}, 300);
return () => clearTimeout(timer);
});
</script>
<input bind:value={query} placeholder="搜索..." />
<ul>
{#each results as result}
<li>{result.name}</li>
{/each}
</ul>
5.3 WebSocket 连接 #
svelte
<script>
let roomId = $state('general');
let messages = $state([]);
let connected = $state(false);
$effect(() => {
const ws = new WebSocket(`wss://example.com/rooms/${roomId}`);
ws.onopen = () => {
connected = true;
};
ws.onmessage = (event) => {
messages = [...messages, JSON.parse(event.data)];
};
ws.onclose = () => {
connected = false;
};
return () => {
ws.close();
};
});
</script>
<div class:connected class:disconnected={!connected}>
Room: {roomId} ({connected ? '已连接' : '未连接'})
</div>
<ul>
{#each messages as message}
<li>{message.text}</li>
{/each}
</ul>
六、事件订阅 #
6.1 窗口事件 #
svelte
<script>
let scrollY = $state(0);
let windowWidth = $state(0);
$effect(() => {
function handleScroll() {
scrollY = window.scrollY;
}
function handleResize() {
windowWidth = window.innerWidth;
}
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize);
handleResize();
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
};
});
</script>
<p>Scroll: {scrollY}px</p>
<p>Width: {windowWidth}px</p>
6.2 键盘事件 #
svelte
<script>
let lastKey = $state('');
let keys = $state([]);
$effect(() => {
function handleKeyDown(e) {
lastKey = e.key;
keys = [...keys, e.key].slice(-10);
}
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
});
</script>
<p>Last key: {lastKey}</p>
<p>Recent keys: {keys.join(', ')}</p>
七、定时器管理 #
7.1 setInterval #
svelte
<script>
let seconds = $state(0);
let running = $state(false);
$effect(() => {
if (!running) return;
const timer = setInterval(() => {
seconds += 1;
}, 1000);
return () => clearInterval(timer);
});
</script>
<p>{seconds} 秒</p>
<button onclick={() => running = !running}>
{running ? '暂停' : '开始'}
</button>
7.2 倒计时 #
svelte
<script>
let countdown = $state(60);
let running = $state(false);
$effect(() => {
if (!running || countdown <= 0) return;
const timer = setInterval(() => {
countdown -= 1;
}, 1000);
return () => clearInterval(timer);
});
function reset() {
countdown = 60;
running = false;
}
</script>
<p>{countdown}</p>
<button onclick={() => running = true} disabled={running || countdown <= 0}>
开始
</button>
<button onclick={reset}>重置</button>
八、实际应用示例 #
8.1 本地存储同步 #
svelte
<script>
let STORAGE_KEY = 'my-app-state';
let todos = $state([]);
$effect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
todos = JSON.parse(saved);
}
});
$effect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
});
function addTodo(text) {
todos = [...todos, { id: Date.now(), text, done: false }];
}
function toggleTodo(id) {
todos = todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
</script>
<input
onkeydown={(e) => {
if (e.key === 'Enter') {
addTodo(e.target.value);
e.target.value = '';
}
}}
/>
<ul>
{#each todos as todo}
<li>
<input type="checkbox" checked={todo.done} onchange={() => toggleTodo(todo.id)} />
<span class:done={todo.done}>{todo.text}</span>
</li>
{/each}
</ul>
<style>
.done {
text-decoration: line-through;
color: #999;
}
</style>
8.2 表单验证 #
svelte
<script>
let email = $state('');
let password = $state('');
let errors = $state({});
let touched = $state({});
$effect(() => {
const newErrors = {};
if (touched.email) {
if (!email) {
newErrors.email = '邮箱不能为空';
} else if (!email.includes('@')) {
newErrors.email = '邮箱格式不正确';
}
}
if (touched.password) {
if (!password) {
newErrors.password = '密码不能为空';
} else if (password.length < 6) {
newErrors.password = '密码至少6位';
}
}
errors = newErrors;
});
function handleSubmit(e) {
e.preventDefault();
touched = { email: true, password: true };
if (Object.keys(errors).length === 0) {
console.log('submit', { email, password });
}
}
</script>
<form onsubmit={handleSubmit}>
<div>
<input
type="email"
bind:value={email}
onblur={() => touched = { ...touched, email: true }}
placeholder="邮箱"
/>
{#if errors.email}
<span class="error">{errors.email}</span>
{/if}
</div>
<div>
<input
type="password"
bind:value={password}
onblur={() => touched = { ...touched, password: true }}
placeholder="密码"
/>
{#if errors.password}
<span class="error">{errors.password}</span>
{/if}
</div>
<button type="submit">提交</button>
</form>
<style>
.error {
color: red;
font-size: 0.8rem;
margin-left: 0.5rem;
}
</style>
九、最佳实践 #
9.1 副作用原则 #
text
✅ 推荐做法
├── 在 $effect 中处理副作用
├── 返回清理函数
├── 避免在派生状态中产生副作用
└── 使用 $effect.pre 处理 DOM 更新前逻辑
❌ 避免做法
├── 在派生状态中修改其他状态
├── 忘记清理定时器和订阅
├── 创建无限循环
└── 在渲染函数中执行副作用
9.2 性能优化 #
svelte
<script>
let data = $state([]);
let filter = $state('');
let filtered = $derived(data.filter(item =>
item.name.includes(filter)
));
$effect(() => {
console.log('filtered changed:', filtered.length);
});
</script>
十、总结 #
| API | 用途 |
|---|---|
$: |
Svelte 4 响应式语句 |
$effect |
Svelte 5 副作用处理 |
$effect.pre |
DOM 更新前执行 |
| 清理函数 | 返回函数在下次执行前或组件销毁时调用 |
副作用处理要点:
- 使用
$effect处理副作用 - 返回清理函数释放资源
- 避免在派生状态中产生副作用
- 合理控制依赖追踪
最后更新:2026-03-28