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