В моята първа публикация от тази поредица обясних как работи конструкторът Promise, като го пресъздадох като конструктор Pledge. Във втория пост от тази поредица обясних как работят асинхронните операции в обещания чрез задания. Ако все още не сте прочели тези две публикации, предлагам да го направите, преди да продължите с тази.

Тази публикация се фокусира върху прилагането на then(), catch() и finally() съгласно ECMA-262. Тази функционалност е изненадващо ангажирана и разчита на много помощни класове и помощни програми, за да накара нещата да работят правилно. Въпреки това, след като овладеете няколко основни концепции, имплементациите са сравнително ясни.

Като напомняне, тази поредица е базирана на моята библиотека с обещания „Pledge“. Можете да видите и изтеглите целия изходен код от GitHub.

Методът then()

Методът then() за обещания приема два аргумента: манипулатор на изпълнение и манипулатор на отхвърляне. Терминът манипулатор се използва за описание на функция, която се извиква в отговор на промяна във вътрешното състояние на обещание, така че манипулаторът на изпълнение се извиква, когато обещанието е изпълнено, а манипулаторът на отхвърляне се извиква, когато обещание е отхвърлено. Всеки от двата аргумента може да бъде зададен като undefined, за да ви позволи да зададете единия или другия, без да изисквате и двата.

Стъпките, предприети при извикване на then(), зависят от състоянието на обещанието:

  • Ако състоянието на обещанието е чакащо (обещанието е неуредено), then() просто съхранява манипулаторите, които да бъдат извикани по-късно.
  • Ако състоянието на обещанието е изпълнено, then() незабавно поставя задача в опашка за изпълнение на манипулатора на изпълнение.
  • Ако състоянието на обещанието е отхвърлено, then() незабавно поставя в опашка задача за изпълнение на манипулатора на отхвърляне.

Освен това, независимо от състоянието на обещанието, then() винаги връща друго обещание, поради което можете да свържете обещанията заедно по следния начин:

const promise = new Promise((resolve, reject) => {
    resolve(42);
});

promise.then(value1 => {
    console.log(value1);
    return value1 + 1;
}).then(value2 => {
    console.log(value2);
});

В този пример promise.then() добавя манипулатор на изпълнение, който извежда стойността на разделителната способност и след това връща друго число въз основа на тази стойност. Второто then() извикване всъщност е на второ обещание, което е разрешено с помощта на върнатата стойност от предходния манипулатор на изпълнение. Именно това поведение прави внедряването на then() един от по-сложните аспекти на обещанията и затова има малка група помощни класове, необходими за правилното внедряване на функционалността.

Записът PromiseCapability

Спецификацията дефинира PromiseCapability запис [1] като притежаващ следните само вътрешни свойства:

  • [[Promise]] — Обект, който може да се използва като обещание.
  • [[Resolve]] — Функцията, която се използва за разрешаване на даден обещаващ обект.
  • [[Reject]] — Функцията, която се използва за отхвърляне на даден обещаващ обект.

Ефективно записът PromiseCapability се състои от обещаващ обект и функциите resolve и reject, които променят вътрешното му състояние. Можете да мислите за това като за помощен обект, който позволява по-лесен достъп до промяна на състоянието на обещание.

Заедно с дефиницията на записа PromiseCapability има и дефиницията на функция NewPromiseCapability() [2], която очертава стъпките, които трябва да предприемете, за да създадете нов запис PromiseCapability. На функцията NewPromiseCapability() се предава един аргумент, C, който е функция, за която се предполага, че е конструктор, който приема функция изпълнител. Ето опростен списък от стъпки:

  1. Ако C не е конструктор, извежда грешка.
  2. Създайте нов PromiseCapability запис с всички вътрешни свойства, зададени на undefined.
  3. Създайте изпълнителна функция, която да премине към C.
  4. Съхранявайте препратка към PromiseCapability на изпълнителя.
  5. Създайте ново обещание с помощта на изпълнителя и го извлечете resolve и reject функции.
  6. Съхранявайте функциите resolve и reject на PromiseCapability.
  7. Ако resolve не е функция, хвърлете грешка.
  8. Ако reject не е функция, хвърлете грешка.
  9. Съхранявайте обещанието на PromiseCapability.
  10. Върнете PromiseCapability

Реших да използвам клас PledgeCapability, за да внедря както PromiseCapability, така и NewPromiseCapability(), правейки го по-идиоматичен за JavaScript. Ето кода:

export class PledgeCapability {

    constructor(C) {

        const executor = (resolve, reject) => {
            this.resolve = resolve;
            this.reject = reject;
        };

        // not used but included for completeness with spec
        executor.capability = this;

        this.pledge = new C(executor);

        if (!isCallable(this.resolve)) {
            throw new TypeError("resolve is not callable.");
        }

        if (!isCallable(this.reject)) {
            throw new TypeError("reject is not callable.");
        }
    }
}

Най-интересната част от конструктора и частта, която ми отне най-много време да разбера, е, че функцията executor се използва просто за хващане на препратки към функциите resolve и reject, които се предават. Това е необходимо, защото не знаете какво е C. Ако C винаги беше Promise, тогава можете да използвате createResolvingFunctions(), за да създадете resolve и reject. Въпреки това, C може да бъде подклас на Promise, който променя начина, по който се създават resolve и reject, така че трябва да вземете действителните функции, които се предават.

Бележка относно дизайна на този клас: избрах да използвам имена на свойства на низове, вместо да се занимавам със създаването на имена на свойства на символи, за да представят, че тези свойства са предназначени да бъдат само вътрешни. Въпреки това, тъй като този клас не е изложен като част от API, няма риск някой случайно да препрати тези свойства извън библиотеката. Като се има предвид това, реших да предпочитам четливостта на имената на свойствата на низовете пред по-технически правилните имена на свойствата на символите.

Класът PledgeCapability се използва по следния начин:

const capability = new PledgeCapability(Pledge);

capability.resolve(42);
capability.pledge.then(value => {
    console.log(value);
});

В този пример конструкторът Pledge се предава на PledgeCapability за създаване на нов екземпляр на Pledge и извличане на неговите resolve и reject функции. Това се оказва важно, защото не знаете класа, който да използвате, когато създавате върнатата стойност за then() до момента на изпълнение.

Използване на Symbol.species

Добре познатият символ Symbol.species не се разбира добре от разработчиците на JavaScript, но е важно да се разбере в контекста на обещанията. Всеки път, когато метод на обект трябва да върне екземпляр от същия клас, спецификацията дефинира статичен Symbol.species getter за класа. Това е вярно за много JavaScript класове, включително масиви, където методи като slice() и concat() връщат масиви, а също така е вярно и за обещания, където методи като then() и catch() връщат друго обещание. Това е важно, защото ако подкласирате Promise, вероятно искате then() да върне екземпляр на вашия подклас, а не екземпляр на Promise.

Спецификацията определя стойността по подразбиране за Symbol.species да бъде this за всички вградени класове, така че класът Pledge прилага това свойство, както следва:

export class Pledge {

    // constructor omitted for space

    static get [Symbol.species]() {
        return this;
    }

    // other methods omitted for space
}

Имайте предвид, че тъй като инструментът за получаване на Symbol.species е статичен, this всъщност е препратка към Pledge (можете да го изпробвате сами, като влезете в Pledge[Symbol.species]). Въпреки това, тъй като this се оценява по време на изпълнение, той ще има различна стойност за подклас, като например тази:

class SuperPledge extends Pledge {
    // empty
}

Използвайки този код, SuperPledge[Symbol.species] се оценява на SuperPledge. Тъй като this се оценява по време на изпълнение, той автоматично препраща към конструктора на класа, който се използва. Точно затова спецификацията дефинира Symbol.species по този начин: това е удобство за разработчиците, тъй като използването на същия конструктор за връщаните стойности на метода е често срещан случай.

Сега, след като разбирате добре Symbol.species, е време да продължите с прилагането на then().

Прилагане на метода then()

Самият метод then() е доста кратък, защото делегира по-голямата част от работата на функция, наречена PerformPromiseThen(). Ето как спецификацията дефинира then()[3]:

  1. Нека promise е стойността this.
  2. Ако IsPromise(promise) е false, хвърлете изключение TypeError.
  3. Нека C е ? SpeciesConstructor(promise, %Promise%).
  4. Нека resultCapability е ? NewPromiseCapability(C).
  5. Връщане PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability).

И ето как кодирах този алгоритъм:

export class Pledge {

    // constructor omitted for space

    static get [Symbol.species]() {
        return this;
    }

    then(onFulfilled, onRejected) {

        assertIsPledge(this);

        const C = this.constructor[Symbol.species];
        const resultCapability = new PledgeCapability(C);
        return performPledgeThen(this, onFulfilled, onRejected, resultCapability);
    }

    // other methods omitted for space
}

Първото нещо, което трябва да отбележа е, че не съм дефинирал променлива за съхраняване на this, както алгоритъмът определя. Това е така, защото е излишно в JavaScript, когато имате директен достъп до this. След това останалата част от метода е директен превод в JavaScript. Конструкторът на вида се съхранява в C и от него се създава нов PledgeCapability. След това цялата информация се предава на performPledgeThen(), за да свърши истинската работа.

Функцията performPledgeThen() е една от по-дългите функции в библиотеката Pledge и прилага алгоритъма за PerformPromiseThen() в спецификацията. Алгоритъмът е малко труден за разбиране, но започва със следните стъпки:

  1. Твърде, че първият аргумент е обещание.
  2. Ако onFulfilled или onRejected не са функции, задайте ги на undefined.
  3. Създайте PromiseReaction записа за всеки от onFulfilled и onRejected.

Ето как изглежда този код в библиотеката на Pledge:

function performPledgeThen(pledge, onFulfilled, onRejected, resultCapability) {
    
    assertIsPledge(pledge);

    if (!isCallable(onFulfilled)) {
        onFulfilled = undefined;
    }

    if (!isCallable(onRejected)) {
        onRejected = undefined;
    }

    const fulfillReaction = new PledgeReaction(resultCapability, "fulfill", onFulfilled);
    const rejectReaction = new PledgeReaction(resultCapability, "reject", onRejected);

    // more code to come

}

Обектите fulfillReaction и rejectReaction винаги се създават, когато onFulfilled и onRejected са undefined. Тези обекти съхраняват цялата информация, необходима за изпълнение на манипулатор. (Имайте предвид, че само една от тези реакции някога ще бъде използвана. Или залогът е изпълнен, така че fulfillReaction се използва, или залогът е отхвърлен, така че rejectReaction се използва. Ето защо е безопасно да прехвърлите същия resultCapability и на двете, въпреки че съдържа само едно копие на Pledge.)

Класът PledgeReaction е еквивалентът на JavaScript на записа PromiseReaction в спецификацията и се декларира по следния начин:

class PledgeReaction {
    constructor(capability, type, handler) {
        this.capability = capability;
        this.type = type;
        this.handler = handler;
    }
}

Всички следващи стъпки в PerformPromiseThen() се основават на състоянието на обещанието:

  1. Ако състоянието е чакащо, запазете реакциите за по-късно.
  2. Ако състоянието е изпълнено, поставете на опашка задача за изпълнение на fulfillReaction.
  3. Ако състоянието е отхвърлено, поставете на опашка задача за изпълнение rejectReaction.

И след това има още две стъпки:

  1. Маркирайте обещанието като обработено (за проследяване на необработено отхвърляне, обсъдено в предстояща публикация).
  2. Върнете обещанието от resultCapability или върнете undefined, ако resultCapability е undefined.

Ето готовия performPledgeThen(), който изпълнява тези стъпки:

function performPledgeThen(pledge, onFulfilled, onRejected, resultCapability) {

    assertIsPledge(pledge);

    if (!isCallable(onFulfilled)) {
        onFulfilled = undefined;
    }

    if (!isCallable(onRejected)) {
        onRejected = undefined;
    }

    const fulfillReaction = new PledgeFulfillReaction(resultCapability, onFulfilled);
    const rejectReaction = new PledgeRejectReaction(resultCapability, onRejected);

    switch (pledge[PledgeSymbol.state]) {

        case "pending":
            pledge[PledgeSymbol.fulfillReactions].push(fulfillReaction);
            pledge[PledgeSymbol.rejectReactions].push(rejectReaction);
            break;

        case "fulfilled": 
            {
                const value = pledge[PledgeSymbol.result];
                const fulfillJob = new PledgeReactionJob(fulfillReaction, value);
                hostEnqueuePledgeJob(fulfillJob);
            }
            break;

        case "rejected":
            {
                const reason = pledge[PledgeSymbol.result];
                const rejectJob = new PledgeReactionJob(rejectReaction, reason);

                // TODO: if [[isHandled]] if false
                
                hostEnqueuePledgeJob(rejectJob);
            }
            break;

        default:
            throw new TypeError(`Invalid pledge state: ${pledge[PledgeSymbol.state]}.`);
    }

    pledge[PledgeSymbol.isHandled] = true;

    return resultCapability ? resultCapability.pledge : undefined;
}

В този код PledgeSymbol.fulfillReactions и PledgeSymbol.rejectReactions най-накрая се използват за нещо. Ако състоянието е чакащо, реакциите се съхраняват за по-късно, така че да могат да бъдат задействани, когато състоянието се промени (това се обсъжда по-късно в тази публикация). Ако състоянието е изпълнено или отхвърлено, тогава се създава PledgeReactionJob за изпълнение на реакцията. PledgeReactionJob преобразува в NewPromiseReactionJob()[4] в спецификацията и се декларира по следния начин:

export class PledgeReactionJob {
    constructor(reaction, argument) {
        return () => {
            const { capability: pledgeCapability, type, handler } = reaction;
            let handlerResult;

            if (typeof handler === "undefined") {

                if (type === "fulfill") {
                    handlerResult = new NormalCompletion(argument);
                } else {
                    handlerResult = new ThrowCompletion(argument);
                }
            } else {
                try {
                    handlerResult = new NormalCompletion(handler(argument));
                } catch (error) {
                    handlerResult = new ThrowCompletion(error);
                }
            }

            if (typeof pledgeCapability === "undefined") {
                if (handlerResult instanceof ThrowCompletion) {
                    throw handlerResult.value;
                }

                // Return NormalCompletion(empty)
                return;
            }

            if (handlerResult instanceof ThrowCompletion) {
                pledgeCapability.reject(handlerResult.value);
            } else {
                pledgeCapability.resolve(handlerResult.value);
            }

            // Return NormalCompletion(status)
        };
    }
}

Този код започва с извличане на цялата информация от reaction, която е предадена. Функцията е малко дълга, тъй като и capability, и handler могат да бъдат undefined, така че има резервни поведения във всеки от тези случаи.

Класът PledgeReactionJob също използва концепцията за запис за завършване[5]. В по-голямата част от кода успях да избегна необходимостта да препращам директно към записите за завършване, но в този код беше необходимо да съответствам по-добре на алгоритъма в спецификацията. Записът за завършване не е нищо повече от запис на това как е приключил контролният поток на операцията. Има четири вида завършване:

  • нормално — когато дадена операция е успешна без никаква промяна в контролния поток (операцията return или излизане в края на функция)
  • прекъсване — когато дадена операция приключи напълно (изявлението break)
  • продължи — когато дадена операция излезе и след това се рестартира (изявлението continue)
  • throw — когато дадена операция води до грешка (изявлението throw)

Тези записи за завършване казват на двигателя на JavaScript как (или дали) да продължи да изпълнява код. За да създам PledgeReactionJob, имах нужда само от нормални и хвърлящи завършвания, така че ги декларирах, както следва:

export class Completion {
    constructor(type, value, target) {
        this.type = type;
        this.value = value;
        this.target = target;
    }
}
export class NormalCompletion extends Completion {
    constructor(argument) {
        super("normal", argument);
    }
}

export class ThrowCompletion extends Completion {
    constructor(argument) {
        super("throw", argument);
    }
}

По същество NormalCompletion казва на функцията да излезе нормално (ако няма pledgeCapability) или да разреши обещание (ако pledgeCapability е дефинирано), а ThrowCompletion казва на функцията или да издаде грешка (ако няма pledgeCapability), или да отхвърли обещание (ако pledgeCapability е дефиниран). В библиотеката на Pledge pledgeCapability винаги ще бъде дефиниран, но исках да съответствам на оригиналния алгоритъм от спецификацията за пълнота.

Покриването на PledgeReactionJob означава, че функцията pledgePerformThen() е завършена и всички манипулатори ще бъдат правилно съхранени (ако състоянието на обещание е чакащо) или изпълнени незабавно (ако състоянието на обещание е изпълнено или отхвърлено). Последната стъпка е да се изпълнят всички реакции за запазване, когато състоянието на обещанието се промени от чакащо на изпълнено или отхвърлено.

Задействане на съхранени реакции

Когато дадено обещание премине от неуредено към уредено, то задейства съхранените реакции за изпълнение (реакции на изпълнение, ако обещанието е изпълнено, и реакции на отхвърляне, когато обещанието е отхвърлено). Спецификацията дефинира тази операция като TriggerPromiseReaction()[6] и това е един от по-лесните алгоритми за изпълнение. Целият алгоритъм основно итерира върху списък (масив в JavaScript) от реакции и след това създава и поставя на опашка нов PromiseReactionJob за всяка от тях. Ето как го внедрих като triggerPledgeReactions():

export function triggerPledgeReactions(reactions, argument) {

    for (const reaction of reactions) {
        const job = new PledgeReactionJob(reaction, argument);
        hostEnqueuePledgeJob(job);
    }

}

Най-важната част е да се предаде правилният аргумент reactions, поради което тази функция се извиква на две места: fulfillPledge() и rejectPledge() (обсъдени в част 1 от тази серия). И за двете функции задействането на реакции е последната стъпка. Ето кода за това:

export function fulfillPledge(pledge, value) {

    if (pledge[PledgeSymbol.state] !== "pending") {
        throw new Error("Pledge is already settled.");
    }

    const reactions = pledge[PledgeSymbol.fulfillReactions];

    pledge[PledgeSymbol.result] = value;
    pledge[PledgeSymbol.fulfillReactions] = undefined;
    pledge[PledgeSymbol.rejectReactions] = undefined;
    pledge[PledgeSymbol.state] = "fulfilled";

    return triggerPledgeReactions(reactions, value);
}

export function rejectPledge(pledge, reason) {

    if (pledge[PledgeSymbol.state] !== "pending") {
        throw new Error("Pledge is already settled.");
    }

    const reactions = pledge[PledgeSymbol.rejectReactions];

    pledge[PledgeSymbol.result] = reason;
    pledge[PledgeSymbol.fulfillReactions] = undefined;
    pledge[PledgeSymbol.rejectReactions] = undefined;
    pledge[PledgeSymbol.state] = "rejected";

    // global rejection tracking
    if (!pledge[PledgeSymbol.isHandled]) {
        // TODO: perform HostPromiseRejectionTracker(promise, "reject").
    }

    return triggerPledgeReactions(reactions, reason);
}

След това добавяне обектите Pledge правилно ще задействат съхранени манипулатори за изпълнение и отхвърляне, когато манипулаторите се добавят преди разрешаването на обещанието. Обърнете внимание, че както fulfillPledge(), така и rejectPledge() премахват всички реакции от обекта Pledge в процеса на промяна на състоянието на обекта и задействане на реакциите.

Методът catch()

Ако винаги сте се чудили дали методът catch() е просто стенограма за then(), тогава сте прави. Всичко, което catch() прави, е да извика then() с undefined първи аргумент и манипулатора onRejected като втори аргумент:

export class Pledge {

    // constructor omitted for space

    static get [Symbol.species]() {
        return this;
    }

    then(onFulfilled, onRejected) {

        assertIsPledge(this);

        const C = this.constructor[Symbol.species];
        const resultCapability = new PledgeCapability(C);
        return performPledgeThen(this, onFulfilled, onRejected, resultCapability);
    }

    catch(onRejected) {
        return this.then(undefined, onRejected);
    }

    // other methods omitted for space
}

Така че да, catch() наистина е просто удобен метод. Методът finally() обаче е по-ангажиращ.

Методът finally()

Методът finally() беше късно допълнение към спецификацията на обещанията и работи малко по-различно от then() и catch(). Докато и then(), и catch() ви позволяват да добавяте манипулатори, които ще получат стойност, когато обещанието е уредено, манипулатор, добавен с finally(), не получава стойност. Вместо това обещанието, върнато от повикването към finally(), се урежда по същия начин като първото обещание. Например, ако дадено обещание е изпълнено, тогава обещанието, върнато от finally(), се изпълнява със същата стойност:

const promise = Promise.resolve(42);

promise.finally(() => {
    console.log("Original promise is settled.");
}).then(value => {
    console.log(value);     // 42
});

Този пример показва, че извикването на finally() на обещание, което е разрешено до 42, ще доведе до обещание, което също е разрешено до 42. Това са две различни обещания, но те са решени на една и съща стойност.

По същия начин, ако дадено обещание бъде отхвърлено, обещанието, върнато от finally(), също ще бъде отхвърлено, както в този пример:

const promise = Promise.reject("Oops!");

promise.finally(() => {
    console.log("Original promise is settled.");
}).catch(reason => {
    console.log(reason);     // "Oops!"
});

Тук promise се отхвърля с причина "Oops!". Манипулаторът, присвоен с finally(), ще се изпълни първи, извеждайки съобщение към конзолата, а обещанието, върнато от finally(), се отхвърля по същата причина като promise. Тази възможност за предаване на отхвърляния на обещание чрез finally() означава, че добавянето на манипулатор finally() не се брои като обработка на отхвърляне на обещание. (Ако отхвърленото обещание има само манипулатор finally(), тогава времето за изпълнение на JavaScript пак ще изведе съобщение за отхвърляне на необработено обещание. Все пак трябва да добавите манипулатор на отхвърляне с then() или catch(), за да избегнете това съобщение.)

С добро разбиране на работата на finally() е време да го внедрите.

Прилагане на метода finally()

Първите няколко стъпки на finally()[7] са същите като при then(), което е да се потвърди, че this е обещание и да се извлече конструкторът на вида:

export class Pledge {

    // constructor omitted for space

    static get [Symbol.species]() {
        return this;
    }

    finally(onFinally) {

        assertIsPledge(this);

        const C = this.constructor[Symbol.species];

        // TODO
    }

    // other methods omitted for space
}

След това спецификацията дефинира две променливи, thenFinally и catchFinally, които са манипулатори за изпълнение и отхвърляне, които ще бъдат предадени на then(). Точно като catch(), finally() в крайна сметка извиква директно метода then(). Единственият въпрос е какви стойности ще бъдат предадени. Например, ако аргументът onFinally не може да бъде извикан, тогава thenFinally и catchFinally се задават равни на onFinally и не е необходимо да се извършва друга работа:

export class Pledge {

    // constructor omitted for space

    static get [Symbol.species]() {
        return this;
    }

    finally(onFinally) {

        assertIsPledge(this);

        const C = this.constructor[Symbol.species];

        let thenFinally, catchFinally;

        if (!isCallable(onFinally)) {
            thenFinally = onFinally;
            catchFinally = onFinally;
        } else {

            // TODO

        }

        return this.then(thenFinally, catchFinally);
    }

    // other methods omitted for space
}

Може да сте объркани защо неизвикаем onFinally ще бъде прехвърлен в then(), както бях аз, когато за първи път прочетох спецификацията. Не забравяйте, че then() в крайна сметка делегира на performPledgeThen(), което от своя страна задава всички неизвикаеми манипулатори на undefined. Така че finally() разчита на тази стъпка за валидиране в performPledgeThen(), за да гарантира, че неизвикваемите манипулатори никога не се добавят официално.

Следващата стъпка е да дефинирате стойностите за thenFinally и catchFinally, ако onFinally може да се извика. Всяка от тези функции е дефинирана в спецификацията като последователност от стъпки, които трябва да се изпълнят, за да се прехвърли състоянието и стойността на сетълмента от първото обещание към върнатото обещание. Стъпките за thenFinally са малко трудни за дешифриране в спецификацията 8, но са наистина ясни, когато видите кода:

export class Pledge {

    // constructor omitted for space

    static get [Symbol.species]() {
        return this;
    }

    finally(onFinally) {

        assertIsPledge(this);

        const C = this.constructor[Symbol.species];

        let thenFinally, catchFinally;

        if (!isCallable(onFinally)) {
            thenFinally = onFinally;
            catchFinally = onFinally;
        } else {

            thenFinally = value => {
                const result = onFinally.apply(undefined);
                const pledge = pledgeResolve(C, result);
                const valueThunk = () => value;
                return pledge.then(valueThunk);
            };

            // not used by included for completeness with spec
            thenFinally.C = C;
            thenFinally.onFinally = onFinally;

            // TODO

        }

        return this.then(thenFinally, catchFinally);
    }

    // other methods omitted for space
}

По същество стойността thenFinally е функция, която приема изпълнената стойност на обещанието и след това:

  1. Обажда се на onFinally().
  2. Създава разрешено обещание с резултата от стъпка 1. (Този резултат в крайна сметка се отхвърля.)
  3. Създава функция, наречена valueThunk, която не прави нищо, освен да връща изпълнената стойност.
  4. Присвоява valueThunk като манипулатор на изпълнение за новосъздаденото обещание и след това връща стойността.

След това препратките към C и onFinally се съхраняват във функцията, но както е отбелязано в кода, те не са необходими за изпълнението на JavaScript. В спецификацията това е начинът, по който функциите thenFinally получават достъп до C и onFinally. В JavaScript използвам затваряне, за да получа достъп до тези стойности.

Стъпките за създаване на catchFinally[9] са подобни, но крайният резултат е функция, която хвърля причина:

export class Pledge {

    // constructor omitted for space

    static get [Symbol.species]() {
        return this;
    }

    finally(onFinally) {

        assertIsPledge(this);

        const C = this.constructor[Symbol.species];

        let thenFinally, catchFinally;

        if (!isCallable(onFinally)) {
            thenFinally = onFinally;
            catchFinally = onFinally;
        } else {

            thenFinally = value => {
                const result = onFinally.apply(undefined);
                const pledge = pledgeResolve(C, result);
                const valueThunk = () => value;
                return pledge.then(valueThunk);
            };

            // not used by included for completeness with spec
            thenFinally.C = C;
            thenFinally.onFinally = onFinally;

            catchFinally = reason => {
                const result = onFinally.apply(undefined);
                const pledge = pledgeResolve(C, result);
                const thrower = () => {
                    throw reason;
                };
                return pledge.then(thrower);
            };

            // not used by included for completeness with spec
            catchFinally.C = C;
            catchFinally.onFinally = onFinally;

        }

        return this.then(thenFinally, catchFinally);
    }

    // other methods omitted for space
}

Може би се чудите защо функцията catchFinally извиква pledge.then(thrower) вместо pledge.catch(thrower). Това е начинът, по който спецификацията определя тази стъпка да се осъществи и наистина няма значение дали използвате then() или catch(), защото манипулатор, който хвърля стойност, винаги ще задейства отхвърлено обещание.

С този завършен finally() метод вече можете да видите, че когато onFinally може да се извика, методът създава thenFinally функция, която разрешава същата стойност като оригиналната функция, и catchFinally функция, която хвърля всяка причина, която получава. След това тези две функции се предават на then(), така че както изпълнението, така и отхвърлянето да се обработват по начин, който отразява установеното състояние на първоначалното обещание.

Обобщавайки

Тази публикация обхваща вътрешността на then(), catch() и finally(), като then() съдържа по-голямата част от функционалността, която представлява интерес, докато catch() и finally() всеки делегира на then(). Обработката на реакциите на обещанията без съмнение е най-сложната част от спецификацията на обещанията. Вече трябва да разбирате добре, че всички реакции се изпълняват асинхронно като задачи (микрозадачи) независимо от състоянието на обещание. Това разбиране наистина е от ключово значение за доброто цялостно разбиране за това как работят обещанията и кога трябва да очаквате да бъдат изпълнени различни манипулатори.

В следващата публикация от тази поредица ще разгледам създаването на уредени обещания с Promise.resolve() и Promise.reject().

Целият този код е достъпен в Pledge на GitHub. Надявам се, че ще го изтеглите и изпробвате, за да разберете по-добре обещанията.

Препратки

  1. PromiseCapability Records
  2. NewPromiseCapability( C )
  3. Promise.prototype.then( onFulfilled, onRejected )
  4. NewPromiseReactionJob( реакция, аргумент)
  5. Тип спецификация на записа за завършване
  6. TriggerPromiseReactions(реакции, аргумент)
  7. Promise.prototype.finally( onFinally)
  8. Накрая функции
  9. Catch Finally Functions

Тази публикация първоначално се появи в Human Who Codes Blog на 6 октомври 2020 г.. Ако тази публикация ви е харесала, моля, помислете за „дарение“ в подкрепа на работата ми.