Async/await паттерны

Mithril предоставляет минималистичный и высокопроизводительный способ работы с пользовательскими интерфейсами. Асинхронные операции в этом фреймворке часто связаны с загрузкой данных, обработкой запросов к API и взаимодействием с внешними сервисами. Для управления асинхронностью в JavaScript используется современный синтаксис async/await, который позволяет писать код, читаемый как синхронный, сохраняя при этом преимущества асинхронного выполнения.

Основы async/await

async и await — это синтаксический сахар над промисами. Любая функция, помеченная как async, возвращает промис. Оператор await приостанавливает выполнение функции до разрешения промиса:

async function fetchData(url) {
    const response = await fetch(url);
    return response.json();
}

В этом примере вызов fetch возвращает промис, и выполнение кода «ждёт» его завершения. Возврат функции автоматически оборачивается в промис, что упрощает цепочки асинхронных операций.

Интеграция async/await с Mithril запросами

Mithril предоставляет собственный метод m.request для выполнения HTTP-запросов. Он возвращает промис, что позволяет использовать await для асинхронного получения данных:

import m from 'mithril';

const UserService = {
    users: [],
    async loadUsers() {
        try {
            this.users = await m.request({
                method: "GET",
                url: "/api/users"
            });
        } catch (error) {
            console.error("Ошибка загрузки пользователей:", error);
        }
    }
};

Использование async/await здесь делает код более линейным и читаемым по сравнению с использованием .then() и .catch().

Асинхронные компоненты

В Mithril компоненты — это объекты с методом view или функции, возвращающие виртуальный DOM. Асинхронные операции внутри компонентов часто используются для загрузки данных перед рендерингом:

const UserList = {
    oninit: async vnode => {
        await UserService.loadUsers();
        m.redraw();
    },
    view: vnode => {
        return m("ul", UserService.users.map(user =>
            m("li", user.name)
        ));
    }
};

Ключевой момент: асинхронная инициализация через oninit не блокирует основной поток выполнения, а m.redraw() вручную обновляет DOM после завершения асинхронного действия.

Обработка ошибок

При использовании async/await обработка ошибок осуществляется через try/catch. Это позволяет централизованно управлять исключениями и отображать пользователю соответствующие сообщения:

async function fetchWithErrorHandling(url) {
    try {
        const data = await m.request({ method: "GET", url });
        return data;
    } catch (err) {
        console.error("Запрос не выполнен:", err);
        return [];
    }
}

Важно учитывать, что промисы, которые отклоняются, должны быть перехвачены, иначе они приведут к необработанной ошибке и могут нарушить работу компонента.

Параллельное выполнение асинхронных операций

Если требуется выполнить несколько независимых запросов одновременно, можно использовать Promise.all в сочетании с await:

async function loadData() {
    const [users, posts] = await Promise.all([
        m.request({ method: "GET", url: "/api/users" }),
        m.request({ method: "GET", url: "/api/posts" })
    ]);
    return { users, posts };
}

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

Асинхронные циклы и обработка последовательностей

Иногда требуется обработка массива асинхронных операций по порядку. Для этого используют цикл for...of вместе с await:

async function processUsers(users) {
    for (const user of users) {
        const details = await m.request({
            method: "GET",
            url: `/api/users/${user.id}`
        });
        console.log(details);
    }
}

В отличие от forEach, использование for...of позволяет корректно ждать завершения каждого запроса перед переходом к следующему.

Интеграция с подписками и потоками данных

Асинхронные операции могут сочетаться с потоками данных и подписками на изменения состояния. Например, сервис может предоставлять метод для обновления данных через событие:

const UserService = {
    users: [],
    async refreshUsers() {
        this.users = await m.request({ method: "GET", url: "/api/users" });
        m.redraw();
    },
    subscribe(callback) {
        this._callback = callback;
    },
    notify() {
        if (this._callback) this._callback(this.users);
    }
};

Асинхронное обновление данных и вызов m.redraw() обеспечивает синхронизацию состояния с представлением, а подписки позволяют компонентам реагировать на изменения без прямого вмешательства.

Паттерн «загрузка с индикатором»

Для улучшения UX важно отображать состояние загрузки. Это реализуется через булевый флаг и асинхронные методы:

const UserList = {
    loading: true,
    oninit: async vnode => {
        try {
            UserList.users = await UserService.loadUsers();
        } finally {
            UserList.loading = false;
            m.redraw();
        }
    },
    view: vnode => UserList.loading
        ? m("p", "Загрузка...")
        : m("ul", UserList.users.map(u => m("li", u.name)))
};

Флаг loading позволяет динамически переключать отображение между индикатором и данными.

Асинхронные маршруты

Mithril поддерживает асинхронные операции при инициализации маршрутов. Можно использовать onmatch для загрузки данных перед рендерингом компонента:

m.route(document.body, "/users", {
    "/users": {
        onmatch: async () => {
            await UserService.loadUsers();
            return UserList;
        }
    }
});

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

Рекомендации по производительности

  • Использовать await только там, где необходимо последовательное выполнение.
  • Для параллельных независимых операций применять Promise.all.
  • В компонентах избегать долгих асинхронных операций в view, лучше инициировать их в oninit или oncreate.
  • Всегда оборачивать асинхронные вызовы в try/catch для предотвращения необработанных ошибок.

Асинхронные паттерны с использованием async/await делают код в Mithril более читаемым, поддерживаемым и безопасным, обеспечивая плавное взаимодействие с внешними API и управлением состоянием приложения.