Item 64: Use Recursion for Asynchronous Loops
Consider a function that takes an array of URLs and tries to download one at a time until one succeeds. If the API were synchronous, it would be easy to implement with a loop:
function downloadOneSync(urls) { for (var i = 0, n = urls.length; i < n; i++) { try { return downloadSync(urls[i]); } catch (e) { } } throw new Error("all downloads failed"); }
But this approach won’t work for downloadOneAsync, because we can’t suspend a loop and resume it in a callback. If we tried using a loop, it would initiate all of the downloads rather than waiting for one to continue before trying the next:
function downloadOneAsync(urls, onsuccess, onerror) { for (var i = 0, n = urls.length; i < n; i++) { downloadAsync(urls[i], onsuccess, function(error) { // ? }); // loop continues } throw new Error("all downloads failed"); }
So we need to implement something that acts like a loop, but that doesn’t continue executing until we explicitly say so. The solution is to implement the loop as a function, so we can decide when to start each iteration:
function downloadOneAsync(urls, onsuccess, onfailure) { var n = urls.length; function tryNextURL(i) { if (i >= n) { onfailure("all downloads failed"); return; } downloadAsync(urls[i], onsuccess, function() { tryNextURL(i + 1); }); } tryNextURL(0); }
The local tryNextURL function is recursive: Its implementation involves
a call to itself. Now, in typical JavaScript environments, a recursive
function that calls itself synchronously can fail after too many calls
to itself. For example, this simple recursive function tries to call itself
100,000 times, but in most JavaScript environments it fails with a
runtime error:
function countdown(n) { if (n === 0) { return "done"; } else { return countdown(n - 1); } } countdown(100000); // error: maximum call stack size exceeded
So how could the recursive downloadOneAsync be safe if countdown explodes when n is too large? To answer this, let’s take a small detour and unpack the error message provided by countdown.
JavaScript environments usually reserve a fixed amount of space in
memory, known as the call stack, to keep track of what to do next after
returning from function calls. Imagine executing this little program:
function negative(x) { return abs(x) * -1; } function abs(x) { return Math.abs(x); } console.log(negative(42));
At the point in the application where Math.abs is called with the argument 42, there are several other function calls in progress, each waiting for another to return. Figure 7.2 illustrates the call stack at this point. At the point of each function call, the bullet symbol (•) depicts the place in the program where a function call has occurred and where that call will return to when it finishes. Like the traditional stack data structure, this information follows a “last-in, first-out” protocol: The most recent function call that pushes information onto the stack (represented as the bottommost frame of the stack) will be the first to pop back off the stack. When Math.abs finishes, it returns to the abs function, which returns to the negative function, which in turn returns to the outermost script.
When a program is in the middle of too many function calls, it can run
out of stack space, resulting in a thrown exception. This condition is
known as stack overflow. In our example, calling countdown(100000)
requires countdown to call itself 100,000 times, each time pushing
another stack frame, as shown in Figure 7.3. The amount of space
required to store so many stack frames exhausts the space allocated
by most JavaScript environments, leading to a runtime error.
Now take another look at downloadOneAsync. Unlike countdown, which
can’t return until the recursive call returns, downloadOneAsync only
calls itself from within an asynchronous callback. Remember that
asynchronous APIs return immediately—before their callbacks are
invoked. So downloadOneAsync returns, causing its stack frame to be
popped off of the call stack, before any recursive call causes a new
stack frame to be pushed back on the stack. (In fact, the callback is
always invoked in a separate turn of the event loop, and each turn of
the event loop invokes its event handler with the call stack initially
empty.) So downloadOneAsync never starts eating up call stack space, no matter how many iterations it requires.
Things to Remember
? Loops cannot be asynchronous.
? Use recursive functions to perform iterations in separate turns of the event loop.
? Recursion performed in separate turns of the event loop does not overflow the call stack.
文章来源于:Effective+Javascript编写高质量JavaScript代码的68个有效方法 英文版