Handwritten Promise, perfect realization of Promise/A+ specification

Handwritten Promise, perfect realization of Promise/A+ specification

The reason for writing this article is to share about the realization of Promise in the front-end team of the company. I feel that the content is pretty good, so I will share it with everyone here. The source code files will be placed on Github , and interested students can check the source code.

What is Promise

Promise
The core idea is
Promise
Represents the result of an asynchronous operation. One
Promise
In one of the following three states:

  • pending
    -
    Promise
    Initialization state
  • fulfilled
    -Means
    Promise
    Status of successful operation
  • rejected
    -Means
    Promise
    The status of the error operation

Promise
The internal state changes as shown in the figure:

What problem does the emergence of Promise solve?

  • 1. The problem of nesting hell

in

Promise
Before it appeared, we would see a lot of similar codes.

const fs = require ( 'fs' ) fs.readFile( './data.txt' , 'utf8' , function ( err,data ) { fs.readFile(data, 'utf8' , function ( err,data ) { fs.readFile(data, 'utf8' , function ( err,data ) { console .log(data); }) }) }) Copy code

Promise
After it appears, it can be written in the form of chain calls.

const fs = require ( 'fs' ) const readFile = ( filename ) => { return new Promise ( ( resolve, reject ) => { fs.readFile(filename, 'utf8' , ( err, data ) => { if (err) reject(err); resolve(data); }) }) } readFile( './data.txt' ).then( ( data ) => { return readFile(data) }).then( ( data ) => { return readFile(data) }).then( ( data ) => { console .log(data); }) Copy code

used

Promise
After that, the code style has become a lot more elegant, and the writing is more intuitive.

  • 2. Concurrent processing of multiple asynchronous requests

Promise.all
The emergence of allows us to more conveniently handle the logic of processing when multiple tasks are completed.

Implemented according to Promise/A+ specification
Promise

Basic function realization

1. Know what functions need to be implemented before writing the code.

  • Promise constructor

new Promise
When the constructor needs to pass in a
executor()
Actuator,
executor
The function will be executed immediately, and it supports passing in two parameters, namely
resolve
with
reject
.

class Promise < T > { constructor ( executor: (resolve: (value: T) => void , reject: >(reason?: any ) => void ) => void ) { } } Copy code
  • Promise
    Status "Promise/A+ 2.1"

Promise
Must be in one of the following three states:

pending
(Waiting), can be converted to
fulfilled
(Complete) or
rejected
(Refuse).

When the state changes from

pending
Switch to
fulfilled
At this time, the state must not transition to other states, and must have a value, which cannot be changed.

When the state changes from

pending
Switch to
rejected
At this time, the state must not be transitioned to another state, and there must be a reason for the failure and cannot be changed.

  • Promise then
    Method "Promise/A+ 2.2"

Promise
There must be one
then
method,
then
Receive two parameters, which are the callback when successful
onFulfilled
, And callback on failure
onRejected
.

onFulfilled
with
onRejected
Is an optional parameter, and if passed in
onFulfilled
with
onRejected
If it is not a function, it must be ignored.

in case

onfulfilled
Is a function. Then it must be in
Promise
The status becomes
fulfilled
It can be called when (completed),
Promise
The value of is the first parameter passed into it. And it can only be called once.

in case

onRejected
Is a function, it must be in
Promise
It is called when the status of is rejected (failed), and the reason for the failure is passed into its first parameter. Can only be called once.

Now that you know those functions that need to be implemented, let's do it yourself. The code is as follows:

//Use enumeration to define the status of Promise enum PROMISE_STATUS { PENDING, FULFILLED, REJECTED } class _Promise < T > { //Save the current status private status = PROMISE_STATUS.PENDING//Save the value of resolve, or the reason for rejection private value: T constructor ( executor: (resolve: (value: T) => void , reject: (reason: any ) => void ) => void ) { executor( this ._resolve, this ._reject) } //The then method to complete a simple function according to the specification then ( onfulfilled: (value: T) => any , onrejected: (value: any ) => any ) { //2.2.1 onfulfilled = typeof onfulfilled === 'function' ? onfulfilled: null ; onrejected = typeof onrejected === 'function' ? onrejected: null ; if ( this .status === PROMISE_STATUS.FULFILLED) { //Call the successful callback function when the status is fulfilled onfulfilled( this .value) } if ( this .status === PROMISE_STATUS.REJECTED) { //Call the failed callback function when the status is rejected onrejected( this .value) } } // Pass in the first parameter of the executor method, call this method is successful private _resolve = ( value ) => { if (value === this ) { throw new TypeError ( 'A promise cannot be resolved with itself.' ) ; } //Only the pending status can update the status to prevent the second call if ( this .status !== PROMISE_STATUS.PENDING) return ; this .status = PROMISE_STATUS.FULFILLED; this .value = value; } // Pass in the second parameter of the executor method, calling this method will fail private _reject = ( value ) => { //Only the pending status can update the status to prevent the second call if ( this .status !== PROMISE_STATUS .PENDING) return ; this .status = PROMISE_STATUS.REJECTED; this .value = value } } Copy code

After the code is written, let's test the function:

const p1 = new _Promise((resolve, reject) => { resolve(2) }) p1.then(res => { console.log(res,'then ok1') }) const p2 = new _Promise((resolve, reject) => { setTimeout(() => { resolve(2) }, 1000); }) p2.then(res => { console.log(res,'then ok2') }) Copy code

The console will print out:

2 "then ok1" Copy code

Yes, it is now in its infancy.

Support asynchronous operation

We have implemented an entry-level

Promise
, But careful students should have found out,
then ok2
This value is not printed.

What is the cause of this problem? It turned out that when we were executing the then function, because it was an asynchronous operation, the state was always in the pending state, and the incoming callback function did not trigger the execution.

Know the problem and solve it. Just store the callback function passed in. Just execute when the resolve or reject method is called. Let's optimize the code:

enum PROMISE_STATUS { PENDING, FULFILLED, REJECTED } class _Promise < T > { private status = PROMISE_STATUS.PENDING private value: T //Save the callback function passed in by the then method private callbacks = [] constructor ( executor: (resolve: (value: T) => void , reject: ( reason: any ) => void ) => void ) { executor( this ._resolve, this ._reject) } then ( onfulfilled: (value: T) => any , onrejected: (value: any ) => any ) { //2.2.1 onfulfilled = typeof onfulfilled === 'function' ? onfulfilled: null ; onrejected = typeof onrejected === 'function' ? onrejected: null ; //Integrate the callback function passed by the then method const handle = () => { if ( this .status === PROMISE_STATUS.FULFILLED) { onfulfilled && onfulfilled( this .value) } if ( this .status === PROMISE_STATUS.REJECTED) { onrejected && onrejected( this .value) } } if ( this .status === PROMISE_STATUS.PENDING) { //When the status is pending, save the callback function into the callback this .callbacks.push(handle) } handle() } private _resolve = ( value ) => { if (value === this ) { throw new TypeError ( 'A promise cannot be resolved with itself.' ); } if ( this .status !== PROMISE_STATUS.PENDING) return ; this .status = PROMISE_STATUS.FULFILLED; this .value = value; //Traverse execution callback this .callbacks.forEach( fn => fn()) } private _reject = ( value ) => { if ( this .status !== PROMISE_STATUS.PENDING) return ; this .status = PROMISE_STATUS.REJECTED; this .value = value //Traverse execution callback this .callbacks.forEach( fn => fn()) } } Copy code

Let's test the above code:

const p2 = new _Promise((resolve, reject) => { setTimeout(() => { resolve(2) }, 1000); }) p2.then(res => { console.log(res,'then ok2') }) Copy code

After waiting for 1s, the console will print out:

2 "then ok2" Copy code

Asynchronous operations can now be supported. You are now a master in the arena.

Chain call of then method

When introducing Promise at the beginning of the article, the concept of chain call was mentioned

.then().then()
, Now we have to implement this vital function, take a look before we start
Promise/A+
Specification

then
Must return a Promise Promise/A+ 2.2.7

promise2 = promise1.then(onFulfilled, onRejected);

If one

onFulfilled
or
onRejected
Return a value
x
, Then run
Promise Resolution Procedure
(This method will be implemented below).

If any one

onFulfilled
or
onRejected
Throw an exception
e
then
promise2
Must start with
e
For the reason
reject
(Refuse).

in case

onFulfilled
Is not a function and
promise1
State already
fuifilled
(Completed), then
promise2
Must use the same value as
promise1
.

in case

onRejected
Not a function but
promise1
Status is
rejected
(Reject), then
promise2
It must be rejected for the same reason as
promise1
.

Promise Resolution Procedure implementation

First of all, the use of this method is similar to the following form

resolvePromise(promise,x,...)

in case

promise
with
x
Refer to the same object, the promise should be rejected on the grounds of TypeError. "Promise/A+ 2.3.1"

in case

x
Is an
promise
, It should be returned in its original state. "Promise/A+ 2.3.2"

Otherwise, judge

x
If it is an object or a function. Then perform the following operations, first declare
let then = x.then
, If there is an abnormal result
e
, Then
e
As
promise
The reason for reject. in case
then
Is a function, use
call
carried out
then
, Put
this
Point to
x
, The first parameter is
resolvePromise
Call, the second one with
rejectPromise
Call "Promise/A+ 2.3.3"

in case

x
Is not an object or method, use
x
Value of
resolve
Complete "Promise/A+ 2.3.4"

It's just not easy to understand through the text, let's take a look at the implementation of the code:

enum PROMISE_STATUS { PENDING, FULFILLED, REJECTED } class _Promise < T > { private status = PROMISE_STATUS.PENDING private value: T private callbacks = [] constructor ( executor: (resolve: (value: T) => void , reject: (reason: any ) => void ) => void ) { executor( this ._resolve, this ._reject) } then ( onfulfilled: (value: T) => any , onrejected: (value: any ) => any ) { //2.2.1 onfulfilled = typeof onfulfilled === 'function' ? onfulfilled: null ; onrejected = typeof onrejected === 'function' ? onrejected: null ; const nextPromise = new _Promise( ( nextResolve, nextReject ) => { const handle = () => { if ( this .status === PROMISE_STATUS.FULFILLED) { const x = (onfulfilled && onfulfilled( this .value)) this . _resolvePromise(nextPromise, x, nextResolve, nextReject) } if ( this .status === PROMISE_STATUS.REJECTED) { if (onrejected) { const x = onrejected( this .value) this ._resolvePromise(nextPromise, x, nextResolve, nextReject) } else { nextReject( this .value) } } } if ( this .status === PROMISE_STATUS.PENDING) { this .callbacks.push(handle) } else { handle() } }); return nextPromise } private _resolve = ( value ) => { if (value === this ) { throw new TypeError ( 'A promise cannot be resolved with itself.' ); } if ( this .status !== PROMISE_STATUS.PENDING) return ; this .status = PROMISE_STATUS.FULFILLED; this .value = value; this .callbacks.forEach( fn => fn()) } private _reject = ( value ) => { if ( this .status !== PROMISE_STATUS.PENDING) return ; this .status = PROMISE_STATUS.REJECTED; this .value = value this .callbacks.forEach( fn => fn()) } private _resolvePromise = <T> ( nextPromise: _Promise<T>, x: any , resolve, reject ) => { //2.3.1 nextPromise cannot be equal to x if (nextPromise === x) { return reject( new TypeError ( 'The promise and the return value are the same' )); } //2.3.2 If x is a Promise, return the state and value of x if (x instanceof _Promise) { x.then(resolve, reject) } //2.3.3 If x is an object or function executes the logic in if if ( typeof x === 'object' || typeof x === 'function' ) { if (x === null ) { return resolve( x); } //2.3.3.1 let then; try { then = x.then; } catch (error) { return reject(error); } //2.3.3.3 if ( typeof then === 'function' ) { //Declare that called is changed to true after calling resolve or reject once to ensure that it can only be called once let called = false ; try { then.call(x, y => { if (called) return ; //2.3.3.3.4.1 called = true ; //The process of recursive resolution (because there may be a promise in the promise) this ._resolvePromise(nextPromise, y, resolve, reject) }, r => { if (called) return ; //2.3.3.3.4.1 called = true ; reject(r) }) } catch (e) { if (called) return ; //2.3.3.3.4.1 //2.3.3.3.4 reject(e) } } else { //2.3.3.4 resolve(x) } } else { //2.3.4 resolve(x); } } } Copy code

Now that the function that can be chained is implemented, let's test it:

const p3 = new _Promise((resolve, reject) => { setTimeout(() => { resolve(3) }, 1000); }) p3.then(res => { console.log(res,'then ok3') return'chain call' }).then(res => { console.log(res) }) Copy code

After waiting for 1s, the console will print out:

3 "then ok3" "Chain call" Copy code

Support micro tasks

Does any student think that a particularly important function is missing, that is, microtasks. How should we implement and build in

Promise
What about the same microtask process?

in

Web Api
There is such a method MutationObserver . We can realize the function of micro-task based on it. And there are related libraries that have encapsulated this method for us, it is asap . Just pass in the function that needs to be executed as a micro task.

asap( function () { //... }); Copy code

Actually

Web Api
There is also such a method queueMicrotask can be used directly. The method of use is to pass in the function to be executed as a microtask.

self.queueMicrotask( () => { //The content of the function }) Copy code

queueMicrotask
The only disadvantage is that the compatibility is not very good, it is recommended to use it in a production environment
asap
This library is used to implement microtasks.

Write before

Promise then
The method is slightly adjusted:

then ( onfulfilled: (value: T) => any , onrejected: (value: any ) => any ) { //2.2.1 onfulfilled = typeof onfulfilled === 'function' ? onfulfilled: null ; onrejected = typeof onrejected === 'function' ? onrejected: null ; const nextPromise = new _Promise( ( nextResolve, nextReject ) => { const _handle = () => { if ( this .status === PROMISE_STATUS.FULFILLED) { const x = (onfulfilled && onfulfilled( this .value)) this . _resolvePromise(nextPromise, x, nextResolve, nextReject) } if ( this .status === PROMISE_STATUS.REJECTED) { if (onrejected) { const x = onrejected( this .value) this ._resolvePromise(nextPromise, x, nextResolve, nextReject) } else { nextReject( this .value) } } } const handle = () => { //Support micro tasks queueMicrotask(_handle) } if ( this .status === PROMISE_STATUS.PENDING) { this .callbacks.push(handle) } else { handle() } }); return nextPromise } Copy code

Now perfectly supports micro-tasks, and built-in

Promises
The sequence of events is the same. Let's test it:

console .log( 'first' ) const p1 = new _Promise( function ( resolve ) { console .log( 'second' ) resolve( 'third' ) }) p1.then ( Console .log) Console .log ( 'Fourth' ) copying the code

You can see that the result printed on the console is:

first second fourth third Copy code

At this point, we have completed the most critical functions of Promise:

Support asynchronous operation
,
then supports chain call
,
Support micro tasks
.

Test whether the completed Promise meets the specification

1. Download the Promise/A+ specification and provide a special test script

promises-aplus-tests

yarn add promises-aplus-tests -D duplicated code

2. Add the following code to our code:

(_Promise as any ).deferred = function () { let dfd = {} as any ; dfd.promise = new Promise ( ( resolve, reject ) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd; } Module1 .exports = _Promise; duplicated code

3. Modify

package.json
The file adds the following content (
./dist/promise/index.js
Is the file path to be tested):

{ "scripts": { "test": "promises-aplus-tests ./dist/promise/index.js" } } Copy code

4. Execution

yarn test

You can see that all 872 test cases passed.

Promise
Other
API
achieve

So far, the above code has been completely implemented in accordance with the Promise/A+ specification, but there are still some built-in APIs that have not been implemented. Let's implement these built-in methods:

class _Promise { catch (onrejected) { return this .then( null , onrejected) } finally ( cb ) { return this .then( value => _Promise.resolve(cb()).then( () => value), reason => _Promise.resolve(cb()).then( () => { throw reason }) ); } static resolve ( value ) { if (value instanceof _Promise) { return value; } return new _Promise( resolve => { resolve(value); }); } static reject ( reason ) { return new _Promise( ( resolve, reject ) => { reject(reason); }); } static race ( promises ) { return new _Promise( function ( resolve, reject ) { if (! Array .isArray(promises)) { return reject( new TypeError ( 'Promise.race accepts an array' )); } for ( var i = 0 , len = promises.length; i <len; i++) { _Promise.resolve(promises[i]).then(resolve, reject); } }); } static all ( promises ) { let result = []; let i = 0 ; return new _Promise( ( resolve, reject ) => { const processValue = ( index, value ) => { result[index] = value; i++; if (i == promises.length) { resolve(result); }; }; for ( let index = 0 ; index <promises.length; index++) { promises[index].then( value => { processValue(index, value); }, reject); }; }); } static allSettled ( promises ) { let result = [] let i = 0 ; return new _Promise( ( resolve, reject ) => { const processValue = ( index, value, status: 'fulfilled' | 'rejected' ) => { result[index] = {status, value} i++; if (i == promises.length) { resolve(result); }; }; for ( let index = 0 ; index <promises.length; index++) { promises[index].then( value => { processValue(index, value, 'fulfilled' ) }, value => { processValue(index, value, 'rejected' ) }); }; }) } ... } Copy code

How to achieve
async
with
await

We are done

Promise
The realization. But have you ever thought
async
with
await
This one
Promise
How to realize the syntactic sugar for?

Here we have to use

Generator
Function to achieve this function, less nonsense, directly on the code:

let gp1 = new _Promise( r => { setTimeout ( () => { r( 1 ) }, 1000 ); }) let gp2 = new _Promise( r => { setTimeout ( () => { r( 2 ) }, 1000 ); }) function * gen () { let a = yield gp1 let b = yield gp2 return b + a } function run ( gen ) { return new _Promise( function ( resolve, reject ) { g = gen() function next ( v ) { ret = g.next(v) if (ret.done) return resolve(ret.value); _Promise.resolve(ret.value).then(next) } next() }) } RUN (Gen) .then ( Console .log) copying the code

The result will be printed out in the console:

3

If for this

run
Function is interested, it is recommended to look at the implementation of this co library, the code is very concise, only about a hundred lines, worth a look.

summary

It took nearly half a day to write this article. The source code files have been placed on Github . Students who don't want to type it again can directly pull down the code execution to view the results. If you have different opinions or ideas, please leave a message.

Links to related resources