Illustrating the principle of Promise implementation -- the realization of Promise prototype method

This article starts with WeChat official account of vivo Internet technology.
Link: https://mp.weixin.qq.com/s/u8wuBwLpczkWCHx9TDt4Nw
By: Morrain

Promise is a solution of asynchronous programming. It was first proposed and implemented by the community. ES6 has written it into the language standard, unified its usage, and provided promise object natively. For more information about promise, please refer to teacher Ruan Yifeng's Promise object for getting started with ES6.

When learning Promise, many students know it but don't know why. They can't understand its usage. This series of articles gradually realize Promise from shallow to deep, and demonstrate with flow chart, examples and animation to achieve the purpose of deep understanding of Promise usage.

This series consists of the following chapters:

  1. Schematic principle of Promise (1) - basic implementation

  2. Illustration of Promise implementation principle (2) -- Promise chain call

  3. Illustration of the principle of Promise (3) -- the realization of Promise prototype method

  4. Illustration of the principle of Promise (4) -- the realization of Promise static method

1, [preface]

In the previous section, we implemented the chain call of Promise. Chained call is the most difficult and important part of Promise. Up to now, Promise has been implemented as follows:


class Promise {
    callbacks = [];
    state = 'pending';//Increase status
    value = null;//Save results
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        return new Promise(resolve => {
            this._handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    }
    _handle(callback) {
        if (this.state === 'pending') {
            this.callbacks.push(callback);
            return;
        }
        //If nothing is passed in then
        if (!callback.onFulfilled) {
            callback.resolve(this.value);
            return;
        }
        var ret = callback.onFulfilled(this.value);
        callback.resolve(ret);
    }
    _resolve(value) {
        if (value && (typeof value === 'object' || typeof value === 'function')) {
            var then = value.then;
            if (typeof then === 'function') {
                then.call(value, this._resolve.bind(this));
                return;
            }
        }
        this.state = 'fulfilled';//Change state
        this.value = value;//Save results
        this.callbacks.forEach(callback => this._handle(callback));
    }
}

This section mainly introduces the implementation of the project prototype method, including the implementation of catch, finally and rejected state.

2, [error handling]

In order to explain the principle, only onFulfilled is implemented. For a project, there are failures as well as successes. In case of failure, the status of the project should be marked as rejected and the registered onRejected should be executed. The Demo is as follows:

/**
 * Impersonate exception asynchronous request
 * @param {*} url
 * @param {*} s
 * @param {*} callback
 */
const mockAjax = (url, s, callback) => {
  setTimeout(() => {
    callback(url + 'Asynchronous request time consuming' + s + 'second', 'Error !');
  }, 1000 * s)
}
 
//demo reject
new Promise((resolve, reject) => {
 
    mockAjax('getUserId', 1, function (result, error) {
        if (error) {
            reject(error)
        } else {
            resolve(result);
        }
    })
 
}).then(result => {
    console.log(result);
}, error => {
    console.log(error);
});

With the previous experience of handling the completed state, it's easy to support error handling. You only need to add new reject logic to register callbacks and handle state changes.

//Complete implementation + reject
class Promise {
    callbacks = [];
    state = 'pending';//Increase status
    value = null;//Save results
    constructor(fn) {
        fn(this._resolve.bind(this), this._reject.bind(this));
    }
    then(onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            this._handle({
                onFulfilled: onFulfilled || null,
                onRejected: onRejected || null,
                resolve: resolve,
                reject: reject
            });
        });
    }
    _handle(callback) {
        if (this.state === 'pending') {
            this.callbacks.push(callback);
            return;
        }
 
        let cb = this.state === 'fulfilled' ? callback.onFulfilled : callback.onRejected;
 
        if (!cb) {//If nothing is passed in then
            cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
            cb(this.value);
            return;
        }
 
        let ret = cb(this.value);
        cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
        cb(ret);
    }
    _resolve(value) {
 
        if (value && (typeof value === 'object' || typeof value === 'function')) {
            var then = value.then;
            if (typeof then === 'function') {
                then.call(value, this._resolve.bind(this), this._reject.bind(this));
                return;
            }
        }
 
        this.state = 'fulfilled';//Change state
        this.value = value;//Save results
        this.callbacks.forEach(callback => this._handle(callback));
    }
    _reject(error) {
        this.state = 'rejected';
        this.value = error;
        this.callbacks.forEach(callback => this._handle(callback));
    }
}

The source code of demo reject

The operation results are as follows:

[Promse-1]:constructor
[Promse-1]:then
[Promse-2]:constructor
[Promse-1]:_handle state= pending
[Promse-1]:_handle callbacks= [ { onFulfilled: [Function],
    onRejected: [Function],
    resolve: [Function],
    reject: [Function] } ]
=> Promise { callbacks: [], name: 'Promse-2', state: 'pending', value: null }
[Promse-1]:_reject
[Promse-1]:_reject value= Error !
[Promse-1]:_handle state= rejected
//Something went wrong!
[Promse-2]:_reject
[Promse-2]:_reject value= undefined

3, [exception handling]

Just introduced error handling, which refers to the errors found in the constructor of Promise and notified through reject. If an exception occurs during the execution of onFulfilled or onRejected, what should be done? For this kind of exception, the handling is also very simple. You can use try catch to catch errors, and then set the corresponding project state to rejected state. The transformation method is as follows:

_handle(callback) {
        if (this.state === 'pending') {
            this.callbacks.push(callback);
            return;
        }
 
        let cb = this.state === 'fulfilled' ? callback.onFulfilled : callback.onRejected;
 
        if (!cb) {//If nothing is passed in then
            cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
            cb(this.value);
            return;
        }
 
        let ret;
 
        try {
            ret = cb(this.value);
            cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
        } catch (error) {
            ret = error;
            cb = callback.reject
        } finally {
            cb(ret);
        }
 
    }

The source code of demo error

Whether it's an error or an exception, it's ultimately implemented by reject. It can be seen that the final handling of errors and exceptions can be handled by onRejected in then. So add a catch method alone, which is the alias of. then(null, onRejected). As follows:

then(onFulfilled, onRejected) {
     return new Promise((resolve, reject) => {
         this._handle({
             onFulfilled: onFulfilled || null,
             onRejected: onRejected || null,
             resolve: resolve,
             reject: reject
         });
     });
 }
 catch(onError){
   return this.then(null, onError);
 }

The source code of demo catch

4, [Finally method]

In practical application, it's easy to encounter such a scenario, no matter what the final state of Promise is, we need to perform certain operations (onDone). For example, the server uses Promise to process requests, and then uses the finally method to shut down the server:

server.listen(port)
.then(function () {
    // do something
 })
.catch(error=>{
    // handle error
})
.finally(server.stop);

In essence, because it is a kind of deformation of then. The effect of the above demo is equivalent to the following code:

server.listen(port)
  .then(function () {
    // do something
  })
.catch(error=>{
    // handle error
})
.then(server.stop, server.stop);

Through the above analysis, finally looks like this:

finally(onDone){
    return this.then(onDone, onDone);
}

However, the onDone of the finally method does not care whether the state of the project is fully or rejected, so the operations in onDone should be state independent and should not have any parameters.

If you use then to implement it, it does not conform to the Promise specification< Why not .then(f, f)? >Description of. One is that onDone has parameters. The other is that when onDone returns a Promise, it will change the value state of Promise returned finally.

According to the specification, finally is implemented as follows:

catch(onError) {
  return this.then(null, onError);
}
finally(onDone) {
  if (typeof onDone !== 'function') return this.then();
  let Promise = this.constructor;
  return this.then(
    value => Promise.resolve(onDone()).then(() => value),
    reason => Promise.resolve(onDone()).then(() => { throw reason })
  );
}

The source code of demo finally

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success');
  }, 1000)
}).finally(() => {
  console.log('onDone')
})

For the above example, the execution result is as follows:

[Promse-1]:constructor
[Promse-1]:finally
[Promse-1]:then
[Promse-2]:constructor
[Promse-1]:_handle state= pending
[Promse-1]:_handle callbacks= [ { onFulfilled: [Function],
    onRejected: [Function],
    resolve: [Function],
    reject: [Function] } ]
=> Promise { callbacks: [], name: 'Promse-2', state: 'pending', value: null }
[Promse-1]:_resolve
[Promse-1]:_resolve value= success
[Promse-1]:_handle state= fulfilled
onDone
Promise::resolve
[Promse-3]:constructor
[Promse-3]:_resolve
[Promse-3]:_resolve value= undefined
[Promse-3]:then
[Promse-4]:constructor
[Promse-3]:_handle state= fulfilled
[Promse-4]:_resolve
[Promse-4]:_resolve value= success
[Promse-2]:_resolve
[Promse-2]:_resolve value= Promise {
  callbacks: [],
  name: 'Promse-4',
  state: 'fulfilled',
  value: 'success' }
[Promse-4]:then
[Promse-5]:constructor
[Promse-4]:_handle state= fulfilled
[Promse-2]:_resolve
[Promse-2]:_resolve value= success
[Promse-5]:_resolve
[Promse-5]:_resolve value= undefined

You can also restore this process by animating:

(Promise.finally demo animation) Click to open > >

The implementation of finally seems simple, but it is still difficult to understand. For the above instance, there are actually five Promise instances in the middle. As shown in the figure below:

There are so many prototype methods. The next section introduces two static methods

More content, please pay attention to vivo Internet technology WeChat official account.

Note: please contact Labs2020 for reprint.

Tags: Front-end Programming

Posted on Mon, 18 May 2020 03:09:54 -0400 by Dan06