Imagine a variant of the Downloadasync function that holds a cache (implemented as a dict) to avoid downloading the same file multiple times. Immediately invoking a callback function is an optimal choice if the file is already cached.
var cache=new Dict();function downloadCachingAsync(url,onsuccess,onerror){ if(cache.has(url)){ onsuccess(cache.get(url)); return; } return downloadAsync(url,function(file){ cache.set(url,file); onsuccess(file); },onerror);}
Typically, it provides data immediately, but this is in violation of the expectations of the asynchronous API client. First, it changes the expected order of operations. The 62nd article shows the following example, for a routine asynchronous API should always record log messages in a predictable order.
downloadAsync(‘file.txt‘,function(file){ console.log(‘finished‘);});console.log(‘starting‘);
Using the Downloadcachingasync implementation above, such client code may eventually log events in any order, depending on whether the file has been cached.
downloadCachingAsync(‘file.txt‘,function(file){ console.log(‘finished‘);});console.log(‘starting‘);
The order of the log messages is one thing. More generally, the purpose of the asynchronous API is to maintain strict separation of each round in the event loop. As explained in article 61st, this simplifies concurrency by reducing the amount of code per round of event loops without worrying about other code concurrently modifying the shared data structure. A synchronous call to an asynchronous callback function violates this separation, causing the code to perform a round of isolated event loops before the current wheel completes.
For example, an application might hold a remaining file queue to download and display messages to the user.
downloadCachingAsync(remaining[0],function(file){ remaining.shift();});status.display(‘Downloading ‘+remaining[0]+‘...‘);
If the callback function is called synchronously, a message with the wrong file name is displayed (or worse, "undefined" is displayed if the queue is empty).
Synchronous calls to asynchronous callback functions can even cause some subtle problems. The 64th article explains that the asynchronous callback function is essentially called with an empty call stack, so it is safe to implement the asynchronous loop as a recursive function without accumulating the risk of exceeding the call stack space at all. Synchronous calls do not guarantee this, making it possible for an asynchronous loop on a surface to run out of call stack space. Another problem is the exception. For the above Downloadcachingasync implementation, if the callback function throws an exception, it will throw the exception in each round of the event loop, which is the start of the download and not the expected split round.
To ensure that callback functions are always called asynchronously, we can use an existing asynchronous API. As we did in 65th and 66th, we use the Universal library function settimeout to add a callback function to the event queue after every minimum time-out. There may be a more perfect alternative to scheduling immediate events than the SetTimeout function, depending on the specific platform.
var cache=new Dict();function downloadCachingAsync(url,onsuccess,onerror){ if(cache.has(url)){ var cache=cache.get(url); setTimeout(onsuccess.bind(null,cached),0); return; } return downloadAsync(url,function(file){ cache.set(url,file); onsuccess(file); },onerror);}
This uses the BIND function to save the result as the first parameter of the onsuccess callback function. Tips
Never call an asynchronous callback function synchronously, even if you can get the data immediately
A synchronous call to an asynchronous callback function disrupts the expected sequence of operations and may lead to unexpected interleaved code
Calling asynchronous callback functions synchronously can result in stack overflow or incorrectly handling exceptions
Use asynchronous APIs, such as the settimeout function, to dispatch asynchronous callback functions to run in another round
[Effective JavaScript notes] 67th: Never invoke asynchronous callback functions synchronously