promise - asynchronous programming tool

This paper understands Promise principle by implementing a simple class, compares Promise with traditional callback, shows Promise's advantages in asynchronous programming, and finally introduces Promise's application in practical development.

I concept

Promise decouples asynchronous operations from callbacks and associates them through execution state. After the asynchronous operation, promise is notified of the status, and promise is responsible for triggering the callback function.

II Promise principle

1. Status change

Input:

let p0 = new Promise((resolve, reject) => {})
console.log('p0', p0)

let p1 = new Promise((resolve, reject) => {
    resolve('success')
})
console.log('p1', p1)

let p2 = new Promise((resolve, reject) => {
    reject('fail')
})
console.log('p2', p2)

let p3 = new Promise((resolve, reject) => {
    throw('report errors')
})
console.log('p3', p3)

Output:

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-xc8bkt3w-1641122902483) (C: \ users \ ys \ appdata \ roaming \ typora \ user images \ image-20220102102219777. PNG)]

Conclusion:

  1. The Promise object just created is in pending state.
  2. When the asynchronous operation is completed, the resolve/reject change state is called fulfilled/rejected.
  3. When an exception (synchronization exception) occurs in the execution function, the change status is rejected.

realization:

class MyPromise {
    constructor(executor) {

        this.initValue();
        // The myresolve / this function must be hard bound to the current myresolve object
        this.initBind();
        
        try {
            // Execute the function passed in
            executor(this.resolve, this.reject);
        } catch (e) {
            // If an error is caught, execute reject directly
            this.reject(e);
        }
    }

    initBind() {
        this.resolve = this.resolve.bind(this);
        this.reject = this.reject.bind(this);
    }

    initValue() {
        this.PromiseResult = null;
        this.PromiseState = 'pending';
    }

    resolve(value) {
        // Status can only be converted from pending to fully / rejected
        if (this.PromiseState == 'pending'){
            this.PromiseState = 'fulfilled';
            this.PromiseResult = value;
        }
    }

    reject(reason) {
         // Status can only be converted from pending to fully / rejected
        if (this.PromiseState !== 'pending'){
            this.PromiseState = 'rejected';
            this.PromiseResult = reason;   
        }
    }
}

2. Execute callback

Input / output:

// Output "resolve = success" immediately“
const p1 = new Promise((resolve, reject) => {
    resolve('success');
}).then(res => console.log('resolve=',res), err => console.log('reject=',err))

// Output "reject = failed after 1 second“
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('fail');
    }, 1000)
}).then(res => console.log('resolve=',res), err => console.log('reject=',err))

Conclusion:

  • then receives two parameters: a successful callback and a failed callback
  • When the Promise status is full, execute a successful callback, and execute a failed callback for rejected
  • You can register multiple callback functions by calling then multiple times. Pay attention to distinguish chain calls

realization:

When registering a callback through then, if the Promise status is fully / rejected, the callback function is executed;

If the Promise status is pending, save the callback function first, and then execute the callback after the asynchronous operation is completed.

initValue() {
    ...
    // Save callback function.
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
}

resolve(value) {
    ...
    // Change the status to fully completed, and execute the saved successful callback
    while (this.onFulfilledCallbacks.length) {
        this.onFulfilledCallbacks.shift()(this.PromiseResult);
    }
}
reject(reason) {
    ...
    // Change the status to rejected and execute the saved failed callback
     while (this.onRejectedCallbacks.length) {
         this.onRejectedCallbacks.shift()(this.PromiseResult);
     }
}

then(onFulfilled, onRejected) {
    // Parameter verification to ensure that it must be a function
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
    if (this.PromiseState === 'fulfilled') {
        // Execute a fully completed callback
        onFulfilled(this.PromiseResult);
    } else if (this.PromiseState === 'rejected') {
        // Execute rejected callback
        onRejected(this.PromiseResult);
    }else if (this.PromiseState === 'pending') {
        // Promise is in pending status, and two callbacks are saved temporarily
       this.onFulfilledCallbacks.push(onFulfilled);
       this.onRejectedCallbacks.push(onRejected);
    }
}

3. Chain call

Input / output

// Chained call output 200
const p3 = new Promise((resolve, reject) => {
    resolve(100);
}).then(res => 2 * res)
  .then(res => console.log(res))

// Chained call output 300
const p4 = new Promise((resolve, reject) => {
    resolve(100);
}).then(res => new Promise((resolve, reject) => resolve(3 * res)))
  .then(res => console.log(res))

conclusion

  1. The then method itself returns a new Promise object.

  2. If the return value of the callback function is a Promise object, the state of the new Promise object is determined by the Promise object.

  3. If the return value of the callback function is not a Promise object, the status of the new Promise object is success.

realization

 then(onFulfilled, onRejected) {
     ...
     var thenPromise = new MyPromise((resolve, reject) => {
         const resolvePromise = cb => {
             try {
                 const x = cb(this.PromiseResult)
                     if(x === thenPromise){
                         // Wait for yourself to complete, loop wait: the return value of then is returned in the then callback.
                         reject(new TypeError('Chaining cycle detected for promise'));
                     }
                     if (x instanceof MyPromise) {
                         // If the return value is a Promise object, the new Promise status is determined by the Promise.
                         x.then(resolve, reject);
                    } else {
                        // Direct success without Promise
                        resolve(x);
                    }
                } catch (err) {
                    // Error handling
                    reject(err);
                }
            }

            if (this.PromiseState === 'fulfilled') {
                // If the current status is success, execute the first callback
                resolvePromise(onFulfilled);
            } else if (this.PromiseState === 'rejected') {
                // If the current state is failed, a second callback is executed
                resolvePromise(onRejected);
            } else if (this.PromiseState === 'pending') {
                // If the status is pending, save two callbacks temporarily
                this.onFulfilledCallback = resolvePromise(onFulfilled);
                this.onRejectedCallback = resolvePromise(onRejected);
            }
        })

        // Returns the Promise of this wrapper
        return thenPromise;

    }

4. Call timing

Input and output

setTimeout(()=>{console.log(0)},0);
const p = new Promise((resolve, reject) => {
    console.log(1);
    resolve()
}).then(() => console.log(2))

console.log(3)

// The output order is 1 3 2 0

conclusion

  1. Even if Promise's status is immediately updated to full, the callback function will not be executed immediately.
  2. The callback function is executed in the micro task of the event loop.
  3. Because callbacks are asynchronous, placing them in the execution of micro tasks can enable the subsequent synchronous code to be executed as soon as possible.

realization

const resolvePromise = cb => {
    setTimeout(() => {
       // Execute callback
    })
}

III Promise VS legacy callback

1. Callback hell

In the scenario of executing multiple asynchronous operations, the traditional callback needs to pass in the callback function as a parameter, which has the problem of "callback hell". Promise decouples the asynchronous operation from the callback function and does not need to pass in the callback function at the beginning.

// Traditional callbacks implement multiple asynchronous operations
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);
// Promise implements multiple asynchronous operations
doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

2. Exception capture

Note: exceptions here refer to callback function exceptions, not asynchronous operation exceptions.

// The following is a typical timeout callback. Since timeoutCallback is called in asynchronous operation, try catch cannot catch asynchronous exceptions.
try{
    setTimeout(()=>timeoutCallback("3 seconds passed"), 3000);
}catch(err){
    // The timeoutCallback exception cannot be caught here.
    console.log(err);
}

// Promise is responsible for calling the callback function. After the asynchronous operation ends and promise state changes, promise will add try catch when calling the callback function.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(3000).then(() => timeoutCallback("3 seconds passed")).catch(err=>{console.log(err)});

IV Promise application

1. Create the completed Promise

Promise.resolve() and Promise.reject() 

2. Encapsulate traditional callback

Due to the problems of callback hell and lack of asynchronous exception capture in traditional callbacks, Promise is used to encapsulate the existing traditional callbacks.

// The following is a typical timeout callback. Because timeoutCallback is executed asynchronously, exceptions in asynchronous callback cannot be caught.
try{
    setTimeout(()=>timeoutCallback("3 seconds passed"), 3000);
}catch(err){
    // The timeoutCallback exception cannot be caught here.
    console.log(err);
}

// Promise can separate asynchronous operation from callback. When callback is invoked when the asynchronous operation is completed, it will automatically add try catch to the callback function.
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(3000).then(() => timeoutCallback("3 seconds passed")).catch(err=>{console.log(err)});

3. Perform multiple asynchronous operations

// Parallel multiple asynchronous operations, and then enter the next operation after all operations are completed
Promise.all([promise1, promise2, promise3])
.then(([result1, result2, result3]) => { 
    // next step 
});
.catch(err){
    // When any operation reject, enter catch and return the corresponding reject information.
}

// Perform multiple asynchronous operations in sequence (chain call)
[promise1, promise2, promise3].reduce((pre, cur) => pre.then(()=>{return cur}), Promise.resolve())
.then(result3 => {
    /* use result3 */ 
}).catch(err=>{
    // When any operation reject, enter catch and return the corresponding reject information.
});
// In es2017, you can also use async/await to optimize the above code
let result;
try{
    for (const pro of [promise1, promise1, promise1]) {
        result = await pro(result);
    }
}catch(err){
    result = err;
}

reference

After reading it, I will write Promise principle, the most easy to understand version

Encapsulating worker with promise

Keywords: Javascript Front-end Promise

Added by broomstick on Tue, 04 Jan 2022 01:22:45 +0200