Виртуализация

SortableJS — это библиотека JavaScript, позволяющая создавать интерактивные списки с перетаскиванием элементов (drag-and-drop). Одной из важных концепций при работе с большими списками является виртуализация, которая позволяет эффективно управлять производительностью при отображении сотен и тысяч элементов.

Принцип виртуализации

Виртуализация заключается в том, что на странице одновременно рендерятся только те элементы списка, которые видимы пользователю, а остальные находятся в «виртуальном» состоянии. Это снижает нагрузку на DOM и ускоряет взаимодействие с элементами, включая перетаскивание.

SortableJS из коробки не реализует виртуализацию, но может быть интегрирован с библиотеками виртуализации, такими как Virtual DOM, react-window или vue-virtual-scroller, обеспечивая плавный drag-and-drop даже для больших массивов данных.

Интеграция с виртуализированными списками

Основная задача при интеграции SortableJS с виртуализацией — синхронизация видимых элементов с внутренними данными и корректное отслеживание положения элементов при их перетаскивании.

Ключевые шаги интеграции:

  1. Инициализация Sortable Sortable создается на контейнере, содержащем видимые элементы виртуализированного списка.

    import Sortable from 'sortablejs';
    
    const sortable = new Sortable(containerElement, {
        animation: 150,
        ghostClass: 'sortable-ghost',
        onEnd: handleDragEnd
    });
  2. Обновление позиции данных После завершения перетаскивания (onEnd) необходимо обновить исходный массив данных, а затем пересчитать видимые элементы для виртуального рендера:

    function handleDragEnd(evt) {
        const { oldIndex, newIndex } = evt;
        const movedItem = data.splice(oldIndex, 1)[0];
        data.splice(newIndex, 0, movedItem);
        virtualList.update(data); // обновление виртуализированного списка
    }
  3. Синхронизация индексов Виртуализация меняет порядок рендеринга элементов, поэтому индексы в DOM могут не совпадать с индексами в массиве данных. Используется отображение DOM-индекса на индекс данных через карту:

    const domToDataIndexMap = new Map();
    
    function renderVisibleItems() {
        const visibleItems = virtualList.getVisibleItems();
        visibleItems.forEach((item, i) => {
            domToDataIndexMap.set(item.domId, item.dataIndex);
        });
    }
  4. Обработка скролла При прокрутке виртуализированного списка необходимо пересоздавать Sortable на новых видимых элементах или обновлять позиции. Можно использовать метод Sortable#destroy перед повторной инициализацией, чтобы избежать конфликтов:

    virtualList.onScroll(() => {
        sortable.destroy();
        renderVisibleItems();
        sortable = new Sortable(containerElement, sortableOptions);
    });

Особенности виртуализированного drag-and-drop

  • Ghost-элемент Для визуальной индикации перемещения создается ghost-элемент, который не зависит от DOM-индексов. Это позволяет корректно отображать перетаскиваемый элемент при виртуализации.

    ghostClass: 'sortable-ghost'
  • Поддержка placeholder Для корректного отображения места вставки используется placeholder. Он должен соответствовать размеру одного элемента и обновляться при изменении видимых элементов.

  • Обработка анимаций Анимации сортировки (animation) должны быть синхронизированы с обновлением DOM виртуального списка, иначе возможны «скачки» элементов при прокрутке.

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

Допустим, используется библиотека react-window. Интеграция SortableJS может выглядеть следующим образом:

import { FixedSizeList as List } from 'react-window';
import Sortable from 'sortablejs';

function VirtualSortableList({ data }) {
    const listRef = useRef();

    useEffect(() => {
        const container = listRef.current._outerRef;
        const sortable = new Sortable(container, {
            animation: 150,
            ghostClass: 'sortable-ghost',
            onEnd: evt => {
                const movedItem = data.splice(evt.oldIndex, 1)[0];
                data.splice(evt.newIndex, 0, movedItem);
                listRef.current.resetAfterIndex(0); // обновление виртуального списка
            }
        });
        return () => sortable.destroy();
    }, [data]);

    return (
        <List
            height={500}
            itemCount={data.length}
            itemSize={50}
            width={300}
            ref={listRef}
        >
            {({ index, style }) => (
                <div style={style} key={data[index].id}>
                    {data[index].text}
                </div>
            )}
        </List>
    );
}

Рекомендации по оптимизации

  • Использовать фиксированную высоту элементов, чтобы виртуализация корректно рассчитывала видимые элементы.
  • Минимизировать перерендеринг при каждом событии onEnd.
  • В крупных списках применять делегирование событий и не инициализировать Sortable на каждом элементе.
  • Ghost и placeholder должны быть настроены так, чтобы не влиять на производительность виртуального рендера.

Заключение по применению

Виртуализация в сочетании с SortableJS позволяет работать с очень большими списками, сохраняя плавность интерфейса. Основная задача — корректная синхронизация DOM с данными и управление ghost/placeholder элементами, а также интеграция с библиотекой виртуализации для динамического рендера. Это обеспечивает стабильное и отзывчивое drag-and-drop взаимодействие даже при тысячах элементов.