[book translation] Chapter 3 of JavaScript concurrent programming uses promises to achieve synchronization

Time:2020-2-13

This is the third chapter of my translation of JavaScript concurrency, which uses promises to realize synchronization. This book mainly uses promises, generator, web workers and other technologies to explain the practice of JavaScript concurrent programming.

Full book translation address: https://github.com/yzsunli/javascript’concurrency’u translation. Due to the limited ability, there must be places where the translation is unclear or even wrong. Welcome to point out the issue and thank you.

Promises was implemented in JavaScript class libraries a few years ago. It all started with the promises / A + specification. The implementations of these libraries have their own forms, and until recently (ES6 to be exact), the promises specification was not included in the JavaScript language. Like the title – it helps us implement the synchronization principle.

In this chapter, we will first briefly introduce the various terms in promises, so as to make it easier to understand the later part of this chapter. Then, in various ways, we will use promises to solve some of the current problems and make concurrent processing easier. are you ready?

Promise related terms

Before we dive into the code, let’s take a moment to make sure we have a firm grasp of promises related terms. There are promise instances, but there are various states and methods. If we can figure out promise, the following chapters will be easier to understand. These explanations are brief and easy to understand, so if you have used promises, you can quickly read these terms and review them.

Promise

Promise, as the name suggests, is a promise. Treat promise as a proxy for a value that does not yet exist. Promise allows us to write concurrent code better because we know that values will exist at some point in the future, and we don’t have to write a lot of state checking boilerplate code.

State

Promises is always in one of three states:

• wait: This is the first state after promise is created. It waits until it is finished or rejected.

• complete: the promise value has been processed and can be provided with a then() callback function.

• reject: there is a problem processing the value of promise. There is no data now.

An interesting feature of promise States is that they only transition once. They are either waiting to finish or waiting to be rejected. Once they do this state transition, they lock in that state later.

Executor (executor)

The actuator function is responsible for resolving the value in some way and will be in a waiting state. Call this function as soon as promise is created. It requires two parameters: the resolver function and the rejector function.

Resolver

A parser is a function passed as a parameter to an executor function. In fact, this is convenient because we can pass the parser function to another function, and so on. The location where the parser function is called is not important, but when it is called, promise enters a complete state. This change in state triggers the then () callback – which we’ll see later.

Rejector (rejector)

The denier is similar to the parser. It is the second parameter passed to the executor function and can be called from anywhere. When it is called, promise changes from a wait state to a reject state. This change in state calls the error callback function, if any, to then() or catch().

Thenable

If an object has a then () method that takes a completion callback and a reject callback as parameters, the object is then able. In other words, promise is thenable. But in some cases, we may want to implement specific parsing semantics.

Complete and reject promises

If the terms just introduced in the previous section sound confusing to you, don’t worry. From this section on, we will see the application practice of all these promises terms. Here, we will show some simple examples of promise resolution and rejection.

Complete promises

The parser is a function that, as the name suggests, completes our promise. This is not the only way to complete promise – we’ll explore more advanced ways later. But so far, this method is the most common. It is passed to the actuator function as the first argument. This means that the executor can do promise directly by simply calling the parser. But it’s not very practical, is it?

More often, the promise executor function sets up an upcoming asynchronous operation – such as making a network call. Then, in the callback functions of these asynchronous operations, we can complete the promise. Passing a parse function in our code may feel counterintuitive at first, but it makes sense once we start using them.

Parser function is a relatively difficult function to understand compared with promise. It can only complete promise once. We can call the parser many times, but only the first call will change the state of promise. Here is a diagram that describes the possible states of promise; it also shows how the states change:

[book translation] Chapter 3 of JavaScript concurrent programming uses promises to achieve synchronization

Now, let’s look at some promise code. Here, we will complete a promise, which will call then() to complete the callback function:

//The executor function used by our promise.
// the first parameter is the parser function, which is called Promise after 1 second.
function executor(resolve) {
    setTimeout(resolve, 1000);
}

//Our promise completion callback function.
//This simply stops the timer after our executor function runs.
function fulfilled() {
    console.timeEnd('fulfillment');
}

//Create promise and run it now,
//Then start a timer to see how long it takes to complete the call.
var promise = new Promise(executor);
promise.then(fulfilled);
console.time('fulfillment');

We can see that the fulfilled () function is called when the parser function is called. The executor does not actually call the parser. Instead, it passes the parser function to another asynchronous function – settimeout(). Executors are not asynchronous code that we try to figure out. You can think of an actuator as a coordinator that orchestrates asynchronous operations and determines when to perform promise.

The previous example did not resolve any values. This is an effective use case when the caller of an operation needs to confirm its success or failure. Instead, let’s try to parse a value this time, as follows:

//The execution function used by our promise.
//After creating promise, set a one second delay to call "resolve()",
//And parse to return a string value - "done!".
function executor(resolve) {
    setTimeout(() => {
        resolve('done!');
    }, 1000);
}

//Our promise completion callback takes a value parameter.
//This value is passed to the parser.
function fulfilled(value) {
    console.log('resolved', value);
}

//Create our promise, provide the executor and complete the callback function.
var promise = new Promise(executor);
promise.then(fulfilled);

We can see that this code is very similar to the previous example. The difference is that our parser function is actually called inside the closure of the callback function passed to settimeout(). This is because we are parsing a string value. There is also a function that passes the parsed parameter value to our fulfilled () function.

Deny promises

Promise executor functions don’t always work as expected, and when problems arise, we need to reject promise. This is a transition from the wait state to another possible state. This is not going to be a complete state but a rejected state. This causes different callbacks to be executed, separate from the completion of the callback function. Fortunately, the mechanism of rejecting promise is very similar to that of completing promise. Let’s see how this works:

//This actuator rejects promise after a one second delay.
//It uses the reject callback function to change the state,
//And pass the rejected parameter value to the callback function.
function executor(resolve, reject) {
    setTimeout(() => {
        reject('Failed');
    }, 1000);
}

//Function used as reject callback.
//It receives a parameter value that provides a reject.
function rejected(reason) {
    console.error(reason);
}

//Create promise and run the actuator.
//Use the "catch()" method to receive the reject callback function.
var promise = new Promise(executor);
promise.catch(rejected);

This code looks very similar to what you saw in the previous section. We set the timeout and we rejected it instead of completing it. This is done using the rejector function and passed to the executor as a second parameter.

We use the catch () method instead of the then () method to set the reject callback function. We’ll see later in this chapter how the then () method can be used to handle both completion and rejection callbacks. The reject callback in this example prints only the reason for the failure. It is usually important to provide this return value. When we complete promise, the return value is also common, though not required. On the other hand, for reject function, it is seldom to output reject reason only through callback function.

Let’s look at another example, which catches exceptions thrown in the executor and provides a more meaningful reason for rejecting the callback function:

//The promise executor throws an error,
//And call the reject callback function to output the error information.
new Promise(() => {
    throw new Error('Problem executing promise');
}).catch((reason) => {
    console.error(reason);
});

//This promise executor caught an error,
//And call the reject callback function to output more meaningful error information.
new Promise((resolve, reject) => {
    try {
        var size = this.name.length;
    } catch (error) {
        reject(error instanceof TypeError ? 'Missing "name" property' : error);
    }
}).catch((reason) => {
    console.error(reason);
});

The interesting thing about the first project in the previous example is that it does change the state, even if we don’t use resolve () or reject () to explicitly change the state of the project. Ultimately, however, it’s important to change the state of promise; we’ll cover this in the next section.

Empty Promises

Although in fact the executor function passes a completion callback and a reject callback, there is no guarantee that promise will change state. In some cases, promise is only suspended, and neither the completion callback nor the reject callback is triggered. This may not be a problem, in fact, a simple promises, it is easy to find and repair the unresponsive promises. However, as we enter a more complex scenario, the completion callback of one promise can be used as the callback result of several other promises. If a promise cannot be completed or rejected, then the whole process will crash. This situation is very troublesome to debug; the following figure can clearly see this situation:

[book translation] Chapter 3 of JavaScript concurrent programming uses promises to achieve synchronization

In the figure, we can see which promise causes the dependent promise to hang, but it is not easy to solve this problem by debugging the code. Now let’s look at the execution function that causes promise to hang:

//This promise can run the executor function normally.
//But the "then()" callback function will never be executed.
new Promise(() => {
    console.log('executing promise');
}).then(() => {
    console.log('never called');
});

//At this point, we don't know what's wrong with promise
console.log('finished executing, promise hangs');

But is there a safer way to deal with this uncertainty? In our code, we don’t need to suspend execution functions that don’t need to be completed or rejected. Let’s implement an executor wrapper function, like a safety net, to let promises, which has not been completed for a long time, execute the reject callback function. This will unveil the mystery of the poor promise scenario:

//The wrapper for the promise actuator function,
//Throws an error after a given timeout.
function executorWrapper(func, timeout) {
    //This is the function actually called.
    //It requires a parser function and a denier function as parameters.
    return function executor(resolve, reject) {
        //Set up our timer.
        //When time arrives, we can use the timeout message to reject promise.
        var timer = setTimeout(() => {
            reject('Promise timed out after $​​ {timeout} MS');
        }, timeout);
        
        //Call our original executor wrapper function.
        //We actually wrapped the completion callback function as well
        //And reject callback function, so when
        //When performers call them, the timers are cleared.
        func((value) => {
            clearTimeout(timer);
            resolve(value);
        }, (value) => {
            clearTimeout(timer);
            reject(value);
        });
    };
}

//The promise timed out after execution,
//A timeout error message is passed to the reject callback.
new Promise(executorWrapper((resolve, reject) => {
    setTimeout(() => {
        resolve('done');
    }, 2000);
}, 1000)).catch((reason) => {
    console.error(reason);
});

//The promise runs as expected after execution,
// call resolve () before the end of the timer.
new Promise(executorWrapper((resolve, reject) => {
    setTimeout(() => {
        resolve(true);
    }, 500);
}, 1000)).then((value) => {
    console.log('resolved', value);
});

Improve promises

Now that we have a good understanding of the implementation mechanism of promises, this section details how to use promises to solve specific problems. Usually, this means that when promises are completed or rejected, we will achieve some of our goals.

We’ll start by looking at the task queues in the JavaScript interpreter and what these mean to our parsing callback functions. Then, we will consider using the result data of promise, handling errors, creating better abstractions to respond to problems, and the problems. Let’s start.

Process task queue

The concept of JavaScript task queues is mentioned in “Chapter 2, JavaScript runtime model.”. Its main responsibility is to initialize the new execution context stack. This is a common task queue. However, there is another type of queue that is dedicated to executing promises callbacks. This means that if they all exist, the algorithm will select a task to execute from these queues.

Promises has built-in concurrency semantics for good reason. If a promise is used to ensure that a value is eventually parsed, it makes sense to give high priority to the code that responds to it. Otherwise, when the value arrives, the code that processes it may have to wait a long time after other tasks to execute. Let’s write some code to demonstrate the following concurrency semantics:

//Create 5 promises, record their execution time,
//And when they respond to the return value.
for (let i = 0; i < 5; i++) {
    new Promise((resolve) => {
        console.log('execting promise');
        resolve(i);
    }).then((value) => {
        console.log('resolved', i);
    });
}

//This is called before any promise completes the callback,
//Because the stack task needs to be completed before the interpreter enters the promise parsing callback queue,
//The current 5 "then()" callbacks will be set back.
console.log('done executing');

//→
//execting promise
//execting promise
// ...
//done executing
//resolved 1
//resolved 2
// ...

Reject callbacks follow the same semantics.

Return data using promise

So far, we’ve seen some examples in this chapter, where the parser function completes promise and returns a value. The value passed to this function is the value finally passed to the completion callback function. Call the resolver by having the executor set a method for any asynchronous operation, such as settimeout(), to delay passing the value. But in these examples, the caller doesn’t actually wait for any value; we only use setTimeout () as an example asynchronous operation. Let’s take a look at the situation where we actually have no value. Asynchronous network requests need to get it:

//A generic function to get resources from the server,
//Returns a promise.
function get(path) {
    return new Promise((resolve, reject) => {
        var request = new XMLHttpRequest();
        
        //Promise parses the JSON data after the data is loaded.
        request.addEventListener('load', (e) => {
            resolve(JSON.parse(e.target.responseText));
        });

        //Promise performs a reject callback function when the request fails.
        request.addEventListener('error', (e) => {
            Reject (e.target.statustext | 'unknown error');
        });


        //If the request is aborted, we call the complete callback function
        request.addEventListener('abort', resolve);
        
        request.open('get', path);
        request.send();
    });
}

//We can attach our "then()" handler directly
//Go to "get()" because it returns a promise.
//Before parsing, the value used here is a real asynchronous operation,
//Because you have to send a request to get the value remotely.
get('api.json').then((value) => {
    console.log('hello', value.hello);
});

Using functions like get (), they not only always return native types like promise, but also encapsulate some annoying asynchronous details. Dealing with XMLHttpRequest objects in our code is not pleasant. We have simplified the various situations that can be returned. Instead of always having to create handlers for load, error, and abort events, we just need to care about one interface – promise. This is all about the principle of synchronous concurrency.

Error callback

There are two ways to handle rejected promises. In other words, provide an error callback. The first is to use the catch () method, which uses a single callback function. Another way is to pass the rejected callback as the second argument to then().

Using the then () method to handle the reject callback function works better in some cases, and it should be used instead of the catch () function. The first scenario is to write code in which promises and thenable objects can be interchanged. The catch () method is not a necessary part of thenable. The second scenario is when we build a callback chain, which we will explore later in this chapter.

Let’s look at some code that compares two ways to provide promises with a reject callback function:

//The promise executor will randomly execute the completion callback or reject the callback
function executor(resolve, reject) {
    cnt++;
    Math.round(Math.random()) ? 
        resolve(`fulfilled promise ${cnt}`) :
        reject(`rejected promise ${cnt}`);
}

//Use the "log()" and "error()" functions as simple callback functions
var log = console.log.bind(console),
    error = console.error.bind(console),
    cnt = 0;

//Create a promise and pass in a reject callback through the "catch()" method.
new Promise(executor).then(log).catch(error);

//Create a promise and pass in a reject callback through the 'then()' method.
new Promise(executor).then(log, error);

We can see that the two methods are actually very similar. There is no real advantage in code aesthetics. However, when it comes to using thenables, the then () method has an advantage, as we’ll see later. However, since we don’t actually use promise instances in any way, there’s no need to worry about catch () and then () being used to register reject callbacks, except to add callbacks.

Always respond

Promises always end up in a finished or rejected state. We usually pass in different callback functions for each state. However, we are likely to want to do some of the same for both States. For example, if a component that uses promise changes state while promise waits, we want to make sure that the state is cleared after the promise is completed or rejected.

We can write code in such a way that each callback of completion and rejection status performs these operations, or each of them can call to perform some common cleanup functions. The following figure shows this way:

[book translation] Chapter 3 of JavaScript concurrent programming uses promises to achieve synchronization

Does it make sense to assign cleanup tasks to promise rather than to other individual results? In this way, the callback function that runs when parsing promise focuses on what it needs to do with the value, while the reject callback focuses on handling the error. Let’s see if we can use the always () method to write some code to extend promises:

//Extend the use of the "always()" method on the promise prototype.
//Whether promise completes or rejects, the given function is always called.
Promise.prototype.always = function(func) {
    return this.then(func, func);
};

//Create promise randomly completed or rejected.
var promise = new Promise((resolve, reject) => {
    Math.round(Math.random()) ? 
    resolve('fullfilled') : reject('rejected');
});

//Pass promise completion and reject callbacks.
promise.then((value) => {
    console.log(value);
}, (reason) => {
    console.error(reason);
});

// this callback function will always be invoked after the callback is executed above.
promise.always((value) => {
    console.log('cleaning up...');
});

Note that the order is important here. If we call always () before then (), then the function will still run, but it will be in.
The callback runs before it is provided to then(). We can actually call always () before and after then () to complete or reject the callback
Run the code before and after.

Handle other promises

So far, most of the promise we saw in this chapter are directly executed by executing program functions, or the results of calling parsers from asynchronous operations when the duty is completed. Passing callback functions like this is actually very flexible. For example, the execution program does not even need to perform any task except to store the parser function somewhere for later calling it to parse promise.

This can be particularly useful when we find ourselves in a more complex synchronization scenario that requires multiple values that have been passed to the caller. If we have a callback function to handle, we can handle promise. Let’s take a look at the multiple promises of the analytic function storing the code, so that each promise can be processed later:

//Stores a list of parser functions.
var resolvers = [];

//Create 5 new promises in the actuator,
//The parser is pushed to the "resolvers" array.
//We can call back every promise.
for(let i = 0; i < 5; i++) {
    new Promise(() => {
        resolvers.push(resolve);
    }).then((value) => {
        console.log(`resolved ${i + 1}`, value);
    });
}

//Set a delay function after 2S,
//When it runs, we iterate through every parser function in the "parser" array,
//And pass in a return value to call it.
setTimeout(() => {
    for(resolver of resolvers) {
        resolver(true);
    }
}, 2000);

As this example shows, we don’t have to deal with them within the executor function. In fact, we don’t even need to explicitly reference promise instances after creating and setting up executors and completing functions. The parser function is stored somewhere and contains a reference to promise.

Class promise object

The promise class is a native JavaScript type. However, we do not always need to create new promise instances to achieve the same synchronization. We can use the static promise. Resolve () method to resolve these objects. Let’s see how to use this method:

//The "promise. Resolve()" method can handle thenable objects.
//This is an executor like object with a "then()" method.
//This actuator will randomly complete or reject promise.
Promise.resolve({then: (resolve, reject) => {
    Math.round(Math.random()) ? resolve('fulfilled') : reject('rejected');

    //This method returns a promise, so we can
    //Set the completed and rejected callback functions.
}}).then((value) => {
    console.log('resolved', value);
}, (reason) => {
    console.error('reason', reason);
});

We’ll revisit the promise. Resolve () method in the last section of this chapter to learn more about use cases.

Establish callback chain

Each of the promise methods we introduced earlier in this chapter will return promise. This allows us to call these methods again on the return value, resulting in a chain of then (). Then () calls, and so on. One of the challenging aspects of chained promise is that the promise method returns a new instance. That is to say, we will discuss the invariance of promise to some extent in this section.

As our applications get larger and larger, concurrency challenges increase. This means that we need to consider better ways to take advantage of native synchronization semantics, such as promises. Just like any other primitive value in JavaScript, we can pass them from function to function. We have to deal with promises – passing them in the same way and build on the chain of callback functions.

Promises only changes state once

Promise is initially a wait state, and they end up in a completed or rejected state. Once promise changes to one of these States, they lock in that state. There are two interesting side effects.

First, multiple attempts to complete or reject promise will be ignored. In other words, parsers and deniers are idempotent – only the first call has an impact on promise. Let’s see how this code executes:

//This executor function attempts to resolve promise twice,
//But the completed callback is called only once.
new Promise((resolve, reject) => {
    resolve('fulfilled');
    resolve('fulfilled');
}).then((value) => {
    console.log('then', value);
});

//This executor function attempts to reject promise twice,
//But rejected callbacks are called only once.
new Promise((resolve, reject) => {
    reject('rejected');
    reject('rejected');
}).catch((reason) => {
    console.error('reason');
});

Another implication of promises changing state only once is that promises can be processed before a callback is added or rejected. Competitive conditions, such as this, are brutal realities of concurrent programming. Typically, callback functions are added to promise at creation time. Because JavaScript runs to completion, the task queue for promise parsing callbacks is not processed until the callback is added. But what if promise immediately resolves in execution? What happens if you add a callback to promise in another JavaScript execution context? Let’s see if we can use some code to better illustrate these situations:

//This actuator function immediately resolves promise. When adding a "then()" callback,
//Promise has been parsed. But the callback function still calls with the resolved value.
new Promise((resolve, reject) => {
    resolve('done');
    console.log('executor', 'resolved');
}).then((value) => {
    console.log('then', value);
});

//Create a new promise executor function that resolves immediately.
var promise = new Promise((resolve, reject) => {
    resolve('done');
    console.log('executor', 'resolved');
});

//This callback is executed immediately after promise parsing.
promise.then((value) => {
    console.log('then 1', value);
});

//This callback was not added to another promise after the promise resolution,
//It is still called immediately and gets the resolved value.
setTimeout(() => {
    promise.then((value) => {
        console.log('then 2', value);
    });
}, 1000);

This code shows a very important feature of promises. Whenever an execution callback is added to promise, the code that uses promise does not change, whether it is in a temporarily suspended or resolved state. On the surface, it doesn’t seem like a big deal. But this type of race condition checking requires more concurrent code to protect itself. Instead, promise native syntax handles this for us, and we can start to think of asynchronous values as primitive types.

Unalterable promises

Promises are not really immutable. They change state, and the then () method adds the callback function to promise. However, there are some immutable promises features worth discussing here, because they will affect our promise code in some cases.

Technically, the then () method does not actually change the promise object. It creates the so-called promise capability, an internal JavaScript record that references promise, and the functions we add. Therefore, it is not the real syntax in the JavaScript language.

This is a diagram showing what happens when we link two or more then() calls together:

[book translation] Chapter 3 of JavaScript concurrent programming uses promises to achieve synchronization

As you can see, the then () method does not return the same instance that was called with the context. Instead, then () creates a new promise instance and returns it. Let’s look at some code to further explain what happens when we link promises together using then():

//Create a promise that resolves immediately,
//And stored in "promise 1".
var promise1 = new Promise((resolve, reject) => {
    resolve('fulfilled');
});

//Use the 'then()' method of 'promise1' to create a
//New promise instance, stored in "promise 2".
var promise2 = promise1.then((value) => {
    console.log('then 1', value);
    //→then 1 fulfilled
});

//Create a 'then()' callback for 'PROMISE2'. This is actually
//Create a third promise instance, but we don't use it for anything.
promise2.then((value) => {
    console.log('then 2', value);
    //→then 2 undefined
});

//Make sure "promise 1" and "promise 2" are actually different objects
console.log('equal', promise1 === promise2);
//→equal false

We can see clearly that the two instances of creating promise are independent promise objects in this example. It is worth noting that before the second promise is executed, it must have executed the first promise. However, we can see that the value is not passed to the second promise. We will address this in the next section.

As many then() callbacks as there are promise objects

As we saw in the previous section, promises created with then () bind to their creators. That is, when the first promise completes, the promise that binds it also completes, and so on. But we also found a small problem. Resolved values are not passed to the first callback function. The reason for this is that each callback that runs in response to promise parsing is the return value of the first callback being sent to the second callback, and so on. The reason our first callback takes a value as a parameter is because this obviously happens in the promise mechanism.

Let’s look at another example of promise chain. This time, we will explicitly return the value in the callback function:

//Create a new promise random call to resolve or reject the callback.
new Promise((resolve, reject) => {
    Math.round(Math.random()) ?
    resolve('fulfilled') : reject('rejected');
}).then((value) => {
    //Call the return value when the original promise is completed,
    //In case another promise is linked to this one.
    console.log('then 1', value); 
    return value;
}).catch((reason) => {
    //Link to the second promise,
    //Executed when the callback is rejected.
    console.error('catch 1', reason);
}).then((value) => {
    //Link to the third promise,
    //Get the expected value and return the value to any next promise callback.
    console.log('then 2', value);
    return value;
}).catch((reason) => {
    //It's never called here,
    //Reject callbacks are not passed through the promise chain.
    console.error('catch 2', reason);
});

It looks good. We can see that the parsed value is passed through the promise chain. There is an exception – the reject callback is not passed back. Instead, only the first promise reject callback in the chain will execute. The remaining promise callbacks are only done, not rejected. This means that the last catch() callback will never run.

When we link promise together in this way, our execution callback needs to be able to handle error conditions. For example, a resolved value might have an error attribute, which you can check for specific problems.

Promises delivery

In this section, we talk about the use of promise as a primitive value. What we often do with raw values is pass them as arguments to the function and return them from the function. The key difference between promise and other native grammars is how we use them. Other values are always present, while promise’s value will not exist until some point in the future. Therefore, we need to define some operation procedures through callback function to execute when the value is obtained.

The advantage of promises is that the interface used to provide these callback functions is small and consistent. When we couple values with the code that will act on them, we don’t need to create synchronization mechanisms on our own. These units can be used in our applications like any other value, and concurrency semantics are common. This is the diagram of several promise functions transferring to each other:

[book translation] Chapter 3 of JavaScript concurrent programming uses promises to achieve synchronization

At the end of the function stack call, we get a promise object that completes the parsing of several promises. The whole promise chain starts from the completion of the first promise. More important than the mechanism of how to traverse the promise chain is that all these functions are free to use the value passed by the promise without affecting other functions.

There are two concurrency principles here. First, we can only process the value once by performing an asynchronous operation; each callback function is free to use the parsed value. Secondly, we do a good job in abstract synchronization mechanism. In other words, the code doesn’t have a lot of duplicate code. Let’s see what the code passing promise actually looks like:

//Simple utility functions,
//Combine several smaller functions into one function.
function compose(...funcs) {
    return function(value) {
        var result = value;
        
        for(let func of funcs) {
            result = func(value);
        }
        return result;
    };
}

//Accept a promise or a completion value.
//If this is a promise, it adds a "then()" callback and returns a new promise.
//Otherwise, it performs an "update" and returns a value.
function updateFirstName(value) {
    if (value instanceof Promise) {
        return value.then(updateFirstName);
    }

    console.log('first name', value.first); 
    return value;
}

//Similar to the above function,
//It just performs a different UI "update.".
function updateLastName(value) {
    if (value instanceof Promise) {
        return value.then(updateLastName);
    } 

    console.log('last name', value.last); 
    return value;
}

//Similar to the above function,除了它
//It just performs a different UI "update.".
function updateAge(value) {
    if (value instanceof Promise) {
        return value.then(updateAge);
    }

    console.log('age', value.age);
    return value;
}

//A promise object,
//After a second delay,
//Carry a data object to complete promise.
var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve({
            first: 'John',
            last: 'Smith',
            age: 37
        });
    });
}, 1000);

//We assemble an "update()" function to update various UI components.
var update = compose(
    updateFirstName,
    updateLastName,
    updateAge
);

//Use promise to call our update function.
update(promise);

The key functions here are our update functions – updatefirstname(), updatelastname(), and updateage(). They are very flexible and accept a promise or promise return value. If any of these functions take promise as an argument, they return the new promise by adding the then () callback function. Notice that it adds the same function. Updatefirstname() will add updatefirstname() as the callback. When the callback is triggered, it will be used with the normal object used to update the UI this time. So if promise fails, we can continue to update the UI.

Promise checks that each function requires three lines, which is not very abrupt. The end result is easy to read and flexible code. The order doesn’t matter; we can wrap our update() function in a different order, and the UI components will all be updated in the same way. We can pass ordinary objects directly to update (), and everything will be executed the same. Concurrent code that doesn’t look like concurrent code is a major success here.

Synchronize multiple promises

Earlier in this chapter, we explored a single promise instance that parses a value, triggers a callback, and may be passed to other promises for processing. In this section, we’ll cover several static promise methods that can help us deal with situations where we need to synchronize multiple promise values.

First of all, we will deal with the situation that the components we develop need to access multiple asynchronous resources synchronously. Then, we’ll look at unusual situations, such as asynchronous operations becoming meaningless due to events in the UI before processing.

Wait for promises

When we are waiting to process multiple promises, maybe we need to transform multiple data sources and provide them to a UI component. We can use the promise. All () method. It takes a collection of promise instances as input and returns a new promise instance. A new instance is returned only when all the entered promises have been completed.

The then () function is the callback we provide for promise to create a new promise. A set of analytical values is given as input. These values correspond to the location of the index input promise. This is a very powerful synchronization mechanism, which can help us implement the principle of synchronous concurrency, because it hides all processing records.

We don’t need a few callbacks to coordinate the promise state they are bound to. We only need a callback with all the parsing data we need. This example shows how to synchronize multiple promises:

//The tool function used to send the "get" HTTP request,
//And returns promise with the parsed data.
function get(path) {
    return new Promise((resolve, reject) => {
        var request = new XMLHttpRequest();
        
        //When the data is loaded, analyze the promise of JSON data
        request.addEventListener('load', (e) => {
            resolve(JSON.parse(e.target.responseText));
        });

        //When the request fails,
        //Promise was rejected for the right reason.
        request.addEventListener('error', (e) => {
            reject(e.target.statusText || 'unknown error');
        });

        //If the request is aborted, we continue to process the request 
        request.addEventListener('abort', resolve);
        
        request.open('get', path);
        request.send();
    });
}


//Save our requests promises.
var requests = [];

//Issue 5 API requests, and send the corresponding 5
//Promise is placed in the "requests" array.
for (let i = 0; i < 5; i++) {
    requests.push(get('api.json'));
}

//Using "promise. All()" let's pass in an array promises,
//When all promises are completed, a new promise that has been completed is returned.
//Our callback gets an array corresponding to the parsed value of promises.
Promise.all(requests).then((values) => {
    console.log('first', values.map(x => x[0])); 
    console.log('second', values.map(x => x[1]));
});

Cancel promises

So far, the XHR requests we’ve seen in this book have handlers that suspend requests. This is because we can manually abort the request and prevent any load callback function from running. A typical scenario where this functionality is required is when the user clicks the Cancel button, or navigates to other parts of the application, making the request meaningless.

If we are going to go one step further in abstract promise, the same principle applies. The execution of some possible concurrent operations makes promise meaningless. The difference between promises and XHR requests is that they have no abort () method. The last thing we need to do is start introducing possibly unnecessary cancellation logic into our promise callback.

The promise. Race () method is here to help us. As the name implies, this method returns a new promise, which is determined by the first input promise to be parsed. You may not hear much, but it’s not easy to implement the logic of promise. Race(). It is actually a synchronization principle, which hides the concurrency complexity in the application code. Let’s see how this method can help us deal with promise cancelled due to user interaction:

//The parser function used to cancel the data request.
var cancelResolver;

//A simple "constant" value to handle the cancellation of promise
var CANCELED = {};

//Our UI components
var buttonLoad = document.querySelector('button.load'),
    buttonCancel = document.querySelector('button.cancel');

//Request data, return a promise.
function getDataPromise() {
    //Create cancel promise.
    //The "resolve" function passed in by the actuator is "cancelresolver",
    //So it can be called later.
    var cancelPromise = new Promise((resolve) => {
        cancelResolver = resolve;
    });

    //The data we actually want
    //This is usually an HTTP request,
    //But let's use setTimeout () here for a simple simulation.
    var dataPromise = new Promise((resolve) => {
        setTimeout(() => {
            resolve({hello: 'world'});
        }, 3000);
    });

    //The "promise. Race()" method returns a new promise,
    //And no matter what the input promise is, it can complete the processing
    return Promise.race([cancelPromise, dataPromise]);
}

//When we click the Cancel button, we use the
//"Cancelresolver()" function to handle cancel promise
buttonCancel.addEventListener('click', () => {
    cancelResolver(CANCELLED);
});

//When we click the load button, we use the
//"Getdatapromise()" sends a request to get the data.
buttonLoad.addEventListener('click', () => {
    buttonLoad.disabled = true;
    getDataPromise().then((value) => {
        buttonLoad.disabled = false;
        //Promise was executed, but that's because
        //The user cancelled the request. So here we are
        //Exit by returning to cancelled "constant".
        //Otherwise, we have data to use.
        if (Object.is(value, CANCELED)) {
            return value;
        }
        
        console.log('loaded data', value);
    });
});

As an exercise, try to imagine a more complex scenario where datapromise is a promise created by promise. All(). Ours
The cancelresolver() function can cancel many complex asynchronous operations at once.

Promises without actuator

In the final section, we will cover the promise. Resolve () and promise. Reject () methods. We’ve seen earlier in this chapter how promise. Resolve () handles the tenable object. It can also process values or other promises directly. These methods come in handy when we implement a function that can be synchronous or asynchronous. This is not the case where we want to use functions with fuzzy concurrent semantics.

For example, this is a function that may be synchronous or asynchronous, which is confusing and almost certain to cause errors in the future:

//An example function that may return "value" from the cache,
//You can also get values asynchronously through "fetches.".
function getData(value) {
    //If it exists in the cache, we return this value directly
    var index = getData.cache.indexOf(value);
    if(index > -1) {
        return getData.cache[index];
    }

    //Otherwise, we have to get it asynchronously through a "fetch.".
    //This "resolve()" call is usually a callback function that initiates a request on the network
    return new Promise((resolve) => {
        getData.cache.push(value);
        resolve(value);
    });
}

//Create a cache.
getData.cache = [];

console.log('getting foo', getData('foo'));
//→getting foo Promise
console.log('getting bar', getData('bar'));
//→getting bar Promise
console.log('getting foo', getData('foo'));
//→getting foo foo

We can see that the last call returned a cache value, not a promise. This is intuitive because we don’t need to get the final value through promise, we already have it! The problem is that we make any code that uses the getdata() function appear inconsistent. That is, the code that calls GetData () needs to handle concurrency semantics. This code is not concurrent. Let’s change it by introducing promise. Resolve():

//An example function that may return "value" from the cache,
//You can also get values asynchronously through "fetches.".
function getData(value) {
    var cache = getData.cache;
    //If this function does not have a cache,
    //Then refuse promise.
    if(!Array.isArray(cache)) {
        return Promise.reject('missing cache');
    }

    //If it exists in the cache,
    //We directly use the cached value to return the completed promise
    var index = getData.cache.indexOf(value);
    
    if (index > -1) {
        return Promise.resolve(getData.cache[index]);
    }

    //Otherwise, we have to get it asynchronously through a "fetch.".
    //This "resolve()" call is usually a callback function that initiates a request on the network
    return new Promise((resolve) => {
        getData.cache.push(value);
        resolve(value);
    });
}

//Create a cache.
getData.cache = [];

//Every call to "getdata()" returns the same.
//Even when using synchronization values,
//They still return the promise that gets the parsing done.
getData('foo').then((value) => {
    console.log('getting foo', `“${value}”`);
}, (reason) => {
    console.error(reason);
});

getData('bar').then((value) => {
    console.log('getting bar', `“${value}”`);
}, (reason) => {
    console.error(reason);
});

getData('foo').then((value) => {
    console.log('getting foo', `“${value}”`);
}, (reason) => {
    console.error(reason);
});

This is better. By default, any code using getdata() is concurrent using promise. Resolve() and promise. Reject(), even if the data acquisition operation is synchronous.

Summary

This chapter introduces a lot of details about the promise object introduced in ES6 to help JavaScript programmers deal with synchronization problems that have plagued the language for many years. A lot of asynchronous callbacks are used, which leads to callback hell, so we should try to avoid it.

Promise helps us deal with synchronization by implementing a common interface that can handle any value. Promise is always in one of three states – wait, finish, or reject, and they change state only once. When these states change, a callback is triggered. Project has an executor function, which is used to set the resolver function or rejector function of asynchronous operation of project to change the state of project.

Most of the value of promise lies in how they help us simplify complex scenarios. Because it’s not worth using promises if we only have to deal with an asynchronous operation that runs a callback with a parsed value. This is unusual. A common situation is several asynchronous operations, each of which needs to parse the return values; and these values need to be synchronized and transformed. Promises has a way to help us do this, so we can better apply the synchronous concurrency principle to our code.

In the next chapter, we will introduce another newly introduced syntax, generator. Similar to promises, generators are mechanisms that help us apply another principle of concurrency – protection.

At last, we will add the book chapter catalogue

  • Chapter one: Javascript concurrency
  • Chapter 2 JavaScript running model in JavaScript concurrent programming
  • Chapter 3 of JavaScript concurrent programming uses promises to achieve synchronization
  • Chapter 4 of JavaScript concurrent programming uses generators to implement lazy computing
  • Chapter 5 of JavaScript concurrent programming using web workers
  • Chapter 6 practical concurrency in JavaScript concurrent programming
  • Chapter 7 of JavaScript concurrent programming extract concurrent logic

In addition, there are also two chapters on the backend concurrency of nodejs and one chapter on the project practice. No more posts here. If you are interested, you can turn to https://github.com/yzsunli/javascript_concurrency_translation.