Quite possibley the most useful feature of promises is nesting. Nesting refers to the fact that when a thenFunction (after another Promise) returns a Promise itself, the Promise chain waits until that returned Promise is furfilled. Let us examine the following code.
// Nesting Snippet #1 new Promise(function apple(accept) { accept("foo string example"); }) .then(function bannana(foo){ // foo = "foo string example" // The variable returnPromiseVar is not neccecary. It is only // for the purposes of demonstration. It would work equally // well just to do return new Promise(/*...*/); var returnPromiseVar = new Promise(function coconut(accept){ accept( "bar string example" ); }); return returnPromise; }) .then(function date(bar){ // bar = "bar string example" /*...*/ });
The way Promise chaining works is that the value of bar passed to the function date will be the return value of function bannana unless the function bannana returns a Promise (which in this case it is a Promise). For purely demonstrational purposes, the Promise returned by bannana is assigned to the variable returnPromiseVar. Now, since Bannana returns the promise returnPromiseVar, the browser waits until returnPromiseVar is accepted or rejected before executing date with the resolved value of the Promise returnPromiseVar. An easy way to think of this is imagining the browser secretly doing returnPromiseVar.then(function date(){/*...*/})
behind the scenes. All three of the below samples do the exact same thing as "Nesting Snippet #1."
new Promise(function apple(accept) { accept("foo string example"); }) .then(function bannana(foo){ // foo = "foo string example" var returnPromiseVar = new Promise(function coconut(accept){ accept( "bar string example" ); }).then(function date(bar){ // bar = "bar string example" /*...*/ }); return returnPromise; });
new Promise(function apple(accept) { accept("foo string example"); }) .then(function bannana(foo){ // foo = "foo string example" return new Promise(function coconut(accept){ accept( "bar string example" ); }).then(function date(bar){ // bar = "bar string example" /*...*/ }); });
new Promise(function apple(accept) { accept("foo string example"); }) .then(function bannana(foo){ // foo = "foo string example" return new Promise(function coconut(accept){ accept( (function date(bar){ // bar = "bar string example" /*...*/ })("bar string example") ); }); });
If you are still lost, then no need to worry. To better visualize nesting, try running the below code in the console.
new Promise( accept=>{ console.log("Checkpoint #1 @ ", (performance.now()/1000).toFixed(1)+"s"); setTimeout(accept, 100, "uno"); } ).then( first_result=>{ console.log("1st: ",first_result, " @ ", (performance.now()/1000).toFixed(1)+"s"); return new Promise( accept=>{ console.log("Checkpoint #2 @ ", (performance.now()/1000).toFixed(1)+"s"); setTimeout(accept, 100, "dos"); } ).then( second_result=>{ console.log("2nd: ", second_result, " @ ", (performance.now()/1000).toFixed(1)+"s"); return "tres"; } ) } ).then( third_result=>console.log("3rd: ", third_result, " @ ", (performance.now()/1000).toFixed(1)+"s") )
However, there is one major problem with Promises: memory. Memory can become a serious issue if you do not know what you are doing because of the ease in creating long-life objects. Examine the below example of the Fetch API.
fetch("https://www.example.com/index.html") .then(/*firstFunc*/ response => response.text()); .then(/*secondFunc*/ textContent => { console.log('"Length: ' + textContent.length + '"'); });
Or, if you are unfamiliar with arrow functions:
fetch("https://www.example.com/index.html") .then(function firstFunc(response){ return response.text(); }); .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); });
The above code style is pretty standard, so it must be pretty efficient, right? Think again. Observe the below persuado-visualization steps of the memory timeline. The number in curly-brackets at the start of each step represents the number of persuado-objects in memory before the step has taken place, and the number in curly brackets at the end of each step represents the number of persuado objects after the step has taken place. Please note that this is only a persuado-exemplification of specific parts of the memory. In reality, there is ALOT more going on behind the scenes.
response.text()
. We will call this promise HTTPtextPromise because it resolves to the text content of the page at the requested URL. {2}The key to efficeint memory management is minimizing the memory taken up during times of wait so as to allow other programs running on the computer to have the memory they need. For instance, if your javascript code has many promises, each holding massive chunks of memory at bay, then the these promises will likely overlap eachother (multiple of the promises, each holding its own ton of memory), resulting in incredibley high peak memory usage that can cause Out-Of-Memory page crashes. If these promises were to keep these big objects for shorter periods of time, then the times big objects are held in memory would overlap less often, resulting in a lower peak-memory usage and less frequent Out-Of-Memory crashes. In step #9, there are 2 units of memory being taken up. But, let us examine if we put secondFunc inside firstFunc.
fetch("https://www.example.com/index.html") .then(function firstFunc(response){ response.text() .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); }); });
response.text()
. We will call this promise HTTPtextPromise because it resolves to the text content of the page at the requested URL. {1}At step 5, there is 1 less unit of memory being taken up while the headers are being downloaded, making this a more efficient solution. And, yet, we can still greatly optimize this further:
fetch("https://www.example.com/index.html") .then(function firstFunc(){ arguments[0].text() .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); }); });
This will reduce the memory usage further because, in theory, its possible to sneak an evil eval statement into there, and access the local variable response from secondFunc, thus keeping the response variable in memory until secondFunc is garbadge collected. For example, take the following code snippet.
var console = { get log(){ var evaluate = Object.getOwnPropertyNames(window).filter(x=>x[0]==="e"&&x[2]==="a")[0]; return Math.random() ? window[evaluate] : function(){}; } }; fetch("https://www.example.com/index.html") .then(function firstFunc(response){ response.text() .then(function secondFunc(textContent){ console.log('"Length: ' + textContent.length + '"'); }); response = null; });
In theory, the way that eval is accessed could be made infinitely complex, thus forcing browser designers to do silly clingy things to memory because there is no way to 100% predict when a random eval will be quietly injected. However, let us not forget that this optimized code uses only only 1 variable and 1 memory unit less, not millions. Being over obsessive about tiny optimizations like this will not "make-or-break" your javascript code. Rather, what will "make-or-break" your javascript code is just staying mindful of memory management and eliminating major memory problems. Below is an example of such a major memory problem. Assume that only one HTTP request will be made that requires the variable bigArray.
var bigArray = [], fromCodePoint = String.fromCodePoint; for (var i=0, Len=0x10ffff; i !== Len; i++) { bigArray[i] = fromCodePoint(i); } fetch("https://www.example.com/index.html") .then(function firstFunc(){ arguments[0].text() .then(function secondFunc(textContent){ var cpoints = Array.from(textContent).map(char => bigArray[char]); console.log("Codepoints: ", cpoints); }); });
During the entire HTTP request, bigArray is hanging around in memory, waiting to be used so it can be garbadge collected. Let us see if we can change that.
fetch("https://www.example.com/index.html") .then(function firstFunc(){ arguments[0].text() .then(function secondFunc(textContent){ var bigArray = [], fromCodePoint = String.fromCodePoint; for (var i=0, Len=0x10ffff; i !== Len; i++) { bigArray[i] = fromCodePoint(i); } var cpoints = Array.from(textContent).map(char => bigArray[char]); console.log("Codepoints: ", cpoints); }); });
Much better. However, while optimizing the Promise calls, a much more fundamental optimization was skipped over: sometimes, it isn't even neccecary to use massive amounts of memory in the first place.
fetch("https://www.example.com/index.html") .then(function firstFunc(){ arguments[0].text() .then(function secondFunc(textContent){ var cpoints = [], cur=0; for (var i=0, Len=textContent.length; i < Len; ) { cur = textContent.codePointAt(0); cpoints.push(cur); i += 1 + (cur>0xffff); } console.log("Codepoints: ", cpoints); }); });
There, now we have eliminated sinister polymorphism and the nasty bigArray variable, making the code snippet several times faster. Sometimes, its easy to overthink certain optimizations while underthinking others. At any rate, it is always best to be knowledgable about the code you are writing.