30 minutes, let you understand the Promise principle thoroughly

Links to the original text

Preface

Some of the usual uses of promise have been documented a while ago. This article goes further to analyze how promise's rule mechanism is implemented. ps: This article is suitable for people who already know the use of promise. If you don't know the use of promise, you can move on to my last article. Bowen.

The promise source code for this article follows Promise/A+Specification To write (do not want to see the English version of the move) Promise/A+Standardized Chinese Translation)

Introduction

In order to make it easier for you to understand, we start with a scenario, let you think step by step, I believe you will be easier to understand.

Consider the following request processing to get the user id

//Example 1
function getUserId() {
    return new Promise(function(resolve) {
        //Asynchronous request
        http.get(url, function(results) {
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //Some Processing
})

getUserIdMethod returns apromise,It can be used by the ____________thenMethod Registration(Be carefulregisterThis word)staypromiseCallbacks performed when an asynchronous operation succeeds. This way of execution makes asynchronous calls very handy.

Principle analysis

So something like thisPromiseHow to achieve it? In fact, according to the above sentence, it is still very basic to achieve a prototype. easy .

Minimalism promise embryonic form

function Promise(fn) {
    var value = null,
        callbacks = [];  //Callbacks are arrays, because there may be many callbacks at the same time

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

The above code is simple, and the general logic is as follows:

  1. Call the then method, and put the callbacks that you want to execute when Promise asynchronous operation succeeds into the callbacks queue. In fact, you can register the callback function and think in the direction of the observer mode.

  2. When a Promise instance is created, the incoming function is assigned a parameter of function type, resolution, which receives a parameter value, representing the result returned by an asynchronous operation. When a step is successfully executed, the user calls the resolution method. At this time, the real operation is to hold the callbacks in the callbacks queue one by one. That's ok;

Consider the code in Example 1. First, when new Promise is sent, the function passed to promise sends an asynchronous request, then calls the promise object's then attribute, registers the callback function that requests success, and then, when the asynchronous request is sent successfully, calls the resolve(results.id) method, which executes the callback array registered by the then method. .

Believe careful people should be able to see that the then method should be able to call chains, but the simplest version of the above obviously can not support chain calls. It is also very simple to make the then method support chain invocation.

this.then = function (onFulfilled) {
    callbacks.push(onFulfilled);
    return this;
};

see? A simple sentence can make a chain call similar to the following:

// Example 2
getUserId().then(function (id) {
    // Some Processing
}).then(function (id) {
    // Some Processing
});

Joining the Delay Mechanism

Careful students should find that there may still be a problem with the above code: what if the resolve function is executed before the then method registers callbacks? For example, the function inside promise is a synchronous function:

// Example 3
function getUserId() {
    return new Promise(function (resolve) {
        resolve(9876);
    });
}
getUserId().then(function (id) {
    // Some Processing
});

This is clearly not allowed, and the Promises/A+specification explicitly requires callbacks to be executed asynchronously to ensure a consistent and reliable execution sequence. So we need to add some processing to ensure that the then method registers all callbacks before resolve executes. We can modify the resolve function in this way:

function resolve(value) {
    setTimeout(function() {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }, 0)
} 

The idea of the above code is also simple, that is, through the setTimeout mechanism, the logic of executing callbacks in resolve is placed at the end of the JS task queue to ensure that when resolve executes, the callback function of the then method is registered.

However, there seems to be a problem. Think about it: if the Promise asynchronous operation succeeds, callbacks registered before the asynchronous operation succeeds will be executed, but callbacks registered after the Promise asynchronous operation succeeds will never be executed again, which is obviously not what we want. .

Added status

Well, in order to solve the problems raised in the previous section, we have to add the state mechanism, which is known as pending, fulfilled, rejected.

Promises/A+2.1 Promise States specifies that pending can be converted to fulfilled or rejected and can only be converted once, that is to say, if pending is converted to fulfilled state, then it cannot be converted to rejected again. And fulfilled and rejected states can only be converted from pending, and they can not be converted to each other. A picture is worth a thousand words:

The improved code is as follows:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        if (state === 'pending') {
            callbacks.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };

    function resolve(newValue) {
        value = newValue;
        state = 'fulfilled';
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                callback(value);
            });
        }, 0);
    }

    fn(resolve);
}

The idea behind the above code is that when resolve executes, it sets the state to fulfilled, and then calls the new callback added by the n to execute immediately.

There's no place to set state as rejected, and in order to focus on the core code, there's a section on this later.

Chain Promise

So here comes the problem again. If the user is still registered as a Promise in the then function, how to solve it? For example, the following example 4:

// Example 4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // job Processing
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}

This scenario is believed that people who have used promises will know that there will be many, so similar to this is the so-called chain Promise.

Chain Promise refers to the start of the next promise after the current promise reaches fulfilled state. So how do we connect the current promise with the future neighbor promise? This is the difficulty here.

Actually, it's not so hot, just return a promise in the then method. That's what Promises/A+2.2.7 says.~

Let's take a look at the hidden mystery of the then method and the resolve method to modify the code:


function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        return new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }
        //If nothing is passed in that
        if(!callback.onResolved) {
            callback.resolve(value);
            return;
        }

        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }

    
    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve);
}

Combining the code of Example 4, we analyze the above code logic. In order to facilitate reading, I paste the code of Example 4 here:

// Example 4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // job Processing
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}
  1. In the then method, a new Promise instance is created and returned, which is the basis of serial Promise and supports chain invocation.

  2. The handle method is the internal method of promise. The parameter onFulfilled passed in by the then method and the resolve passed in when creating a new Promise instance are push ed into the callbacks queue of the current promise, which is the key to link the current promise with the next promise (here we must analyze the role of handle well).

  3. The promise (getUserId promise) generated by getUserId succeeds in asynchronous operation, and its internal method resolve is executed. The parameters passed in are the result id of the asynchronous operation.

  4. Call the handle method to handle callbacks in the callbacks queue: the getUserJobById method to generate a new promise (getUserJobById promise)

  5. The resolve method of the new promise (called bridge promise) generated by the then method of getUserId promise before execution is passed in as getUserJobById promise. In this case, the resolve method is passed into the then method of getUserJobById promise and returned directly.

  6. When the getUserJobById promise asynchronous operation succeeds, perform the callback in its callbacks: the resolve method in getUserId bridge promise

  7. Finally, the callbacks in the callbacks of the next-neighbor promise of getUserId bridge promise are executed.

To be more straightforward, you can see the following picture. One picture is worth thousands of words.

Failure handling

When an asynchronous operation fails, it is marked as rejected and registered failback is performed:

//Example 5
function getUserId() {
    return new Promise(function(resolve) {
        //Asynchronous request
        http.get(url, function(error, results) {
            if (error) {
                reject(error);
            }
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //Some Processing
}, function(error) {
    console.log(error)
})

With previous experience in dealing with fulfilled states, it is easy to support error handling by adding new logic in registering callbacks and handling state changes:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled, onRejected) {
        return new Promise(function (resolve, reject) {
            handle({
                onFulfilled: onFulfilled || null,
                onRejected: onRejected || null,
                resolve: resolve,
                reject: reject
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }

        var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
            ret;
        if (cb === null) {
            cb = state === 'fulfilled' ? callback.resolve : callback.reject;
            cb(value);
            return;
        }
        ret = cb(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve, reject);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        execute();
    }

    function reject(reason) {
        state = 'rejected';
        value = reason;
        execute();
    }

    function execute() {
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve, reject);
}

The above code adds a new reject method, which can be called when the asynchronous operation fails. At the same time, the common parts of the resolve and reject are extracted to form the execute method.

Error bubbling is a very practical feature that the code already supports. When a callback that fails to specify an asynchronous operation is found in the handle, the bridge promise (the promise returned by the function) is set to rejected state directly, thus achieving the effect of performing subsequent failed callbacks. This helps to simplify the cost of fail handling for serial Promise, because a set of asynchronous operations often corresponds to an actual function, and fail handling methods are usually consistent:

//Example 6
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // Handling job s
    }, function (error) {
        // Errors in getUserId or getUerJobById
        console.log(error);
    });

exception handling

Careful students will think: if the implementation of successful callback, failure callback code error? For such exceptions, try-catch can be used to catch errors and set bridge promise to rejected state. The handle method is modified as follows:

function handle(callback) {
    if (state === 'pending') {
        callbacks.push(callback);
        return;
    }

    var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
        ret;
    if (cb === null) {
        cb = state === 'fulfilled' ? callback.resolve : callback.reject;
        cb(value);
        return;
    }
    try {
        ret = cb(value);
        callback.resolve(ret);
    } catch (e) {
        callback.reject(e);
    } 
}

If in an asynchronous operation, resolve or reject is executed many times and the callback is repeated, it can be solved by a built-in flag bit.

summary

When you first look at the promise source code, you can't understand the operation mechanism of the then and resolution functions very well, but if you calm down, you can deduce it according to the logic of executing the promise. Here we must pay attention to the point that the function in promise is only the code that needs to be executed after registration. The real execution is executed in the resolve method. It will save more effort to analyze the source code after clearing up this layer.

Now review the implementation of Promise, which mainly uses the observer pattern in the design pattern:

  1. Through Promise.prototype.then and Proise. prototype. catch method, the observer method is registered in the observee Promise object, and a new Promise object is returned so that it can be called in a chain.

  2. The observer manages the internal pending, fulfilled and rejected state transitions, and actively triggers the state transitions and notifies the observer through the resolve and reject methods passed in the constructor.

Reference

Deep Understanding of Promise
JavaScript Promises ... In Wicked Detail

Keywords: Javascript Attribute

Added by Russ on Sun, 30 Jun 2019 03:33:24 +0300