[Effective JavaScript 笔记]第64条:对异步循环使用递归

假设需要有这样一个函数,接收一个URL的数组并尝试依次下载每个文件直到有一个文件被成功下载。如果API是同步的,使用循环很简单实现。

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.');
}

在异步情况下,上面的这种方式就无法正确工作。因为不能在回调函数中暂停循环并恢复。如果尝试使用循环,它将启动所有的下载,这不是等待完成一个再进行下一个。

function downloadOneAsync(urls,onsucess,onerror){
    for(var i=0,n=urls.length;i < n;i++){
        downloadAsync(urls[i],onsucess,function(error){
            //?
        });
        //loop continues
    }
    throw new Error('all downloads failed');
}

这里我们要实现一个类似循环的东西,我们需要显式地说继续执行,它才会继续执行。解决方案是将循环实现为一个函数,可以决定何时开始每次迭代。

function downloadOneAsync(urls,onsucess,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);
}

局部函数tryNextURL是一个递归函数。它的实现调用了其自身。典型的javascript环境中一个递归函数同步调用自身过多次会导致失败。例如,下例中的递归函数试图调用自身10万次,在大多数的js环境中会产生一个运行时错误。

function countdown(n){
    if(n===0){
        return 'done';
    } else {
        return countdown(n-1);
    }
}

当n太大时countdown函数会执行失败,那么如何确保downloadOneAsync函数是安全的呢?查看一下countdown函数提供的错误信息。

VM58:1 Uncaught RangeError: Maximum call stack size exceeded(…)

js环境通常在内存中保存一块固定的区域,称为调用栈,用于记录函数调用返回前下一步该做什么。执行下面的小程序。

function negative(x){
    return abs(x)*-1;
}
function abs(x){
    return Math.abs(x);
}
console.log(negative(42));

当程序使用参数42调用Math.abs方法时,有几个其他的函数调用也在进行,每个都在等待另一个的调用返回。在每个函数调用时,项目符号(.)描述了在程序中已经发生的函数调用地方及这次调用完成后将返回哪里。就像传统的栈数据结构,这个信息遵循“先进后出”协议。最新的函数调用将信息推入栈(被表示为栈的最底层的帧),该信息也将首先从栈中弹出。当Math.abs执行完毕,将会返回给abs函数,其将返回给negative函数,然后将返回到最外面的脚本。
当一个程序执行中有太多的函数调用,它会耗尽栈空间,最终抛出异常。这种情况被称为栈溢出。在此例中,调用countdown(10万次)需要countdown调用自身10万次,每次推入一个栈桢。存储这么多栈帧需要的空间量会耗尽大多数js环境分配空间,导致运行时错误。

现在再看看downloadOneAsync函数。不像countdown直到递归调用返回后才会返回,downloadOneAsync只在异步回调函数中调用自身。记住异步API在其回调函数被调用前会立即返回。所以downloadOneAsync返回,导致其栈帧在任何递归调用将新的栈帧推入栈前,会从调用栈中弹出。(事实上,回调函数总在事件循环的单独轮次中被调用,事件循环的每个轮次中调用其他事件处理程序的调用栈最初是空的。)所以无论downloadOneAsync需要多少次迭代,都不会耗尽栈空间。

提示

  • 循环不能是异步的

  • 使用递归函数在事件循环的单独轮次中执行迭代

  • 在事件循环的单独轮次中执行递归,并不会导致调用栈溢出

posted @ 2016-07-27 14:27  脚后跟着猫  阅读(1369)  评论(0编辑  收藏  举报
返回
顶部