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