Signals信号 #
一、Signals 概述 #
1.1 什么是 Signals #
Signals 是 Preact 提供的响应式状态管理方案,可以自动追踪依赖并在变化时更新相关组件。
jsx
import { signal, computed, effect } from '@preact/signals';
const count = signal(0);
const double = computed(() => count.value * 2);
effect(() => {
console.log(`Count is ${count.value}`);
});
1.2 Signals vs State #
| 方面 | Signals | useState |
|---|---|---|
| 更新粒度 | 精确更新 | 组件级别 |
| 订阅 | 自动追踪 | 手动依赖 |
| 跨组件 | 天然支持 | 需要 Context |
| 性能 | 更优 | 较好 |
1.3 安装 #
bash
npm install @preact/signals
二、基本用法 #
2.1 创建 Signal #
jsx
import { signal } from '@preact/signals';
// 创建 signal
const count = signal(0);
const name = signal('Alice');
const items = signal([]);
// 读取值
console.log(count.value);
// 更新值
count.value = 1;
count.value++;
2.2 在组件中使用 #
jsx
import { signal } from '@preact/signals';
const count = signal(0);
function Counter() {
return (
<div>
<p>Count: {count.value}</p>
<button onClick={() => count.value++}>
Increment
</button>
</div>
);
}
2.3 useSignal Hook #
jsx
import { useSignal } from '@preact/signals';
function Counter() {
const count = useSignal(0);
return (
<div>
<p>Count: {count.value}</p>
<button onClick={() => count.value++}>
Increment
</button>
</div>
);
}
三、Computed 计算属性 #
3.1 基本用法 #
jsx
import { signal, computed } from '@preact/signals';
const firstName = signal('John');
const lastName = signal('Doe');
// 计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
console.log(fullName.value); // "John Doe"
firstName.value = 'Jane';
console.log(fullName.value); // "Jane Doe"
3.2 链式计算 #
jsx
const price = signal(100);
const quantity = signal(2);
const tax = signal(0.1);
const subtotal = computed(() => price.value * quantity.value);
const taxAmount = computed(() => subtotal.value * tax.value);
const total = computed(() => subtotal.value + taxAmount.value);
console.log(total.value); // 220
3.3 在组件中使用 #
jsx
function ShoppingCart() {
const items = useSignal([
{ id: 1, name: 'Item 1', price: 10, quantity: 2 },
{ id: 2, name: 'Item 2', price: 20, quantity: 1 }
]);
const total = useComputed(() => {
return items.value.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
});
return (
<div>
<ul>
{items.value.map(item => (
<li key={item.id}>
{item.name} x {item.quantity} = ${item.price * item.quantity}
</li>
))}
</ul>
<p>Total: ${total.value}</p>
</div>
);
}
四、Effect 副作用 #
4.1 基本用法 #
jsx
import { signal, effect } from '@preact/signals';
const count = signal(0);
// 自动追踪依赖
const dispose = effect(() => {
console.log(`Count changed to ${count.value}`);
document.title = `Count: ${count.value}`;
});
// 清理 effect
dispose();
4.2 在组件中使用 #
jsx
import { useSignal, useComputed, useSignalEffect } from '@preact/signals';
function UserProfile({ userId }) {
const user = useSignal(null);
const loading = useSignal(true);
useSignalEffect(() => {
loading.value = true;
fetchUser(userId).then(data => {
user.value = data;
loading.value = false;
});
});
if (loading.value) return <p>Loading...</p>;
return (
<div>
<h1>{user.value.name}</h1>
<p>{user.value.email}</p>
</div>
);
}
五、Signal 数组和对象 #
5.1 数组操作 #
jsx
function TodoList() {
const todos = useSignal([]);
const addTodo = (text) => {
todos.value = [...todos.value, {
id: Date.now(),
text,
completed: false
}];
};
const toggleTodo = (id) => {
todos.value = todos.value.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
);
};
const removeTodo = (id) => {
todos.value = todos.value.filter(todo => todo.id !== id);
};
return (
<div>
<input
onKeyDown={(e) => {
if (e.key === 'Enter') {
addTodo(e.target.value);
e.target.value = '';
}
}}
/>
<ul>
{todos.value.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>×</button>
</li>
))}
</ul>
</div>
);
}
5.2 对象操作 #
jsx
function Form() {
const form = useSignal({
username: '',
email: '',
password: ''
});
const updateField = (field, value) => {
form.value = {
...form.value,
[field]: value
};
};
const isValid = useComputed(() => {
const { username, email, password } = form.value;
return username.length > 0 &&
email.includes('@') &&
password.length >= 6;
});
return (
<form>
<input
value={form.value.username}
onInput={(e) => updateField('username', e.target.value)}
placeholder="Username"
/>
<input
value={form.value.email}
onInput={(e) => updateField('email', e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={form.value.password}
onInput={(e) => updateField('password', e.target.value)}
placeholder="Password"
/>
<button disabled={!isValid.value}>Submit</button>
</form>
);
}
六、全局状态 #
6.1 创建全局 Signal #
jsx
// store.js
import { signal, computed } from '@preact/signals';
export const user = signal(null);
export const cart = signal([]);
export const cartTotal = computed(() => {
return cart.value.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
});
export const cartCount = computed(() => {
return cart.value.reduce((sum, item) => sum + item.quantity, 0);
});
export const addToCart = (product) => {
const existing = cart.value.find(item => item.id === product.id);
if (existing) {
cart.value = cart.value.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
cart.value = [...cart.value, { ...product, quantity: 1 }];
}
};
export const removeFromCart = (productId) => {
cart.value = cart.value.filter(item => item.id !== productId);
};
6.2 使用全局 Signal #
jsx
import { user, cart, cartTotal, addToCart } from './store';
function Header() {
return (
<header>
<h1>My Shop</h1>
<div>
{user.value ? (
<span>Hello, {user.value.name}</span>
) : (
<a href="/login">Login</a>
)}
<span>Cart: ${cartTotal.value}</span>
</div>
</header>
);
}
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
))}
</div>
);
}
七、批量更新 #
7.1 batch 函数 #
jsx
import { signal, batch } from '@preact/signals';
const firstName = signal('John');
const lastName = signal('Doe');
const age = signal(25);
// 批量更新,只触发一次渲染
batch(() => {
firstName.value = 'Jane';
lastName.value = 'Smith';
age.value = 30;
});
7.2 在组件中使用 #
jsx
function UserForm() {
const user = useSignal({ name: '', email: '', age: 0 });
const resetForm = () => {
batch(() => {
user.value = { name: '', email: '', age: 0 };
});
};
const updateFromAPI = async () => {
const data = await fetchUser();
batch(() => {
user.value = data;
});
};
return (
<form>
{/* ... */}
<button type="button" onClick={resetForm}>Reset</button>
</form>
);
}
八、最佳实践 #
8.1 组织 Signal #
jsx
// store/user.js
export const user = signal(null);
export const isLoggedIn = computed(() => !!user.value);
// store/cart.js
export const cart = signal([]);
export const cartTotal = computed(() => /* ... */);
// store/index.js
export * from './user';
export * from './cart';
8.2 封装操作 #
jsx
// 推荐:封装操作函数
export function useUserStore() {
const login = async (credentials) => {
const data = await loginAPI(credentials);
user.value = data;
};
const logout = () => {
user.value = null;
};
return { user, login, logout };
}
8.3 类型定义 #
typescript
import { Signal } from '@preact/signals';
interface User {
id: number;
name: string;
email: string;
}
const user: Signal<User | null> = signal(null);
九、总结 #
| 要点 | 说明 |
|---|---|
| signal | 创建响应式状态 |
| computed | 计算属性 |
| effect | 副作用 |
| batch | 批量更新 |
| useSignal | 组件内使用 |
核心原则:
- 使用 .value 读写
- computed 自动追踪依赖
- 批量更新优化性能
- 合理组织全局状态
最后更新:2026-03-28