Справочник по JS

Делаем промис - шаг 2

Продолжаем писать собственную реализацию промиса. В этом шаге мы создадим функцию then.

then

then принимает две функции-колбэка:

  • onFulfilled — вызывается при успешном выполнении промиса,
  • onRejected — вызывается при ошибке.

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

class MyPromise {
    /* ... констркутор итд */

    // наш then возвращает новый промис
    then(onFulfilled, onRejected) {
        return new MyPromise((resolve, reject) => {

            if (onFulfilled) {
                // вызываем callback со значением исходного промиса
                const result = onFulfilled(this.value); 
                resolve(result);
            }

            if (onRejected) {/* сделаем позже*/}
        });
    }
}

На первый взгляд кажется, что всё почти готово, но такой вариант неправильный. Разберёмся почему.

Проблема 1: обработчики then вызываются синхронно

Сейчас вызов resolve(result) происходит синхронно, хотя по спецификации колбэки then всегда должны выполняться асинхронно, через очередь микро-задач. А сейчас у нас это работает так:

new Promise(r => r(10)).then(console.log);
console.log("end");
Вывод:
10
end
А должно быть наоборот

Проблема 2: Мы не учитываем, что значение промиса может быть доступно не сразу

Вот пример, где это проявляется:

new Promise(resolve => {
    setTimeout(() => {
        console.log("1 - вызываем resolve");
        resolve(100);
    }, 1000);
})
    .then(value => console.log("2) then получил " + value))
Спустя 1 секунду вывод:
then получил - undefined
1 - вызываем resolve

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

Исправим эти две проблемы.

Вспомогательная функция для асинхронного вызова

Создадим функцию-обертку, чтобы вызывать задачи асинхронно.

function runAsync(fn) { // принимает на вход другую фунцию
    queueMicrotask(fn); // и кладет ее в очередь микро-задач
}

Обновим then

class MyPromise {
    /* констркутор итд */

    then(onFulfilled, onRejected) { 
        return new MyPromise((resolve, reject) => {

            // создадим функцию-обработчик для колбэка onFulfilled
            const handleFillfilled = () => { 
                if (onFulfilled) {
                    const result = onFulfilled(this.value);
                    resolve(result);
                }
            }
            
            // далее решим, что делать с этой функцией обработчик в зависимости от состояния промиса
            if (this.state === "fulfilled") { // если промис выполнен успешно
                // то вызываем обработчик колбэка, но асинхронно
                runAsync(handleFillfilled); 
            }
            
            if (this.state === "pending") {
                // ЧТО ДЕЛАТЬ ЗДЕСЬ?
            }
            
            if (this.state === "rejected") {
                // напишем позже, когда напишем обработчик для колбэка onRejected 
            }


            if (onRejected) {/* сделаем позже*/}
        });
    }
}

Но чтобы это работало, нам нужно подготовить конструктор.

Доработка конструктора: хранилища обработчиков

Когда промис находится в pending, мы не можем выполнить обработчики - их надо где-то хранить. Для этого создадим в конструкторе два массива:

  • thenHandlers,
  • catchHandlers.

А при вызове resolve() и reject() — выполним все накопленные обработчики асинхронно.

constructor(executor) { 
    this.state = "pending"; 
    this.value = undefined; 
    this.reason = undefined; 
    this.thenHandlers = []; // добавили массив для обработчиков fulfilled
    this.catchHandlers = []; // добавили массив для обработчиков reject
    
    const resolve = (value) => {
        if (this.state !== "pending") return;
        this.state = "fulfilled";
        this.value = value;
        runAsync(() => this.thenHandlers.forEach(callback => callback())); // выполняем асихронно все колбэки
    }
    
    const reject = (reason) => { 
        if (this.state !== "pending") return;
        this.state = "rejected";
        this.reason = reason;
        runAsync(() => this.catchHandlers.forEach(callback => callback())); // выполняем асихронно все колбэки 
    }
    
    // кроме этого сделаем более безопасным вызов executor - функции
    try {
       executor(resolve, reject); // вызываем функцию executor синхронно
    } catch(e) {
      reject(e);
    }
}

Теперь, если then вызвали в момент, когда промис ещё в pending, мы просто кладём обработчик в массив - и позже resolve() их вызовет.

if (this.state === "pending") {
    // ЧТО ДЕЛАТЬ ЗДЕСЬ?
    this.thenHandlers.push(handleFulfilled)
}

Так уже лучше: наш then уже работает асинхронно, и не выполняется если состояние "родительского" промиса pending. Но к then есть еще требования:

  1. Если на вход пришла не функция, а значение, то нужно это значение просто зарезолвить.
  2. Должна быть обработка случая, когда результатом вызова onFulfilled является промис.

Реализуем первый пункт.

const handleFulfilled = () => {
    // добавим проверку, функция ли пришла в качестве колбэка
    if (typeof onFulfilled !== "function") {
        // и если нет, то просто зарезолвим value
        resolve(this.value); 
        return;
    }   
    
    try { // сделаем вызов более безопасным
        const result = onFulfilled(this.value);
        resolve(result);                    
    } catch (e) {
        reject(e);
    }
}

Проверяем, работает ли асинхронность и цепочки

new MyPromise((resolve, reject) => {
    console.log("promise start");
    resolve(1);
}).then((value) => {
    console.log("then 1")
    console.log(value);
    return value + 1;
}).then((value) => {
    console.log("then 2")
    console.log(value);
});

console.log("sync")
Вывод:
promise start
sync
then 1
1
then 2
2

Итог

  • then вызывается асинхронно,
  • обработчики не теряются, если промис ещё в pending,
  • цепочки работают,
  • обработчики вызываются корректно в зависимости от состояния промиса,
  • если передан не колбэк, значение просто пробрасывается дальше.

Ссылка на весь код на CodePen

Но важной части пока не хватает:

Что делать, если onFulfilled возвращает ещё один промис?

Это критический момент, сделаем это в шаге №3.

27-11-2025
Сергей Железников

Другие материалы по теме