About promises (promises) is already written a lot. This article is just an attempt to collect the most practical use of promises with sufficiently detailed explanations of how this works.
First a few definitions.
Promises (promises) are objects that allow you to streamline the execution of asynchronous calls.
An asynchronous call is a function call in which the execution of the main code stream does not wait for the call to complete. For example, the execution of the http request does not interrupt the execution of the main thread. That is, the request is executed, and immediately, without waiting for its completion, the code following this call is executed, and the result of the http request is processed after its completion by the callback function (callback function).
Next, we will consistently deal with the functioning of promises. For the time being, we will proceed from the fact that we already have an object that promises to perform a certain asynchronous call. We will talk about where promises come from and how to form them yourself.
1. Promises provide a mechanism for managing the sequence of asynchronous calls. In other words, a promise is just a wrapper over an asynchronous call. The promise may resolve successfully or fail. The essence of the mechanism is as follows. Each promise provides two functions: then () and catch (). As an argument, a callback function is passed to each of the functions. The callback function in then is called if the promise is successfully resolved, and in catch if it is an error:
promise.then(function(result){ // }).catch(function(error){ // });
2. The next point to understand. A promise is allowed only once. In other words, an asynchronous call that occurs within a promise is executed only once. In the future, the promise simply returns the saved result of the asynchronous call, no longer fulfilling it. For example, if we have some promise promise, then:
// promise.then(function(result){ // , // }); // promise.then(function(result){ // . // // });
3. The then () and catch () functions return promises. Thus, you can build a chain of promises. The result, which the callback function returns in then (), is passed as an argument to the input of the next callback function (). And the throw (message) call argument is passed as an argument to the callback function in catch ():
promise.then(function(result){ // ( ) return result1; // }).then(function(result){ // result result1, // if(condition){ throw("Error message"); } }).catch(function(error){ // , // , // error "Error message" });
4. As a result, the callback function in then () can return not only a value, but also another promise. In this case, the next call to then () will be passed the result of the resolution of this promise:
promise1.then(function(result1){ return promise2.then(function(result2){ // - return result3; }); }).then(function(result){ // result result3 }).catch(function(error){ // });
5. You can do the same by building the resolution of the promises in one linear chain without investments:
promise1.then(function(result1){ return promise2; }).then(function(result2){ // - return result3; }).then(function(result){ // result result3 }).catch(function(error){ // });
Notice that each promise chain ends with a call to catch (). If this is not done, then the errors that occur will disappear in the depths of the promises, and then it will be impossible to determine where the error occurred.
If it is necessary to make a sequence of asynchronous calls (that is, when each next call must be executed strictly after the previous one is completed), then these asynchronous calls must be built into a chain of promises.
Promises can be obtained in two ways: either the functions of the library used can return ready-made promises, or you can independently wrap promises asynchronous calls to functions that do not support promises (promisification). Consider each option separately.
Many modern libraries support work with promises. You can find out whether the library supports the promise or not from the documentation. Some libraries support both asynchronous call handling options: callback functions and promises. Consider a few examples from life.
The documentation provides the following example of working with library functions (for example, data sampling):
Model.find({id: [1,2,3]}).exec(function(error, data){ if(error){ // } else{ // , data } });
It seems that everything is beautiful. However, if after data sampling and processing it is necessary to do some more data operations, for example, updating records in one table, then inserting new records in another table, then the nesting of callback functions becomes such that readability of the code begins to strive to zero:
Model.find({id: [1,2,3]}).exec(function(error, data){ if(error){ // } else{ // , data Model_1.update(...).exec(function(error, data){ if(error){/* */} else{ // Model_2.insert(...).exec(function(error, data){ // .. }); } }); } });
Even in the absence of the main code in the above example, it is already very difficult to understand what is written. And if the database queries still need to be executed in a loop, then when using callback functions, the task in general becomes intractable.
However, the functions of the Waterline ORM library are able to work with promises, although this is mentioned in the documentation somehow casually. But the use of promises greatly simplifies life. It turns out that database query functions return promises. This means that the last example in the language of promises can be written as:
Model.find(...).then(function(data){ // , data return Model_1.update(...); }).then(function(data){ // return Model_2.insert(...); }).then(function(data){ // . . }).catch(function(error){ // });
I think that there is no need to say which solution is better. Particularly pleased that the processing of all errors is now done in one place. Although it cannot be said that it is always good. Sometimes it happens that it is not clear from the context of the error in which particular block it occurred, respectively, debugging becomes more complicated. In this case, no one forbids putting a catch () call into each promise:
Model.find(...).then(function(data){ // , data return Model_1.update(...).catch(function(error){...}); }).then(function(data){ // return Model_2.insert(...).catch(function(error){...}); }).then(function(data){ // . . }).catch(function(error){ ... });
Even in this case, the code is more comprehensible than when using callback functions.
As in the previous case, examples from the documentation use callback functions:
MongoClient.connect(url, function(err, db) { assert.equal(null, err); console.log("Connected succesfully to server"); db.close(); });
We have already seen in the previous example that using callbacks makes life very difficult when you need to make many consecutive asynchronous calls. Fortunately, with a careful study of the documentation, you can find out that if the function of this library does not pass a callback function as an argument, it will return the promise. So, the previous example can be written as:
MongoClient.connect(url).then(function(db){ console.log("Connected succesfully to server"); db.close(); }).catch(function(err){ // });
The peculiarity of working with this library is that the database descriptor object db returned from an asynchronous call to connect () is used to make any database request. That is, if in the course of our work we need to perform many queries to the database, then it becomes necessary to receive this descriptor every time. And here the use of promises beautifully solves this problem. To do this, simply save the promise of connecting to the database in a variable:
var dbConnect = MongoClient.connect(url); // // dbConnect.then(function(db){ return db.collection('collection_name').find(...); }).then(function(data){ // // db , // return dbConnect.then(function(db){ return db.collection('collection_name').update(...); }); }).then(function(result){ // update // .. ... }).catch(function(error){ // }); // // , // dbConnect.then(function(db){ ... }).catch(function(error){ ... });
What is good with such an organization of code is that the connection to the database is established once (I remind you that the promise is resolved only once, then the saved result is simply returned). However, in the example above, there is one problem. After completion of all requests to the database, we need to close the connection to the database. Closing the connection to the base could be inserted at the end of the promise chain. However, we do not know which of the two chains we started will end earlier, which means it is not clear which of the two chains should insert the closure of the connection with the base. The Promise.all () call helps to solve this problem. We will talk about it a bit later.
Also, for the time being, we will postpone the discussion of how using promises to organize the cyclical execution of asynchronous calls. After all, as mentioned above, the use of callbacks does not allow to solve this problem in general.
We now turn to the question of how to independently create promises if the library does not support them.
It often happens that the library does not keep promises. And offers to use only callbacks. In the case of a small script and simple logic, you can be content with this. But if the logic is complicated, then promises cannot be done. So, you need to be able to wrap asynchronous calls into promises.
As an example, consider wrapping the promise of an asynchronous function to retrieve an attribute value from the redis repository. The redis library functions do not keep promises and only work with callback functions. That is, the standard call of the asynchronous function will look like this:
var redis = require('redis').createClient(); redis.get('attr_name', function(error, reply){ if(error){ // } else{ // } });
Now we wrap this feature into a promise. JavaScript provides a Promise object for this. That is, to create a new promise you need to do this:
new Promise(function(resolve, reject){ // });
As an argument, the Promise constructor is passed a function, within which an asynchronous call occurs. The arguments of this function are the function resolve and reject. The resolve function must be called if the asynchronous call completes successfully. The result of the asynchronous call is transferred to the function input resolve. The reject function should be called in case of an error. An error is passed to the reject function.
Putting it all together we get the function get (attr), which returns a promise to get a parameter from the redis repository:
var redis = require('redis').createClient(); function get(attr){ return new Promise(function(resolve, reject){ redis.get(attr, function(error, reply){ if(error){ reject(error); } else{ resolve(reply); } }); }); }
That's all. Now you can safely use our get () function in the usual way for promises:
get('attr_name').then(function(reply){ // }).catch(function(error){ // });
Any sane person will definitely have this question. What are we doing now? We clarified how to sequentially execute (asynchronous) function calls. In any "normal" (not working with asynchronous calls) programming language, this is done simply by sequentially writing instructions. No promises are needed. And there is so much torment in order to make a simple sequence of calls. Maybe asynchronous calls are not needed at all? After all, because of them so many problems!
But every cloud has a silver lining. With asynchronous calls, we can choose how to make calls: sequentially or in parallel. That is, if one action requires for its execution the presence of the result of another action, then we use then () calls in promises. If the execution of several actions does not depend on each other, then we can run these actions on parallel execution.
How to start parallel execution of several independent actions? To do this, use the call:
Promise.all([ promise_1, promise_2, ..., promise_n ]).then(function(results){ // - });
That is, the promise array is passed to the Promise.all function. In this case, all promises are executed immediately. The Promise.all function returns a promise to fulfill all promises, so the then () call in this case is triggered after all promises of the array are fulfilled. The results parameter, which is passed to the input of the function in then (), is an array of the results of the promises in the sequence in which they were passed to the Promise.all input.
Of course, you can promise and not wrap in Promise.all, but simply sequentially launch it. However, in this case we lose control over the moment of completion of the fulfillment of all promises.
Often there is the task of consistently fulfilling promises in a cycle. Here we will consider the sequential fulfillment of promises, since if promises allow parallel execution, then there is nothing easier to make in the loop (or using map, filter functions, etc.) an array of promises and pass it to the input Promise.all.
How does the cycle consistently fulfill promises? The answer is simple - no way. That is, strictly speaking, promises in a cycle cannot be fulfilled, but in a cycle you can make a chain of promises that are fulfilled one after the other, which is essentially equivalent to the cyclical execution of asynchronous actions.
Let us now consider the mechanisms for cyclically constructing chains of promises. Let us build on the types of cycles familiar to all programmers: the for loop and the forEach loop.
Suppose that a certain function doSomething () returns a promise to perform some kind of asynchronous action, and we need to perform this action successively n times. Let the promise itself return a certain string result, result, which is used in the next asynchronous call. Also assume that the first call is made with an empty string. In this case, building a chain of promises is done like this:
// var actionsChain = Promise.resolve(""); // for(var i=0; i<n; i++){ actionsChain = actionsChain.then(function(result){ return doSomething(result); }); } // , // actionsChain.then(function(result){ // , }).catch(function(error){ // });
In this sample template, only the first line requires an explanation. The rest should be clear from the above material. The Promise.resolve () function returns a successfully resolved promise whose result is the argument of this function. That is, Promise.resolve ("") is equivalent to the following:
new Promise(function(resolve, reject){ resolve(""); });
Suppose we need to perform some asynchronous action for each element of the array array. Let this asynchronous action be wrapped in a certain function doSomething (), which returns a promise to perform this asynchronous action. The pattern of how, in this case, to build a chain of promises, oddly enough, is not based on the use of the function forEach, but on the use of the function reduce.
First, let's look at how the reduce function works. This function is designed to process the elements of an array while maintaining an intermediate result. Thus, in the end, we can get some integral result for a given array. For example, using the reduce function, you can calculate the sum of the elements of an array:
var sum = array.reduce(function(sum, current){ return sum+current; }, 0);
The reduce function arguments are:
The function that is called for each element of the array. The following arguments are passed as arguments to this function: the result of the previous call, the current element of the array, the index of this element, and the array itself (in the example above, the last two arguments are omitted due to the lack of necessity).
The initial value that is passed as the result of the previous action when processing the first element of the array.
Now let's see how to use the reduce function to build a chain of promises for the task:
array.reduce(function(actionsChain, value){ return actionsChain.then(function(){ return doSomething(value); }); }, Promise.resolve());
In this example, the actions of the previous action is the promise of actionsChain, after the resolution of which a new promise is created to execute the doSomething action, which in turn is returned as a result. So the chain is built for each element of the array. Promise.resolve () is used as the initial promise of actionsChain.
. : GET . , , , () .
:
// HTTP- var server = http.createServer(processRequest); // redis server.on('close', function(){ storage.quit(); }); // server.listen(config.port, config.host); console.log('Service is listening '+config.host+':'+config.port);
http-, node.js. processRequest — http-. redis. redis , . redis, . : processRequest. requestQueue. :
var requestQueue = Promise.resolve(); /** * * * @param {Object} req * @param {Object} res */ function processRequest(req, res){ requestQueue = requestQueue .then(function(){ // GET-, ... if(req.method == 'GET'){ // UUID return calcUUID() // .then(function(uuid){ res.writeHead(200, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-cache' }); res.end(uuid); }) // .catch(function(error){ console.log(error); res.writeHead(500, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-cache' }); res.end(error); }); } // GET-, Bad Request else{ res.statusCode = 400; res.statusMessage = 'Bad Request'; res.end(); } }); }
calcUUID(), . , . . , , .
, , . .
, . - .
Source: https://habr.com/ru/post/311804/
All Articles