30 минут, чтобы вы полностью поняли принцип Promise

JavaScript Promise

Оригинальная ссылка

предисловие

Некоторое время назад я записал некоторые распространенные варианты использования промисов, а эта статья идет на один уровень глубже, чтобы проанализировать, как реализован механизм правил промисов. ps: Эта статья подходит для людей, которые уже знакомы с использованием промисов.Если вы мало знаете об их использовании, вы можете перейти к моей предыдущей статье.Сообщение блога.

Исходный код обещания этой статьи основан наСпецификация Обещание/A+писать (не хочу видеть английскую версию ходаКитайский перевод спецификации Promise/A+)

Введение

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

Рассмотрим следующую обработку запроса, чтобы получить идентификатор пользователя.

//例1
function getUserId() {
    return new Promise(function(resolve) {
        //异步请求
        http.get(url, function(results) {
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些处理
})

getUserIdметод возвращаетpromise, через которыйthenрегистрация метода (примечание注册слово) вpromiseОбратный вызов для выполнения при успешном выполнении асинхронной операции. Этот метод выполнения делает асинхронные вызовы очень простыми.

Принципиальный анализ

Тогда что-то вроде этогоPromiseКак этого добиться?其实按照上面一句话,实现一个最基础的雏形还是很easy的。

Предельно простое обещание

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

Приведенный выше код очень прост, и общая логика выглядит следующим образом:

  1. передачаthenметод, захочетсяPromiseОбратный вызов, который выполняется при успешном выполнении асинхронной операции, помещается вcallbacksОчередь, по сути, заключается в регистрации callback-функции, которую можно мыслить в сторону режима наблюдателя;
  2. СоздайтеPromiseФункция, переданная в экземпляре, получит параметр типа функции, то естьresolve, который получает значение параметра, представляющее результат, возвращаемый асинхронной операцией. Когда один шаг операции выполнен успешно, пользователь вызоветresolveметод, фактическая операция в настоящее время заключается вcallbacksОбратные вызовы в очереди выполняются один за другим;

можно комбинировать例1Из кода в , сначалаnew Promiseкогда перейти кpromiseФункция, которая отправляет асинхронный запрос, а затем вызываетpromiseобъектthenАтрибут, зарегистрируйте функцию обратного вызова для успешного запроса, а затем вызовите, когда асинхронный запрос будет успешно отправлен.resolve(results.id)метод, который выполняетthenМассив обратных вызовов, зарегистрированных методом.

Я считаю, что внимательный человек должен уметь это видеть,thenМетоды должны иметь возможность связываться в цепочку, но самая основная и простая версия выше, очевидно, не поддерживает цепочку. хочу сделатьthenМетод поддерживает связанные вызовы, что на самом деле очень просто:

this.then = function (onFulfilled) {
    callbacks.push(onFulfilled);
    return this;
};

См. Цепной вызов, подобный следующему, может быть достигнут с помощью простого предложения:

// 例2
getUserId().then(function (id) {
    // 一些处理
}).then(function (id) {
    // 一些处理
});

Добавьте механизм задержки

Внимательные студенты должны обнаружить, что с приведенным выше кодом может быть проблема: еслиthenПрежде чем метод зарегистрирует обратный вызов,resolveФункция выполнена, что делать? НапримерpromiseВнутренняя функция является синхронной функцией:

// 例3
function getUserId() {
    return new Promise(function (resolve) {
        resolve(9876);
    });
}
getUserId().then(function (id) {
    // 一些处理
});

Это явно запрещено, т.Promises/A+Спецификация явно требует, чтобы обратные вызовы выполнялись асинхронно, чтобы обеспечить согласованный и надежный порядок выполнения. Итак, мы собираемся добавить некоторую обработку, чтобы убедиться, чтоresolveПеред казнью,thenМетод зарегистрировал все обратные вызовы. Мы можем изменить это такresolveфункция:

function resolve(value) {
    setTimeout(function() {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }, 0)
}

Идея приведенного выше кода также очень проста, то есть черезsetTimeoutмеханизм, воляresolveЛогика, которая выполняет обратный вызов вJSконец очереди задач, чтобы убедиться, чтоresolveПри выполнении,thenФункция обратного вызова метода зарегистрирована.

Однако, похоже, проблема все же есть, над ней можно подумать: еслиPromiseАсинхронная операция выполнена успешно. В это время будут выполнены обратные вызовы, зарегистрированные до успешной асинхронной операции.PromiseВызывается после успешного выполнения асинхронной операции.thenЗарегистрированный обратный вызов больше никогда не будет выполняться, а это явно не то, чего мы хотим.

присоединиться к состоянию

Итак, чтобы решить проблему, поднятую в предыдущем разделе, мы должны добавить механизм состояния, известный какpending,fulfilled,rejected.

Promises/A+2.1 в спецификацииPromise Statesчетко оговаривается,pendingможет быть преобразован вfulfilledилиrejectedи может быть преобразован только один раз, то есть, еслиpendingПеревести вfulfilledсостояние, то оно уже не может быть преобразовано вrejected. а такжеfulfilledа такжеrejectedсостояние можно определить толькоpendingДва не могут быть преобразованы друг в друга. Одна картинка стоит тысячи слов:

alt promise state
alt promise state

Улучшенный код выглядит следующим образом:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        if (state === 'pending') {
            callbacks.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };

    function resolve(newValue) {
        value = newValue;
        state = 'fulfilled';
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                callback(value);
            });
        }, 0);
    }

    fn(resolve);
}

Идея приведенного выше кода заключается в следующем:resolveПри выполнении установит состояние наfulfilled, после этого звонкаthenДобавленные новые обратные вызовы выполняются немедленно.

Здесь нет местаstateустановить какrejected, чтобы все могли сосредоточиться на основном коде, после этого вопроса будет специальный раздел.

Связанные обещания

Итак, снова возникает проблема: если пользователь регистрируется в функции then, она по-прежнемуPromise, как решить? Например, следующее例4:

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 对job的处理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}

Я считаю, что этот сценарий был использованpromiseВсе знают, что их будет много, поэтому примерно так и получается так называемая цепочкаPromise.

цепьPromiseзначит на данный моментpromiseдостигатьfulfilledстатус, начать следующийpromise(рядом сpromise). Итак, как нам связаться с текущимpromiseи соседиpromiseШерстяная ткань? (Это сложный момент здесь).

На самом деле, это не так уж и сложно, еслиthenвнутри методаreturnОдинpromiseПросто хорошо.Promises/A+Вот что написано в спецификации 2.2.7 (смайлик)~

Вот взгляд на эту скрытую тайнуthenМетоды иresolveКод модификации метода:


function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        return new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }
        //如果then中没有传递任何东西
        if(!callback.onFulfilled) {
            callback.resolve(value);
            return;
        }

        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }


    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve);
}

мы объединяем例4код, проанализируйте логику приведенного выше кода, для удобства чтения ставлю例4Код размещен здесь:

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 对job的处理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}
  1. thenВ методе создайте и верните новыйPromiseнапример, это сериалPromiseи поддерживает связанные вызовы.
  2. handleпутьpromiseвнутренний метод.thenПараметры, переданные в методonFulfilledи создавать новыеPromiseпрошел в экземпляреresolveоба былиpushк текущемуpromiseизcallbacksочередь, которая является текущим соединениемpromiseи соседиpromiseКлюч (здесь мы должны проанализировать роль ручки).
  3. getUserIdСгенерированоpromise(упоминается какgetUserId promise) асинхронная операция завершается успешно и выполняется ее внутренний методresolve, входящий параметр является результатом асинхронной операцииid
  4. передачаhandleметод обработкиcallbacksОбратные вызовы в очереди:getUserJobByIdМетод, генерировать новыйpromise(getUserJobById promise)
  5. перед исполнениемgetUserId promiseизthenметод сгенерировал новыйpromise(называетсяbridge promise)изresolveметод, входные параметрыgetUserJobById promise. В этом случаеresolveметод передан вgetUserJobById promiseизthenметод и возврат напрямую.
  6. существуетgetUserJobById promiseКогда асинхронная операция завершится успешно, выполните ееcallbacksобратный вызов через:getUserId bridge promiseсерединаresolveметод
  7. окончательное исполнениеgetUserId bridge promiseпо соседствуpromiseизcallbacksобратный звонок в .

Чтобы быть более простым, вы можете посмотреть на следующую картинку, картинка стоит тысячи слов (все нарисовано в соответствии с вашим собственным пониманием, пожалуйста, поправьте меня, если я ошибаюсь):

alt promise analysis
alt promise analysis

обработка отказов

При сбое асинхронной операции отметьте ее статус какrejectedи выполните зарегистрированный обратный вызов ошибки:

//例5
function getUserId() {
    return new Promise(function(resolve) {
        //异步请求
        http.get(url, function(error, results) {
            if (error) {
                reject(error);
            }
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些处理
}, function(error) {
    console.log(error)
})

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

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled, onRejected) {
        return new Promise(function (resolve, reject) {
            handle({
                onFulfilled: onFulfilled || null,
                onRejected: onRejected || null,
                resolve: resolve,
                reject: reject
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }

        var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
            ret;
        if (cb === null) {
            cb = state === 'fulfilled' ? callback.resolve : callback.reject;
            cb(value);
            return;
        }
        ret = cb(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve, reject);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        execute();
    }

    function reject(reason) {
        state = 'rejected';
        value = reason;
        execute();
    }

    function execute() {
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve, reject);
}

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

Всплытие ошибок — это функция, которую приведенный выше код уже поддерживает и которая очень полезна. существуетhandleКогда обнаруживается, что обратный вызов для отказа асинхронной операции не указан, он будет напрямуюbridge promise(thenвозвращается функциейpromise, то же самое ниже) установлен наrejectedСтатус, чтобы добиться эффекта выполнения последующих обратных вызовов с ошибкой. Это помогает упростить сериализациюPromiseПоскольку набор асинхронных операций часто соответствует реальной функции, метод обработки сбоев обычно непротиворечив:

//例6
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 处理job
    }, function (error) {
        // getUserId或者getUerJobById时出现的错误
        console.log(error);
    });

Обработка исключений

Внимательные студенты подумают: а что, если в коде есть ошибка при выполнении успешного обратного вызова и неудачного обратного вызова? Для таких исключений вы можете использоватьtry-catchпоймать ошибку иbridge promiseустановить какrejectedусловие.handleМетод модифицируется следующим образом:

function handle(callback) {
    if (state === 'pending') {
        callbacks.push(callback);
        return;
    }

    var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
        ret;
    if (cb === null) {
        cb = state === 'fulfilled' ? callback.resolve : callback.reject;
        cb(value);
        return;
    }
    try {
        ret = cb(value);
        callback.resolve(ret);
    } catch (e) {
        callback.reject(e);
    } 
}

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

Суммировать

Когда я впервые читал исходный код промиса, я не очень хорошо понимал механизм работы функций then и resolve, но если успокоиться и вывести его по логике выполнения промиса, то понять несложно. Здесь следует обратить внимание на то, что функция then в промисе регистрирует только тот код, который необходимо выполнить позже, реальное выполнение выполняется в методе разрешения, после уточнения этого слоя это сэкономит много усилий. для анализа исходного кода.

Теперь рассмотрим процесс реализации Promise, который в основном использует шаблон наблюдателя в шаблоне проектирования:

  1. Метод наблюдателя регистрируется в объекте Promise наблюдателя через Promise.Prototype.Then и Promise.Prototype.catch, и для вызова возвращается новый объект Promise.
  2. Наблюдатель управляет внутренними ожидающими, выполненными и отклоненными переходами состояний, и в то же время он активно запускает переходы состояний и уведомляет наблюдателя с помощью методов разрешения и отклонения, переданных в конструкторе.

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

Глубокое понимание промисов
JavaScript Promises ... In Wicked Detail