Chapter 02

Signals 信号系统

SolidJS 响应式的核心:createSignal、createEffect、createMemo 三大原语的深度解析

1. createSignal — 响应式数据源

createSignal 是 SolidJS 的核心原语,用于创建响应式数据。它返回一个元组 [getter, setter]——getter 是读函数,setter 是写函数。

基本用法

import { createSignal } from "solid-js";

// 语法:createSignal(初始值, 选项?)
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal("Alice");
const [items, setItems] = createSignal<string[]>([]);

// 读取:调用 getter 函数
console.log(count()); // 0
console.log(name());  // "Alice"

// 写入方式一:直接传值
setCount(5);

// 写入方式二:传函数(接收旧值,推荐用于依赖旧值的更新)
setCount(prev => prev + 1); // 推荐,避免闭包陷阱

// 写入数组:必须返回新数组引用
setItems(prev => [...prev, "new item"]);

Signal 的相等性检查

默认情况下,SolidJS 使用 === 比较新旧值,如果相等则不触发更新。可以通过 equals 选项自定义比较逻辑:

// equals: false —— 每次 set 都强制触发更新
const [data, setData] = createSignal([], { equals: false });

// 自定义比较函数(例如深比较)
const [user, setUser] = createSignal(
  { name: "Alice", age: 25 },
  { equals: (prev, next) => prev.name === next.name }
);

2. createEffect — 响应式副作用

createEffect 用于执行带有副作用的响应式代码,例如:打印日志、操作 DOM、发起网络请求。它会追踪内部读取的 Signal,并在这些 Signal 变化时自动重新执行

import { createSignal, createEffect } from "solid-js";

const [count, setCount] = createSignal(0);
const [name, setName] = createSignal("Alice");

// Effect 立即执行一次,追踪 count 和 name
createEffect(() => {
  console.log(`${name()}: ${count()}`);
  // 每当 count 或 name 变化时重新执行
});

// Effect 的清理函数:返回值在下次执行前调用
createEffect(() => {
  const timer = setInterval(() => setCount(c => c + 1), 1000);
  // 返回清理函数,在 Effect 重新执行或组件卸载时调用
  return () => clearInterval(timer);
});

Effect 的执行时机

ℹ️

与 React useEffect 的区别:React 的 useEffect 在浏览器绘制后异步执行;SolidJS 的 createEffect 在 DOM 更新后同步执行(microtask)。如果需要在 DOM 更新前执行,使用 createRenderEffect;如果需要在 DOM 绘制后执行,使用 onMount

3. createMemo — 派生状态

createMemo 创建一个缓存的计算 Signal。只有当其依赖的 Signal 变化时才重新计算,其他任何时候读取都返回缓存值。适用于计算开销大、被多处读取的派生数据。

import { createSignal, createMemo } from "solid-js";

const [price, setPrice] = createSignal(100);
const [qty, setQty] = createSignal(3);
const [discount, setDiscount] = createSignal(0.1);

// createMemo:只有当 price/qty/discount 变化时才重算
const total = createMemo(() => {
  console.log("重新计算 total"); // 可以观察到何时重算
  return price() * qty() * (1 - discount());
});

// total 是一个 getter 函数,读取方式与 Signal 相同
console.log(total()); // 270

setPrice(200); // total 重算 → 540

// 多次读取不会多次计算
console.log(total()); // 540 (从缓存读取)
console.log(total()); // 540 (从缓存读取)

Memo vs 普通派生函数

// 方式一:普通函数(每次调用都重新计算)
const totalFn = () => price() * qty();

// 方式二:createMemo(缓存,依赖变化时才重算)
const totalMemo = createMemo(() => price() * qty());

// 何时用 Memo?
// - 计算代价高(复杂排序、过滤大列表)
// - 同一值被多个地方读取
// - 作为其他 Memo/Effect 的依赖,避免重复计算链

4. batch — 批量更新

默认情况下,每次调用 setter 都会立即触发所有相关 Effect。batch 函数将多个 Signal 的更新合并成一次,所有 Effect 只执行一次。

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

const [x, setX] = createSignal(0);
const [y, setY] = createSignal(0);

createEffect(() => {
  console.log(`x=${x()}, y=${y()}`);
});

// 不用 batch:Effect 执行两次
setX(1); // Effect: x=1, y=0
setY(1); // Effect: x=1, y=1

// 使用 batch:Effect 只执行一次
batch(() => {
  setX(2);
  setY(2);
}); // Effect: x=2, y=2(只触发一次)

事件处理器自动批量:SolidJS 的 DOM 事件处理器(如 onClick)内部的所有 Signal 更新自动包裹在 batch 中,无需手动调用。只有在异步场景(setTimeout、fetch 回调等)才需要手动使用 batch。

5. untrack — 逃离追踪

untrack 允许在 Effect/Memo 内部读取 Signal 而不建立订阅关系。当你只想"偷看"一个值但不希望它触发重新计算时非常有用。

import { createSignal, createEffect, untrack } from "solid-js";

const [trigger, setTrigger] = createSignal(0);
const [data, setData] = createSignal("initial");

createEffect(() => {
  // 只追踪 trigger,不追踪 data
  trigger(); // 建立订阅

  // untrack 内读取 data,不建立订阅
  const currentData = untrack(() => data());
  console.log(`Triggered! data = ${currentData}`);
});

setTrigger(1); // Effect 重新执行(读取当时的 data 值)
setData("changed"); // Effect 不执行(data 不在追踪中)

6. on — 显式依赖声明

on 是一个工具函数,让你明确声明 Effect 的触发来源,类似 React 的 useEffect([deps]),但更灵活:

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

const [count, setCount] = createSignal(0);
const [other, setOther] = createSignal("x");

// 只有 count 变化才触发,other 变化不触发
createEffect(on(count, (value, prevValue) => {
  console.log(`count: ${prevValue}${value}`);
}));

// defer: true —— 跳过初始执行(类似 React 的 useEffect 依赖不包含初始值)
createEffect(on(count, (value) => {
  console.log(`count changed to ${value}`);
}, { defer: true }));

7. 实战:计数器 + 购物车小计

综合运用本章所学,实现一个带商品列表和实时小计的购物车组件。

import { createSignal, createMemo, batch } from "solid-js";
import { For } from "solid-js";

interface CartItem {
  id: number;
  name: string;
  price: number;
  qty: number;
}

function ShoppingCart() {
  const [items, setItems] = createSignal<CartItem[]>([
    { id: 1, name: "SolidJS 贴纸", price: 9.9, qty: 2 },
    { id: 2, name: "高性能 T 恤", price: 79, qty: 1 },
    { id: 3, name: "Signal 马克杯", price: 39, qty: 3 },
  ]);

  // 派生状态:总金额(依赖 items Signal)
  const subtotal = createMemo(() =>
    items().reduce((sum, item) => sum + item.price * item.qty, 0)
  );

  const itemCount = createMemo(() =>
    items().reduce((sum, item) => sum + item.qty, 0)
  );

  const updateQty = (id: number, delta: number) => {
    setItems(prev => prev.map(item =>
      item.id === id
        ? { ...item, qty: Math.max(0, item.qty + delta) }
        : item
    ).filter(item => item.qty > 0));
  };

  const clearCart = () => batch(() => setItems([]));

  return (
    <div>
      <h2>购物车({itemCount()} 件)</h2>
      <For each={items()}>
        {(item) => (
          <div>
            <span>{item.name}</span>
            <button onClick={() => updateQty(item.id, -1)}>-</button>
            <span>{item.qty}</span>
            <button onClick={() => updateQty(item.id, 1)}>+</button>
            <span>¥{(item.price * item.qty).toFixed(2)}</span>
          </div>
        )}
      </For>
      <p>小计:¥{subtotal().toFixed(2)}</p>
      <button onClick={clearCart}>清空购物车</button>
    </div>
  );
}

本章小结:createSignal 是数据源,createEffect 是副作用订阅者,createMemo 是缓存派生值。batch 合并更新减少执行次数,untrack 读取值但不订阅,on 显式声明依赖。三大原语构成了 SolidJS 整个响应式系统的基础。