Итерирование через each()

Метод each() в Dexie.js предназначен для последовательного обхода всех элементов выборки, сформированной через Collection. Он используется в случаях, когда требуется выполнить побочный эффект для каждого объекта без необходимости заранее материализовывать весь результат в массив.


each() применяется к экземпляру Collection, который формируется после операций where(), filter(), orderBy(), limit() и других методов Dexie.

db.users.where('age').above(18).each(user => {
    console.log(user.name);
});

Ключевая особенность заключается в том, что данные не загружаются целиком. Итерация происходит по мере чтения из IndexedDB, что снижает потребление памяти при больших выборках.


Сигнатура и поведение

collection.each(callback)

callback получает текущий элемент коллекции и, при необходимости, объект курсора.

collection.each((value, cursor) => {
    // value — объект из таблицы
    // cursor — внутренний IndexedDB cursor (опционально)
});

Основные характеристики:

  • выполнение происходит синхронно в рамках одной итерации событийного цикла
  • не возвращает массив
  • возвращает Promise<void> после завершения обхода
  • не ожидает async внутри callback

Порядок обхода

Порядок зависит от источника Collection:

  • orderBy(index) — строго упорядоченный обход по индексу
  • where(index) без сортировки — порядок индекса
  • reverse() — инверсия порядка
  • limit() — ограничение количества элементов
db.users.orderBy('age').each(user => {
    console.log(user.age);
});

Работа с фильтрацией

each() часто применяется после фильтров where() и filter().

where()

db.orders.where('status').equals('pending').each(order => {
    console.log(order.id);
});

filter()

db.orders
    .filter(order => order.total > 1000)
    .each(order => {
        console.log(order.total);
    });

Важно учитывать, что filter() выполняется на стороне клиента после извлечения данных, поэтому each() будет вызываться только для уже отфильтрованных элементов.


Преждевременная остановка итерации

each() поддерживает механизм прерывания обхода через возврат false из callback.

db.users.each(user => {
    if (user.age > 30) {
        return false;
    }
    console.log(user.name);
});

При возврате false дальнейшее чтение из IndexedDB прекращается. Это позволяет экономить ресурсы при поиске первого подходящего элемента или при частичной обработке данных.


Асинхронные операции внутри each()

Callback each() не ожидает Promise. Это важное ограничение, которое часто приводит к ошибкам архитектуры.

db.users.each(async user => {
    await fetch('/log', {
        method: 'POST',
        body: JSON.stringify(user)
    });
});

В данном случае each() не дожидается завершения fetch, и выполнение продолжается параллельно.

Корректный подход при необходимости асинхронной обработки:

const users = await db.users.toArray();

for (const user of users) {
    await fetch('/log', {
        method: 'POST',
        body: JSON.stringify(user)
    });
}

Или при необходимости потоковой обработки без накопления массива — использование each() допускается только для синхронных операций.


Отличие от toArray()

each() и toArray() решают разные задачи:

toArray()

  • загружает всю коллекцию в память
  • возвращает массив
  • удобно для последующей обработки
const users = await db.users.toArray();
users.forEach(u => console.log(u.name));

each()

  • потоковая обработка
  • минимальное потребление памяти
  • отсутствие промежуточного массива
await db.users.each(u => console.log(u.name));

Производительность

При работе с большими наборами данных each() демонстрирует более стабильное потребление памяти, так как данные не материализуются полностью.

Факторы влияния:

  • наличие индекса
  • использование where() с индексированными полями
  • применение filter() (замедляет выполнение)
  • порядок сортировки (orderBy() требует дополнительных операций)

Использование с индексами

Эффективность each() напрямую зависит от индексации.

db.users.where('email').equals('test@example.com').each(user => {
    console.log(user);
});

При наличии индекса операция выполняется через IndexedDB cursor без полного сканирования таблицы.


Поведение в цепочках

each() завершает цепочку методов и не допускает дальнейшего chaining.

db.users
    .where('age').above(18)
    .each(user => {
        console.log(user);
    })
    .orderBy('name'); // не применяется

После each() дальнейшие методы коллекции не имеют смысла, так как итерация уже началась.


Доступ к курсору

В некоторых сценариях требуется доступ к низкоуровневому курсору IndexedDB.

db.users.each((user, cursor) => {
    console.log(cursor.primaryKey);
});

Курсор позволяет:

  • получить ключ записи
  • управлять позицией итерации
  • потенциально оптимизировать обход

Итерация по большим наборам данных

При обработке больших объемов данных важно учитывать:

  • отсутствие накопления результатов
  • отсутствие ожидания асинхронных операций
  • возможность прерывания через return false

Пример потоковой обработки:

let count = 0;

await db.logs.each(log => {
    if (log.level === 'error') {
        count++;
    }

    if (count > 100) {
        return false;
    }
});

Совместимость с транзакциями

each() выполняется в контексте текущей транзакции Dexie.

db.transaction('r', db.users, async () => {
    await db.users.each(user => {
        console.log(user.id);
    });
});

При завершении транзакции чтение данных может быть автоматически зафиксировано в зависимости от режима доступа.


Ограничения метода

  • нельзя использовать await как механизм синхронизации
  • нельзя трансформировать результат в новый массив напрямую
  • нельзя продолжать chaining после вызова
  • поведение зависит от IndexedDB cursor, что накладывает ограничения браузерного API

Сравнение с другими методами итерации Dexie

Метод Память Асинхронность Результат
each() низкая нет void
toArray() высокая да array
filter() средняя да collection
reverse().each() низкая нет void

Практика использования в архитектуре

each() используется в сценариях:

  • логирование записей
  • агрегация без хранения массива
  • потоковая обработка больших таблиц
  • ранний выход при поиске первого совпадения
  • миграции данных без промежуточных структур

Потоковый характер выполнения

Внутренняя реализация Dexie опирается на IndexedDB cursor API, что означает:

  • чтение идёт последовательно
  • браузер сам управляет буферизацией
  • выполнение callback синхронизировано с движением курсора
  • минимальная нагрузка на память при больших выборках

Поведение при изменении данных во время итерации

Если во время обхода изменяются записи той же таблицы:

  • новые записи могут не попасть в текущую итерацию
  • удалённые записи могут быть пропущены или вызвать сдвиг курсора
  • поведение зависит от транзакционного контекста IndexedDB

Завершение итерации и Promise

Метод возвращает Promise, который резолвится после завершения обхода или после досрочного прерывания.

await db.users.each(user => {
    console.log(user.name);
});

Это позволяет синхронизировать поток выполнения с завершением обхода без накопления результатов.