Computed计算属性 #

一、createMemo 基础 #

1.1 什么是 Memo #

Memo(记忆化计算)是 Solid 的派生状态原语,用于基于其他 Signal 计算派生值,并自动缓存结果。

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

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

// 创建派生状态
const doubled = createMemo(() => count() * 2);

// doubled() 返回 count() * 2 的缓存结果
console.log(doubled()); // 0
setCount(5);
console.log(doubled()); // 10

1.2 Memo 结构 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Memo 工作原理                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Signal (count)                                            │
│       │                                                     │
│       ↓ 依赖                                                │
│   ┌─────────────┐                                           │
│   │   Memo      │                                           │
│   │ () => count() * 2                                      │
│   └─────────────┘                                           │
│       │                                                     │
│       ↓ 缓存结果                                            │
│   doubled()                                                 │
│                                                             │
│   特点:                                                    │
│   1. 自动追踪依赖                                           │
│   2. 缓存计算结果                                           │
│   3. 依赖不变时不重新计算                                   │
│   4. 只读的 Signal                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、基本用法 #

2.1 简单计算 #

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

  // 派生值
  const doubled = createMemo(() => count() * 2);
  const squared = createMemo(() => count() * count());

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

2.2 字符串处理 #

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

  // 组合字符串
  const fullName = createMemo(() => `${firstName()} ${lastName()}`);
  
  // 大写
  const upperName = createMemo(() => fullName().toUpperCase());

  return (
    <div>
      <p>Full Name: {fullName()}</p>
      <p>Upper: {upperName()}</p>
    </div>
  );
}

2.3 条件计算 #

jsx
function DiscountCalculator() {
  const [price, setPrice] = createSignal(100);
  const [isMember, setIsMember] = createSignal(false);

  const finalPrice = createMemo(() => {
    if (isMember()) {
      return price() * 0.9; // 会员 9 折
    }
    return price();
  });

  const discount = createMemo(() => {
    return price() - finalPrice();
  });

  return (
    <div>
      <p>Original: ${price()}</p>
      <p>Final: ${finalPrice()}</p>
      <p>Discount: ${discount()}</p>
      <button onClick={() => setIsMember(m => !m)}>
        Toggle Member
      </button>
    </div>
  );
}

三、缓存机制 #

3.1 计算缓存 #

Memo 会缓存计算结果,依赖不变时直接返回缓存:

jsx
function ExpensiveCalculation() {
  const [count, setCount] = createSignal(0);
  const [other, setOther] = createSignal(0);

  let calculationCount = 0;

  const expensive = createMemo(() => {
    calculationCount++;
    console.log('Calculating...', calculationCount);
    
    // 模拟昂贵计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += i;
    }
    return result + count();
  });

  // 读取多次,只计算一次
  console.log(expensive()); // 计算并缓存
  console.log(expensive()); // 返回缓存
  console.log(expensive()); // 返回缓存

  // other 变化不影响 expensive
  setOther(1);
  console.log(expensive()); // 返回缓存(count 未变)

  // count 变化触发重新计算
  setCount(1);
  console.log(expensive()); // 重新计算

  return (
    <div>
      <p>Result: {expensive()}</p>
      <button onClick={() => setCount(c => c + 1)}>Count++</button>
      <button onClick={() => setOther(o => o + 1)}>Other++</button>
    </div>
  );
}

3.2 依赖追踪 #

jsx
function DynamicDeps() {
  const [mode, setMode] = createSignal('a');
  const [valueA, setValueA] = createSignal(1);
  const [valueB, setValueB] = createSignal(2);

  const result = createMemo(() => {
    // 动态依赖
    if (mode() === 'a') {
      return valueA(); // 只依赖 mode 和 valueA
    } else {
      return valueB(); // 只依赖 mode 和 valueB
    }
  });

  createEffect(() => {
    console.log('Result:', result());
  });

  return (
    <div>
      <p>Mode: {mode()}</p>
      <p>Result: {result()}</p>
      <button onClick={() => setMode(m => m === 'a' ? 'b' : 'a')}>
        Toggle Mode
      </button>
    </div>
  );
}

四、链式 Memo #

4.1 Memo 链 #

Memo 可以依赖其他 Memo:

jsx
function ChainedMemo() {
  const [price, setPrice] = createSignal(100);
  const [quantity, setQuantity] = createSignal(1);

  // 第一层计算
  const subtotal = createMemo(() => price() * quantity());

  // 第二层计算
  const tax = createMemo(() => subtotal() * 0.1);

  // 第三层计算
  const total = createMemo(() => subtotal() + tax());

  return (
    <div>
      <p>Price: ${price()}</p>
      <p>Quantity: {quantity()}</p>
      <p>Subtotal: ${subtotal()}</p>
      <p>Tax: ${tax()}</p>
      <p>Total: ${total()}</p>
    </div>
  );
}

4.2 计算图 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Memo 计算图                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Signal        Signal                                      │
│   ┌─────┐       ┌─────────┐                                │
│   │price│       │quantity │                                │
│   └──┬──┘       └────┬────┘                                │
│      │               │                                      │
│      └───────┬───────┘                                      │
│              ↓                                              │
│         ┌─────────┐                                         │
│         │subtotal │ = price * quantity                      │
│         └────┬────┘                                         │
│              │                                              │
│              ↓                                              │
│         ┌─────────┐                                         │
│         │   tax   │ = subtotal * 0.1                        │
│         └────┬────┘                                         │
│              │                                              │
│              ↓                                              │
│         ┌─────────┐                                         │
│         │  total  │ = subtotal + tax                        │
│         └─────────┘                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

五、高级用法 #

5.1 带初始值的 Memo #

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

const summary = createMemo(
  (prev) => {
    const current = items();
    if (current.length === 0) return { count: 0, total: 0 };
    
    return {
      count: current.length,
      total: current.reduce((sum, item) => sum + item.price, 0)
    };
  },
  { count: 0, total: 0 } // 初始值
);

5.2 自定义 equals #

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

const summary = createMemo(
  (prev) => {
    const current = items();
    return {
      count: current.length,
      total: current.reduce((sum, item) => sum + item.price, 0)
    };
  },
  undefined,
  {
    equals: (prev, next) => {
      // 只有 count 和 total 都相等时才认为相等
      return prev?.count === next?.count && prev?.total === next?.total;
    }
  }
);

5.3 延迟计算 #

jsx
function LazyCalculation() {
  const [data, setData] = createSignal(null);

  // 只有在需要时才计算
  const processed = createMemo(() => {
    const d = data();
    if (!d) return null;
    
    // 复杂计算
    return d.map(item => ({
      ...item,
      processed: true
    }));
  });

  return (
    <Show when={processed()}>
      {(p) => (
        <ul>
          <For each={p()}>
            {(item) => <li>{item.name}</li>}
          </For>
        </ul>
      )}
    </Show>
  );
}

六、Memo vs 函数 #

6.1 性能对比 #

jsx
function Comparison() {
  const [count, setCount] = createSignal(0);
  const [other, setOther] = createSignal(0);

  // 使用 Memo - 缓存结果
  const memoized = createMemo(() => {
    console.log('Memo calculated');
    return count() * 2;
  });

  // 使用函数 - 每次调用都计算
  const notMemoized = () => {
    console.log('Function called');
    return count() * 2;
  };

  createEffect(() => {
    // Memo 只在 count 变化时重新计算
    console.log('Memo:', memoized());
    console.log('Memo again:', memoized()); // 使用缓存
  });

  return (
    <div>
      <p>Memo: {memoized()}</p>
      <p>Function: {notMemoized()}</p>
    </div>
  );
}

6.2 使用场景 #

场景 推荐使用
简单计算 函数
昂贵计算 Memo
需要缓存 Memo
需要追踪依赖 Memo
动态参数 函数

七、实际应用 #

7.1 过滤和排序 #

jsx
function TodoList() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: 'Learn Solid', done: false },
    { id: 2, text: 'Build app', done: true },
    { id: 3, text: 'Deploy', done: false }
  ]);
  const [filter, setFilter] = createSignal('all');
  const [sortBy, setSortBy] = createSignal('id');

  const filteredTodos = createMemo(() => {
    let result = todos();
    
    // 过滤
    switch (filter()) {
      case 'active':
        result = result.filter(t => !t.done);
        break;
      case 'completed':
        result = result.filter(t => t.done);
        break;
    }
    
    // 排序
    result = [...result].sort((a, b) => {
      if (sortBy() === 'id') return a.id - b.id;
      return a.text.localeCompare(b.text);
    });
    
    return result;
  });

  return (
    <div>
      <select value={filter()} onChange={(e) => setFilter(e.target.value)}>
        <option value="all">All</option>
        <option value="active">Active</option>
        <option value="completed">Completed</option>
      </select>
      
      <ul>
        <For each={filteredTodos()}>
          {(todo) => <li>{todo.text}</li>}
        </For>
      </ul>
    </div>
  );
}

7.2 购物车计算 #

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

  const subtotal = createMemo(() =>
    items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

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

  const discount = createMemo(() => {
    const s = subtotal();
    if (s > 100) return s * 0.1;
    if (s > 50) return s * 0.05;
    return 0;
  });

  const tax = createMemo(() => (subtotal() - discount()) * 0.08);

  const total = createMemo(() => subtotal() - discount() + tax());

  return (
    <div>
      <p>Items: {itemCount()}</p>
      <p>Subtotal: ${subtotal().toFixed(2)}</p>
      <p>Discount: -${discount().toFixed(2)}</p>
      <p>Tax: ${tax().toFixed(2)}</p>
      <p>Total: ${total().toFixed(2)}</p>
    </div>
  );
}

7.3 表单验证 #

jsx
function FormValidation() {
  const [email, setEmail] = createSignal('');
  const [password, setPassword] = createSignal('');

  const emailError = createMemo(() => {
    const e = email();
    if (!e) return 'Email is required';
    if (!e.includes('@')) return 'Invalid email format';
    return null;
  });

  const passwordError = createMemo(() => {
    const p = password();
    if (!p) return 'Password is required';
    if (p.length < 8) return 'Password must be at least 8 characters';
    return null;
  });

  const isValid = createMemo(() => 
    !emailError() && !passwordError()
  );

  const handleSubmit = (e) => {
    e.preventDefault();
    if (isValid()) {
      console.log('Submit:', { email: email(), password: password() });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={email()}
          onInput={(e) => setEmail(e.target.value)}
        />
        <Show when={emailError()}>
          <span class="error">{emailError()}</span>
        </Show>
      </div>
      
      <div>
        <input
          type="password"
          value={password()}
          onInput={(e) => setPassword(e.target.value)}
        />
        <Show when={passwordError()}>
          <span class="error">{passwordError()}</span>
        </Show>
      </div>
      
      <button type="submit" disabled={!isValid()}>
        Submit
      </button>
    </form>
  );
}

八、总结 #

8.1 Memo 核心 API #

API 说明
createMemo(fn) 创建 Memo
createMemo(fn, initialValue) 带初始值
createMemo(fn, initialValue, options) 带选项

8.2 Memo 特点 #

  • 只读:不能直接设置值
  • 缓存:依赖不变时返回缓存
  • 自动追踪:自动收集依赖
  • 惰性求值:只在被读取时计算

8.3 最佳实践 #

  1. 昂贵计算使用 Memo:避免重复计算
  2. 简单计算用函数:减少开销
  3. 合理链式:避免过深的依赖链
  4. 注意依赖:理解动态依赖行为
  5. 自定义 equals:优化更新判断
最后更新:2026-03-28