Всем доброго времени суток!

Началось всё с того, что в качестве тестового задания на собеседованиях, я начал просить соискателей реализовать предзагрузчик картинок на JS. Помимо самой предзагрузки, скрипт должен был уметь подставлять fallback-картинку, если нужная картинка не могла быть загружена. Обязательным условием было — использование ES6 Promise.
Затем я подумал: «А почему бы самому не реализовать такой предзагрузчик и не выложить в общее пользование? Да это же еще и отличный повод написать статью на Хабр!».
Собственно, под катом описание логики работы такого предзагрузчика + ссылка на сам предзагрузчик.

Для начала, давайте вспомним — что такое промис в JS.

Про промисы
Промис — это, в первую очередь, способ организации асинхронного кода. Хотя и не обязательно асинхронного…
Для создания промиса необходима функция, которая будет выполнена сразу же, после создания промиса.
Наша задача, внутри этой функции, вызвать один из двух методов, передаваемых в эту функцию автоматически — resolve или reject.
Вызовом одного из этих методов, мы говорим промису о статусе задачи: выполнена успешно (resolve) или неудачно (reject).
Делается это для того, чтобы можно было построить цепочку дальнейших действий в случае успешного или не успешного выполнения задачи.

var p = new Promise(function(resolve, reject) {
    someAsycOperation(function(e, result) {
        if (e) {
            reject(e);
        } else {
            resolve(result);
        }
    });
});


Про then
У каждого промиса есть метод then, который принимает, в качестве аргументов, две функции (then-callbacks).
Первая, ее еще называют onFulfilled callback — будет выполнена в случае успешного выполнения задачи, а вторая, ее еще называют onRejected callback — в случае провала задачи.
Но пока мы не сообщим промису о статусе задачи, ни одна этих двух функций вызвана не будет.
Метод then возвращает другой промис, который так же может быть resolved или rejected и на который тоже можно повесить then.
И так по кругу…

Если, при вызове resolve или reject, передать какой-то аргумент, то этот аргумент будет проброшен в следующий then-callback, который будет выполнен после завершения текущей задачи.

var p = new Promise(function(resolve, reject) {
    someAsyncOperation(function(e, result) {
        if (e) {
            reject(e);
        } else {
            resolve(result);
        }
    });
}).then(function(value) {
    //on success
    ....
}, function(e) {
    //on fail
    console.error(e);
});


Then-callback так же может пробросить какое-то значение в следующий then-callback, просто вернув его при помощи оператора return;

var p = new Promise(function(resolve, reject) {
    someAsyncOperation(function(e, result) {
        if (e) {
            reject(e);
        } else {
            resolve(result);
        }
    });
}).then(function(value) {
    //on success
    ...

    return 'some value';
}, function(e) {
    //on fail
    console.error(e);
}).then(function(value) {
    //value === 'some value' в случае успешного выполнения асинхронной операции
});


Про состояния
Внутри стартовой функции, у нас есть всего 3 способа сообщить о результате задачи.
Выполнено успешно:
  • вызвать resolve


Выполнено неудачно:
  • вызвать reject
  • выбросить исключение


Для функций внутри then есть небольшое правило:


Другими словами: если then-callback выбрасывает исключение или возвращает другой промис, который будет в состоянии rejected, то и весь промис перейдет в статус rejected, во всех остальных случаях, промис будет в статусе resolved (даже если then-callback ничего не возвращает).
Посмотрите на правило для then-callback, а потом на последний пример…

Получается, что, если асинхронная операция будет выполнена успешно, то выполнится onFulfilled callback в первом then, а далее onFulfilled callback во втором then.
Но что, если асинхронная операция завершится неудачей? Выполнится onRejected callback в первом then, а затем(внимание!) onFulfilled callback во втором then.
Почему? Смотрите выше правило для then-callback.
Исходя из него — чтобы вызвать следующий onRejected callback(которого кстати нет), необходимо: либо вернуть промис, который будет rejected, либо выбросить исключение.

Кстати, если как-то так получилось, что вам нужно повесить на промис только onRejected callback, то есть shorthand-метод catch()

var p = new Promise(function(resolve, reject) {
    console.log('Начало асинхронной операции');

    someAsyncOperation(function(e, result) {
        if (e) {
            reject(e);
        } else {
            resolve(result);
        }
    });
}).catch(function(e) {
    console.log('Асинхронной операция провалена');
    console.error(e);
}).then(function() {
    console.log('Асинхронная операция завершена!');
});


Либо, если используете then, вместо onFulfilled-callback можно просто передать null.

Но вернемся к теме… посмотрите что получается…
onRejected callback в первом then играет роль эдакого fallback-действия. Затем, цепочка then выполняется дальше, как если бы асинхронная операция была выполнена успешно.

var p = new Promise(function(resolve, reject) {
    console.log('Начало асинхронной операции');

    someAsyncOperation(function(e, result) {
        if (e) {
            reject(e);
        } else {
            resolve(result);
        }
    });
}).then(function(result) {
    console.log('Асинхронная операция выполнена успешно');
}, function(e) {
    console.log('Асинхронная операция провалена');
    console.error(e);
}).then(function() {
    console.log('Асинхронная операция завершена!');
});


Получается, что промисы в JS уже «из коробки» поддерживают fallback-действия.

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

Кстати, предзагрузчик использует allSettled-функцию, работа которой основывается на том же принципе fallback-действий.
Так же рекомендую ознакомиться с кодом.

Спасибо за внимание!

P.S. не поленитесь — почитайте документацию по промисам!

Комментарии (44)