响应式语句 #

一、副作用概述 #

副作用是指在状态变化时需要执行的额外操作,例如:

  • 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