preface

Promises have become very common as a solution to asynchronous programming. Hand-written promises are often asked in job interviews, so let’s take a step-by-step approach to making PromiseA+ promises that perfectly comply with the PromiseA+ specification

The preparatory work

PromiseA+ specification translation

The node version v12.10.0

71 + Chrome version

PromiseZ we use Promises – aplus-Tests (version 2.1.2) to test PromiseZ we wrote

Global installation

npm install promises-aplus-tests -g
Copy the code

Add in PromiseZ (such as index.js)

PromiseZ.deferred = function() {
    let defer = {};
    defer.promise = new PromiseZ((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    });
    return defer;
}
module.exports = PromiseZ;
Copy the code

Execute the script

promises-aplus-tests index.js
Copy the code

In the process of writing, if you do not understand something, you can locate the corresponding test cases in Promises – aplus-Tests according to the popular prompt in the console, which is easy to deepen your understanding

1. Basic use

Well, first of all, let’s look at some of the more common ones

new Promise((resolve, reject) => { queueMicrotask(() => { resolve('resolved'); }, 2000); }).then(res => { console.log(res); }) // Output resolvedCopy the code

We know that Promise has three states: Pending, depressing, and Rejected

The status can only change from Pending to fulfilled or Rejected, and the status is irreversible.

When a Promise is used as a constructor, it passes in a function as its argument

And Promise is a function that contains the then method

Based on that, let’s write a basic one

const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; function PromiseZ(fn) { this.status = PENDING; this.value = undefined; this.reason = undefined this.onFulfilledCallback; // Assign this. OnRejectedCallback to the then method; // Const me = this in the then method; function resolve(value) { if (me.status === PENDING) { me.status = FULFILLED; me.value = value; me.onFulfilledCallback && me.onFulfilledCallback(value); } } function reject(reason) { if (me.status === PENDING) { me.status = REJECTED; me.reason = reason; me.onRejectedCallback && me.onRejectedCallback(reason); } } try { fn(resolve, reject); } catch (e) { reject(e); } } PromiseZ.prototype.then = function (onFulfilled, onRejected) { const me = this; const onFulfilledCallback = typeof onFulfilled === 'function' ? onFulfilled : value => value; const onRejectedCallback = typeof onRejected === 'function' ? onRejected : reason => { throw reason }; if (me.status === FULFILLED) { onFulfilledCallback(me.value); } else if (me.status === REJECTED) { onRejectedCallback(me.reason); } else { me.onFulfilledCallback = onFulfilledCallback; me.onRejectedCallback = onRejectedCallback; }}Copy the code

If “resolve” or “reject” is executed synchronously, “then” is not pending, oncallledCallback or onRejectedCallback is invoked.

When fn executes resolve or reject asynchronously, the. Then state is still pending. Assign oncallledCallback and onRejectedCallback to this and use resolve/ Reject to perform the callback.

2. Call. Then multiple times

We know that a Promise can call the THEN method multiple times, for example

let p = new Promise((res) => { queueMicrotask(() => { res(10); }, 1000)}); p.then(v => { console.log(v + 1); }); p.then(v => { console.log(v + 2); }); // Output 11, 12Copy the code

So this. oncallledCallback and this.onRejectedCallback should be array structures that receive multiple methods passed in then;

Function PromiseZ(fn) {/** omit **/- this.onFulfilledCallback;
- this.onRejectedCallback;
+ this.onFulfilledCallbacks = [];
+ this.onRejectedCallbacks = [];
    function resolve(value) {
        if (me.status === PENDING) {
            me.status = FULFILLED;
            me.value = value;
- me.onFulfilledCallback && me.onFulfilledCallback(value);
+ me.onFulfilledCallbacks.forEach(cb => cb(value));
        }
    }
    function reject(reason) {
        if (me.status === PENDING) {
            me.status = REJECTED;
            me.reason = reason;
- me.onRejectedCallback && me.onRejectedCallback(reason);
+ me.onRejectedCallbacks.forEach(cb => cb(reason));/ / this is a big pity. / / this is a big pity. / / this is a big pity.- me.onFulfilledCallback = onFulfilledCallback;
- me.onRejectedCallback = onRejectedCallback;
+ me.onFulfilledCallbacks.push(onFulfilledCallback);
+ me.onFulfilledCallbacks.push(onFulfilledCallback);}}Copy the code

Now we can call the then method multiple times with a Promise

This is a big onFulfilled and onRejected

Promise A+ : Promise A+ : Promise A+ : Promise A+

console.log('start'); new Promise(resolve => { resolve('resolved'); }).then(() => { console.log('then'); }); console.log('end'); // Output sequence is Start resolved end thenCopy the code

However, using our PromiseZ output sequence is Start Resolved then end

Reason is that when performing method then state has become FULFILLED/REJECTED, we immediately carried out onFulfilledCallback/onRejectedCallback, cause the entire execution order does not fit PromiseA + specification.

Look at another chestnut

let resolve1; console.log('start'); new Promise(resolve => { console.log('pending'); resolve1 = resolve; }).then(() => { console.log('then'); }); resolve1(); console.log('end'); // Start pending end THENCopy the code

Using our PromiseZ output sequence as start Pending then end when executing the THEN method, the state is pending, so we immediately execute onperform ledCallback into the array queue. After resolve1 is executed, all methods in the queue will be executed immediately when the status changes.

To solve this problem, we use queueMicrotask to implement microtasks. The queueMicrotask Api is relatively new and can also be simulated using setTimeout

/** */ function resolve(value) {if (me. Status)=== PENDING) {
            me.status = FULFILLED;
            me.value = value;
+ queueMicrotask(() => {
                me.onFulfilledCallbacks.forEach(cb => cb(value));
+});
           
        }
    }
    function reject(reason) {
        if (me.status === PENDING) {
            me.status = REJECTED;
            me.reason = reason;
+ queueMicrotask(() => {
                me.onRejectedCallbacks.forEach(cb => cb(reason));
+});/ / this is a big pity! / / this is a big pity! / / this is a big pity=== FULFILLED) {
+ queueMicrotask(() => {
            onFulfilledCallback(me.value);
+});
    } else if (me.status === REJECTED) {
+ queueMicrotask(() => {
            onRejectedCallback(me.reason);
+});} else {/** omit **/}}Copy the code

4. Chain call

New Promise().then(dothing1).then(dothing2) This call has become very common, essentially returning a new Promise after each execution of the then method.

let p = new Promise(res => res(2));
let then = p.then(v => v);
then instanceof Promise // true
then === p // false
Copy the code

Rewrite the then method to return a new PromiseZ: promise2

Promise.prototype. Then = function (ondepressing, onRejected) {/** omit **/+ let promise2 = new PromiseZ((resolve, reject) => {
        if (me.status === FULFILLED) {
            queueMicrotask(() => {
+ try {
- onFulfilledCallback(me.value);
+ let x = onFulfilledCallback(me.value);
+ resolve(x);
+ } catch(e) {
+ reject(e);
+}
            });
        } else if (me.status === REJECTED) {
            queueMicrotask(() => {
+ try {
- onRejectedCallback(me.reason);
+ let x = onRejectedCallback(me.reason);
+ resolve(x); // Use resolve instead of reject
+ } catch(e) {
+ reject(e);
+}
            });
        } else {
- me.onFulfilledCallbacks.push(onFulfilledCallback);
- me.onFulfilledCallbacks.push(onFulfilledCallback);
+ me.onFulfilledCallbacks.push((value) => {
+ try {
+ let x = onFulfilledCallback(value);
+ resolve(x);
+ } catch (e) {
+ reject(e);
+}
+});
+ me.onRejectedCallbacks.push((reason) => {
+ try {
+ let x = onRejectedCallback(reason);
+ resolve(x); // Use resolve instead of reject
+ } catch(e) {
+ reject(e);
+}
+});
        }
+})
+ return promise2;
}
Copy the code

The reason why resolve is used instead of reject is that when we receive the last error in the onRejected method in the THEN method, it means that we have processed the expected error, and the ondepressing of the next THEN should be performed when we transfer the next layer. Unless another error occurs during the execution of resolve

Test the

console.log('start'); new PromiseZ(res => { queueMicrotask(() => { console.log('resolve'); res(10); }, 3000) }).then(v => { console.log('then1'); return v + 3; }).then(v => { console.log('then2'); console.log(v); }) console.log('end'); Start end resolve then1 then2 13Copy the code

X is a Promise

On a link, we define a variable x is used to receive onFulfilledCallback/onRejectedCallback results. None of the provided test cases are of the PromiseZ type.

If x is also a PromiseZ, then the state of promise2 depends on the state of X

For example,

console.log('start'); new Promise((res) => { console.log('promise1 pending'); queueMicrotask(() => { console.log('promise1 resolve'); res(1); }, 2000); }).then(v => { console.log(`then1: ${v}`); return new Promise(res => { console.log(`promise2 pending: ${v}`); queueMicrotask(() => { console.log(`promise2 resolve: ${v}`); res(v + 3); }, 2000); }) }).then(v => { console.log(`then2: ${v}`); }); console.log('end'); Start promisE1 pending end promisE1 resolve THEN1:1 promise2 Pending: 1 promise2 resolve: 1 THEN2:4Copy the code

Resolve (promise2, x, resolve, reject); Resolve and reject are provided by promise2, which can be interpreted as the invocation of resolve/ Reject to change the state of promise2 when THE state of X becomes depressing /REJECTED

This is a big pity. Then = function (onFulfilled, onRejected) {/** omitted **/ let promise2 = new promise2 ((forget, forget)) Reject) => {/** omit **/- resolve(x);
+ resolvePromise(promise2, x, resolve, reject);/** omit **/}); }Copy the code
function resolvePromise(promise2, x, resolve, reject) { if (x instanceof PromiseZ) { try { let then = x.then; // Then. Call (x, y => {resolvePromise(promise2, y, resolve, reject); }, r => { reject(r); }); } catch (e) { reject(e); } } else { resolve(x); }}Copy the code

Recursive invocation: When the state of X becomes depressing, the result of resolve may be a PromiseZ, and the state of promise2 again depends on y……

So we need to make a recursive call to this;

X is a thenable

First, the Thenable definition given by the Promise specification

‘thenable’ is an object or function that defines the then method

Let’s start with some chestnuts

new Promise(res => res(10)).then(v => {
    return {
        other: v,
        then: v + 2
    }
}).then(ans => {
    console.log(ans);
});

new Promise(res => res(10)).then(v => {
    return {
        other: v,
        then: () => {
            return v + 2;
        }
    }
}).then(ans => {
    console.log(ans);
});

new Promise(res => res(10)).then(v => {
    return {
        other: v,
        then: (res, rej) => {
            res(v + 2);
        }
    }
}).then(ans => {
    console.log(ans);
});
Copy the code

To guess the output of the three THEN methods above, here are the correct returns

// The first {other: 10, then: 12} // the second // will not print, i.e. the code in the then method (the Promise state is always pending) // the third 12Copy the code

In summary, the Promise does special treatment to Thenable and treats it as a Promise

function resolvePromise(promise2, x, resolve, reject) {
- if (x instanceof PromiseZ) {
+ if (typeof x === 'object' && x || typeof x === 'function') {
       try {
            let then = x.then;
+ if (type of then === 'function')
                then.call(x, y => {
                    resolvePromise(promise2, y, resolve, reject);
                }, r => {
                    reject(r);
                });
+ } else {
+ resolve(x);
+}} catch (e) { reject(e); }} else {/** omit **/}}Copy the code

[x is a thenable] is actually a case involving [x is a Promise]

So far, we’ve implemented most of Promise’s functionality, but we’ll have to tweak it a bit to fully comply with the Promise specification

7. x === promise2

When running the test case, a circular reference is generated when x === promise2. Let’s look at a simple test case

let promise = new PromiseZ(res => res()).then(function () {
    return promise;
});
Copy the code

When a circular reference is generated, a TypeError is rejected

function resolvePromise(promise2, x, resolve, reject) {
+ if (x === promise2) {
+ reject(new TypeError('chaining cycle'));
+ } else if (typeof x === 'object' && x || typeof x === 'function') {/** omit **/} else {resolve(x); }}Copy the code

8. Resolve or reject can be configured only once

As mentioned earlier, promises are irreversible, and a repeat of resolve or Reject should be ignored after the execution of resolve or Reject. In PromiseZ, we add this logic. Similarly, in Thenable, we should follow this rule as well. Look at the test case below.

New Promise(res => res()). Then () => {return (onFulfilled) {// onFulfilled(onFulfilled); function (onFulfilled) { queueMicrotask(function () { onFulfilled('onFulfilled1'); }, 0); }}); Ondischarge (' ondischarge '); // Ondischarge (' ondischarge '); }}; }).then(value => { console.log(value); }); // Output onondata 1 correctlyCopy the code

This is a big onFulfilled fulfilled asynchronously. However, onpowered 2 will be printed in our PromiseZ because a Thenable is returned after executing the first onFulfilled fulfilled asynchronously. Therefore, the current state of the PromiseZ is still pending, so the second ondepressing will be continued. So we need to add an identifier called to ignore subsequent calls

function resolvePromise(promise2, x, resolve, reject) {
    if (x === promise2) {
        reject(new TypeError('chaining cycle'))
    } else if (typeof x === 'object' && x || typeof x === 'function') {
+ let called
        try {
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
+ if (called) return;
+ called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, r => {
+ if (called) return;
+ called = true;
                    reject(r);
                });
            } else {
+ if (called) return;
+ called = true;
                resolve(x);
            }  
        } catch (e) {
+ if (called) return;
+ called = true;reject(e); } } else { resolve(x); }}Copy the code

At this point, a PromiseZ that perfectly conforms to the PromiseA+ specification is complete

Refer to the link

Source code implementation of Promise (perfect compliance with the Promise/A+ specification)

ECMAScript 6 Getting Started promises

PromiseA+

[翻译] We have a problem with promises