Effect副作用 #

一、createEffect 基础 #

1.1 什么是 Effect #

Effect 是 Solid 的副作用原语,用于在响应式状态变化时执行副作用操作。

jsx
import { createSignal, createEffect } from 'solid-js';

const [count, setCount] = createSignal(0);

// 创建副作用
createEffect(() => {
  console.log('Count changed:', count());
});

setCount(1); // 自动执行 Effect

1.2 Effect 结构 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Effect 工作原理                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 首次执行                                               │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ createEffect(() => {                                │  │
│   │   console.log(count()); // 读取 Signal              │  │
│   │ })                                                  │  │
│   └─────────────────────────────────────────────────────┘  │
│                         ↓                                   │
│   2. 收集依赖                                               │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ count() 被读取 → 添加到依赖列表                       │  │
│   └─────────────────────────────────────────────────────┘  │
│                         ↓                                   │
│   3. 依赖变化时重新执行                                     │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ setCount(1) → count 变化 → 重新执行 Effect          │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、基本用法 #

2.1 简单副作用 #

jsx
function Counter() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    document.title = `Count: ${count()}`;
  });

  return (
    <div>
      <p>Count: {count()}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

2.2 多依赖 #

jsx
function UserProfile() {
  const [firstName, setFirstName] = createSignal('John');
  const [lastName, setLastName] = createSignal('Doe');

  createEffect(() => {
    console.log(`User: ${firstName()} ${lastName()}`);
  });

  // firstName 或 lastName 变化都会触发 Effect
}

2.3 条件依赖 #

jsx
function ConditionalEffect() {
  const [show, setShow] = createSignal(true);
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    if (show()) {
      // 只有 show() 为 true 时才追踪 count
      console.log('Count:', count());
    } else {
      console.log('Hidden');
    }
  });

  return (
    <div>
      <button onClick={() => setShow(s => !s)}>Toggle</button>
      <button onClick={() => setCount(c => c + 1)}>Count++</button>
    </div>
  );
}

三、清理机制 #

3.1 onCleanup #

使用 onCleanup 在 Effect 清理时执行操作:

jsx
import { createSignal, createEffect, onCleanup } from 'solid-js';

function Timer() {
  const [seconds, setSeconds] = createSignal(0);

  createEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // 清理函数
    onCleanup(() => {
      clearInterval(interval);
      console.log('Timer cleaned up');
    });
  });

  return <p>Seconds: {seconds()}</p>;
}

3.2 常见清理场景 #

jsx
function EventListener() {
  const [position, setPosition] = createSignal({ x: 0, y: 0 });

  createEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);

    onCleanup(() => {
      window.removeEventListener('mousemove', handleMouseMove);
    });
  });

  return (
    <p>Position: {position().x}, {position().y}</p>
  );
}

3.3 WebSocket 示例 #

jsx
function WebSocketChat() {
  const [messages, setMessages] = createSignal([]);
  const [connected, setConnected] = createSignal(false);

  createEffect(() => {
    const ws = new WebSocket('wss://example.com/chat');

    ws.onopen = () => setConnected(true);
    ws.onclose = () => setConnected(false);
    ws.onmessage = (e) => {
      setMessages(prev => [...prev, JSON.parse(e.data)]);
    };

    onCleanup(() => {
      ws.close();
      setConnected(false);
    });
  });

  return (
    <div>
      <p>Status: {connected() ? 'Connected' : 'Disconnected'}</p>
      <ul>
        <For each={messages()}>
          {(msg) => <li>{msg.text}</li>}
        </For>
      </ul>
    </div>
  );
}

四、Effect 配置 #

4.1 延迟执行 #

jsx
import { createSignal, createEffect } from 'solid-js';

const [count, setCount] = createSignal(0);

// 默认:立即执行
createEffect(() => {
  console.log('Immediate:', count());
});

// 延迟执行:首次不执行,依赖变化后才执行
createEffect(() => {
  console.log('Deferred:', count());
}, { defer: true });

4.2 使用 on 函数 #

jsx
import { createSignal, createEffect, on } from 'solid-js';

const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);

// 显式声明依赖
createEffect(on(a, (value) => {
  console.log('a changed:', value);
}));

// 多依赖
createEffect(on([a, b], ([aVal, bVal], prevValues) => {
  console.log('a or b changed:', aVal, bVal);
}));

// 带选项
createEffect(on(a, (value) => {
  console.log('a changed:', value);
}, { defer: true }));

4.3 on 函数选项 #

jsx
// defer: 延迟首次执行
createEffect(on(count, (value) => {
  console.log('Count:', value);
}, { defer: true }));

// 完整示例
createEffect(on(
  [firstName, lastName],
  ([first, last], prev) => {
    console.log(`Name: ${first} ${last}`);
    console.log('Previous:', prev);
  },
  { defer: true }
));

五、高级用法 #

5.1 嵌套 Effect #

jsx
function NestedEffects() {
  const [user, setUser] = createSignal(null);
  const [posts, setPosts] = createSignal([]);

  createEffect(() => {
    const u = user();
    if (u) {
      // 内层 Effect 在 user 存在时才创建
      createEffect(() => {
        console.log('User posts:', posts());
      });
    }
  });
}

5.2 动态 Effect #

jsx
function DynamicEffects() {
  const [items, setItems] = createSignal([]);

  createEffect(() => {
    items().forEach((item, index) => {
      // 为每个 item 创建独立的 Effect
      createEffect(() => {
        console.log(`Item ${index}:`, item.name);
      });
    });
  });
}

5.3 批量更新中的 Effect #

jsx
import { createSignal, createEffect, batch } from 'solid-js';

const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);

createEffect(() => {
  console.log('Effect:', a(), b());
});

// 批量更新:Effect 只执行一次
batch(() => {
  setA(10);
  setB(20);
});

六、实际应用 #

6.1 本地存储同步 #

jsx
function PersistentState() {
  const [todos, setTodos] = createSignal(
    JSON.parse(localStorage.getItem('todos') || '[]')
  );

  createEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos()));
  });

  const addTodo = (text) => {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  };

  return (
    <div>
      <button onClick={() => addTodo('New task')}>Add</button>
      <ul>
        <For each={todos()}>
          {(todo) => <li>{todo.text}</li>}
        </For>
      </ul>
    </div>
  );
}

6.2 API 请求 #

jsx
function UserProfile({ userId }) {
  const [user, setUser] = createSignal(null);
  const [loading, setLoading] = createSignal(false);
  const [error, setError] = createSignal(null);

  createEffect(() => {
    const id = userId();
    if (!id) return;

    setLoading(true);
    setError(null);

    fetch(`/api/users/${id}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  });

  return (
    <div>
      <Show when={loading()}>
        <p>Loading...</p>
      </Show>
      <Show when={error()}>
        <p>Error: {error()}</p>
      </Show>
      <Show when={user()}>
        {(u) => (
          <div>
            <h2>{u().name}</h2>
            <p>{u().email}</p>
          </div>
        )}
      </Show>
    </div>
  );
}

6.3 文档标题管理 #

jsx
function useDocumentTitle(title) {
  createEffect(() => {
    const previousTitle = document.title;
    document.title = title();

    onCleanup(() => {
      document.title = previousTitle;
    });
  });
}

function Page() {
  const [pageTitle, setPageTitle] = createSignal('Home');

  useDocumentTitle(pageTitle);

  return (
    <div>
      <input
        value={pageTitle()}
        onInput={(e) => setPageTitle(e.target.value)}
      />
    </div>
  );
}

6.4 订阅管理 #

jsx
function useSubscription(channel) {
  const [messages, setMessages] = createSignal([]);

  createEffect(() => {
    const ch = channel();
    if (!ch) return;

    const handler = (message) => {
      setMessages(prev => [...prev, message]);
    };

    ch.subscribe(handler);

    onCleanup(() => {
      ch.unsubscribe(handler);
    });
  });

  return messages;
}

七、Effect 注意事项 #

7.1 避免无限循环 #

jsx
// 错误:无限循环
createEffect(() => {
  setCount(count() + 1); // Effect 中修改依赖的 Signal
});

// 正确:使用事件触发
function Counter() {
  const [count, setCount] = createSignal(0);

  createEffect(() => {
    console.log('Count:', count());
  });

  return (
    <button onClick={() => setCount(c => c + 1)}>+</button>
  );
}

7.2 异步操作 #

jsx
// 注意:异步中读取 Signal 不会追踪依赖
createEffect(() => {
  setTimeout(() => {
    console.log(count()); // 不会追踪 count
  }, 1000);
});

// 正确:同步读取后传递
createEffect(() => {
  const value = count(); // 同步读取,追踪依赖
  setTimeout(() => {
    console.log(value);
  }, 1000);
});

7.3 条件渲染 #

jsx
function ConditionalEffect() {
  const [show, setShow] = createSignal(true);
  const [count, setCount] = createSignal(0);

  // 正确:在条件内读取 Signal
  createEffect(() => {
    if (show()) {
      console.log(count()); // 只在 show() 为 true 时追踪
    }
  });

  return (
    <div>
      <button onClick={() => setShow(s => !s)}>Toggle</button>
      <Show when={show()}>
        <button onClick={() => setCount(c => c + 1)}>Count++</button>
      </Show>
    </div>
  );
}

八、总结 #

8.1 Effect 核心 API #

API 说明
createEffect(fn) 创建 Effect
createEffect(fn, options) 带选项
onCleanup(fn) 注册清理函数
on(deps, fn) 显式声明依赖

8.2 Effect 选项 #

选项 说明
defer 延迟首次执行

8.3 最佳实践 #

  1. 清理资源:使用 onCleanup 清理定时器、事件监听器等
  2. 避免循环:不要在 Effect 中修改自己依赖的 Signal
  3. 显式依赖:使用 on 函数明确依赖
  4. 异步处理:在同步上下文中读取 Signal
  5. 条件依赖:理解动态依赖追踪行为

8.4 常见用途 #

  • 数据同步(localStorage、服务器)
  • 事件监听
  • 定时器管理
  • WebSocket 连接
  • 文档标题更新
  • 日志记录
最后更新:2026-03-28