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 整个响应式系统的基础。