6. Мемо: createMemo
createMemo: кэшированные вычисления 🧮
Заголовок раздела «createMemo: кэшированные вычисления 🧮»Привет! 👋 Яша здесь. Мы знаем сигналы и эффекты. Пришло время третьего основного примитива — мемо. createMemo решает конкретную задачу: «вычисляй производное значение, но только когда зависимости изменились».
Если сигнал — это атомарное значение, а эффект — реакция на изменения, то мемо — это умная производная, которая кэширует результат и не пересчитывается без нужды.
📦 Базовый синтаксис
Заголовок раздела «📦 Базовый синтаксис»import { createSignal, createMemo } from 'solid-js';
const [count, setCount] = createSignal(5);const [multiplier, setMultiplier] = createSignal(3);
// createMemo: пересчитывается только при изменении count() или multiplier()const result = createMemo(() => { console.log('Пересчёт!'); return count() * multiplier();});
console.log(result()); // «Пересчёт!» → 15
// Читаем result() несколько раз — пересчёта нет!console.log(result()); // (нет пересчёта) → 15console.log(result()); // (нет пересчёта) → 15
setCount(10);console.log(result()); // «Пересчёт!» → 30
// Снова несколько чтений — только один пересчётconsole.log(result()); // (нет пересчёта) → 30Ключевые свойства мемо:
- Ленивый: вычисляется при первом чтении, не при создании
- Кэшированный: повторные вызовы
result()не пересчитывают - Реактивный: автоматически отслеживает зависимости (как эффект)
- Синхронный: обновляется сразу при изменении зависимостей
🆚 Обычная функция vs createMemo
Заголовок раздела «🆚 Обычная функция vs createMemo»Это главный вопрос: когда использовать мемо, а когда достаточно обычной функции?
Обычная функция-производная
Заголовок раздела «Обычная функция-производная»const [count, setCount] = createSignal(5);const [otherSignal, setOtherSignal] = createSignal('привет');
// Обычная функция: пересчитывается каждый раз, когда вызываетсяconst doubled = () => count() * 2;
// В JSX:// {doubled()} — пересчитывается при каждом рендере узла// Но поскольку Solid обновляет DOM только при изменении зависимостей,// doubled() будет вызван только когда изменится count()createMemo
Заголовок раздела «createMemo»const doubled = createMemo(() => count() * 2);
// doubled() — гарантированно возвращает кэш, если count() не менялось// Особенно важно, если doubled() используется в нескольких местах!Когда мемо выигрывает
Заголовок раздела «Когда мемо выигрывает»const [a, setA] = createSignal(1);const [b, setB] = createSignal(2);
// Дорогое вычислениеconst expensive = createMemo(() => heavyFibonacci(a() + b()));
// expensive() читается в 10 разных местах JSX// Без memo: heavyFibonacci вызывается 10 раз при каждом изменении// С memo: вычисляется ОДИН раз, все 10 мест получают кэш!🔗 Цепочки мемо
Заголовок раздела «🔗 Цепочки мемо»Мемо можно составлять в цепочки:
import { createSignal, createMemo } from 'solid-js';
function PricingCalculator() { const [quantity, setQuantity] = createSignal(1); const [basePrice, setBasePrice] = createSignal(100); const [taxRate, setTaxRate] = createSignal(0.2); // 20% const [promoCode, setPromoCode] = createSignal('');
// Шаг 1: скидка по промокоду const discount = createMemo(() => { const code = promoCode(); if (code === 'SALE10') return 0.1; if (code === 'SALE20') return 0.2; if (code === 'HALF') return 0.5; return 0; });
// Шаг 2: цена с учётом скидки const discountedPrice = createMemo(() => { return basePrice() * (1 - discount()); // зависит от мемо! });
// Шаг 3: итоговая сумма с налогом const total = createMemo(() => { return (discountedPrice() * quantity() * (1 + taxRate())).toFixed(2); });
return ( <div> <p>Базовая цена: {basePrice()} ₽</p> <p>Скидка: {(discount() * 100).toFixed(0)}%</p> <p>Цена со скидкой: {discountedPrice().toFixed(2)} ₽</p> <p>Итого ({quantity()} шт. + налог {taxRate() * 100}%): {total()} ₽</p> </div> );}Если изменить promoCode, пересчитается только discount → discountedPrice → total. Если изменить quantity — пересчитается только total. Максимально эффективно!
⚖️ Опция equals
Заголовок раздела «⚖️ Опция equals»По умолчанию мемо обновляет подписчиков при изменении ссылки или значения. Можно настроить это поведение:
import { createMemo } from 'solid-js';
// Не обновлять, если значение не изменилось (по умолчанию)const doubled = createMemo(() => count() * 2);
// Кастомная функция сравненияconst normalizedName = createMemo( () => name().trim().toLowerCase(), undefined, { equals: (prev, next) => prev === next } // не обновлять при одинаковых строках);
// Всегда обновлять (отключить кэш)const alwaysNew = createMemo( () => ({ timestamp: Date.now(), value: count() }), undefined, { equals: false } // всегда считается «изменённым»);Практический пример: нормализация данных
Заголовок раздела «Практический пример: нормализация данных»const [rawInput, setRawInput] = createSignal(' Яша Иванов ');
// Нормализованное имя — не обновляет если результат тот жеconst cleanName = createMemo( () => rawInput().trim().toLowerCase(), undefined, { equals: (a, b) => a === b });
setRawInput(' Яша Иванов '); // Тот же результат → подписчики НЕ обновятсяsetRawInput(' яша иванов '); // cleanName = 'яша иванов' → ДА, обновятся🔄 createMemo vs производная сигнальная функция vs createEffect
Заголовок раздела «🔄 createMemo vs производная сигнальная функция vs createEffect»// 1. Обычная функция: пересчёт при каждом вызовеconst doubled = () => count() * 2;// Подходит для: простые вычисления, используемые в 1-2 местах
// 2. createMemo: кэш + реактивностьconst doubled = createMemo(() => count() * 2);// Подходит для: дорогие вычисления, используемые в нескольких местах
// 3. createEffect: побочные эффекты (не возвращает значение для JSX)createEffect(() => { console.log(count() * 2);});// Подходит для: sync с DOM, fetch, аналитика — не для вычисления значений!🧩 Реальный паттерн: фильтрованный список
Заголовок раздела «🧩 Реальный паттерн: фильтрованный список»import { createSignal, createMemo, For } from 'solid-js';
interface Product { id: number; name: string; price: number; category: string }
function ProductList() { const [products, setProducts] = createSignal<Product[]>([ { id: 1, name: 'Ноутбук', price: 50000, category: 'electronics' }, { id: 2, name: 'Мышь', price: 1500, category: 'electronics' }, { id: 3, name: 'Стол', price: 8000, category: 'furniture' }, { id: 4, name: 'Кресло', price: 12000, category: 'furniture' }, ]);
const [search, setSearch] = createSignal(''); const [category, setCategory] = createSignal('all'); const [sortBy, setSortBy] = createSignal<'name' | 'price'>('name');
// Мемо: фильтрация — пересчитывается только при изменении search или category const filtered = createMemo(() => { const q = search().toLowerCase(); const cat = category(); return products().filter(p => (cat === 'all' || p.category === cat) && p.name.toLowerCase().includes(q) ); });
// Мемо: сортировка — пересчитывается при изменении filtered() или sortBy() const sorted = createMemo(() => { const key = sortBy(); return [...filtered()].sort((a, b) => key === 'price' ? a.price - b.price : a.name.localeCompare(b.name) ); });
// Мемо: статистика — пересчитывается только при изменении filtered() const stats = createMemo(() => ({ count: filtered().length, total: filtered().reduce((sum, p) => sum + p.price, 0), avg: filtered().length ? filtered().reduce((sum, p) => sum + p.price, 0) / filtered().length : 0, }));
return ( <div> <input value={search()} onInput={e => setSearch(e.target.value)} placeholder="Поиск..." /> <p>Найдено: {stats().count}, итого: {stats().total} ₽</p> <For each={sorted()}> {(product) => <div>{product.name} — {product.price} ₽</div>} </For> </div> );}🎮 Playground: сравнение memo vs обычная функция
Заголовок раздела «🎮 Playground: сравнение memo vs обычная функция»Fibonacci: смотри, как memo кэширует вычисление, а обычная функция выполняется снова и снова: