如何加速你的 JavaScript:优化循环、递归与异步操作

在上一篇文章中,我们讨论了JavaScript执行缓慢的第一个原因:循环中有太多操作。而这次我们要探讨的是函数执行过慢的问题。通常这意味着函数中存在过多的循环、递归过多,或者是函数中执行了太多的不同操作。

1. 嵌套循环过多

一个常见的性能瓶颈是循环内部又嵌套了循环,这会让JavaScript引擎卡住,直到所有的迭代完成。例如冒泡排序就是一个典型的例子。尽管JavaScript已经有了原生的sort()方法,但理解冒泡排序的低效是很有必要的,因为你可以从中识别出类似的模式。下面是一个典型的JavaScript冒泡排序实现:

javascript 复制代码
function bubbleSort(items) {
    for (var i = items.length - 1; i >= 0; i--) {
        for (var j = items.length - i; j >= 0; j--) {
            if (items[j] < items[j - 1]) {
                var temp = items[j];
                items[j] = items[j - 1];
                items[j - 1] = temp;
            }
        }
    }
}

冒泡排序的主要问题是,对于数组中的每个n项,需要进行n²次循环。这对于大数组来说处理起来非常缓慢,因为每次内部循环都要执行比较和交换操作,尽管这些操作本身很简单,但过多的重复会导致浏览器变得迟缓,甚至触发"长时间运行脚本"的警告。

为了解决这个问题,可以参考Yahoo工程师Julien Lecomte的建议,他提出将大型JavaScript操作分解成多个部分执行。他的改进版冒泡排序算法,每次只遍历数组的一部分,然后异步执行下一步:

javascript 复制代码
function bubbleSort(array, onComplete) {
    var pos = 0;

    (function() {
        var j, value;
        for (j = array.length; j > pos; j--) {
            if (array[j] < array[j - 1]) {
                value = array[j];
                array[j] = array[j - 1];
                array[j - 1] = value;
            }
        }
        pos++;
        if (pos < array.length) {
            setTimeout(arguments.callee, 10);
        } else {
            onComplete();
        }
    })();
}

这种方法将冒泡排序的每次循环分解成单独的步骤,并使用setTimeout()异步执行,确保每次操作后让浏览器有时间处理其他任务,从而避免锁死浏览器。完成排序后,通过onComplete()函数通知数组已经排序完毕。

2. 递归过多

过多的递归调用会导致内存耗尽,并可能在弹出长时间运行脚本警告前,就使浏览器变得无法使用。Douglas Crockford曾在一次演讲中用生成斐波那契数列的例子讨论了这一问题:

javascript 复制代码
function fibonacci(n) {
    return n < 2 ? n :
        fibonacci(n - 1) + fibonacci(n - 2);
}

调用fibonacci(40)会导致函数自我调用331,160,280次,显然效率非常低。解决递归问题的一种方法是使用记忆化技术,将计算过的结果缓存起来,以避免重复计算。Crockford引入了一个通用的记忆化函数:

javascript 复制代码
function memoizer(memo, fundamental) {
    var shell = function(n) {
        var result = memo[n];
        if (typeof result !== 'number') {
            result = fundamental(shell, n);
            memo[n] = result;
        }
        return result;
    };
    return shell;
}

然后,将其应用到斐波那契数列生成器中:

javascript 复制代码
var fibonacci = memoizer([0, 1], function(recur, n) {
    return recur(n - 1) + recur(n - 2);
});

这样,调用fibonacci(40)只会执行40次,极大提升了性能。记忆化的核心思想是:不要重复计算相同的结果。如果某个值会被多次使用,那么将其缓存起来,而不是每次都重新计算。

3. 过多操作

另一个常见的性能问题是函数执行了太多不相关的操作。例如:

javascript 复制代码
function doAlot() {
    doSomething();
    doSomethingElse();
    doOneMoreThing();
}

这些操作彼此独立,只需要按照顺序执行,而并不需要等待其他操作完成。为了避免阻塞浏览器,可以通过分块执行这些函数,使用类似如下的调度函数:

javascript 复制代码
function schedule(functions, context) {
    setTimeout(function() {
        var process = functions.shift();
        process.call(context);

        if (functions.length > 0) {
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}

schedule函数接受两个参数:函数队列和上下文对象(即this的值)。函数队列中的每个函数按顺序被执行。可以这样调用:

javascript 复制代码
schedule([doSomething, doSomethingElse, doOneMoreThing], window);

类似的功能已经出现在一些库中,例如YUI 3.0中的Queue对象,它可以帮助管理多个函数按顺序执行。

总结

无论有多少工具帮助分解复杂的过程,开发者仍需具备识别瓶颈的能力。无论是循环过多、递归过多,还是操作过多,都可以通过本文提到的技术来优化。但请记住,这些技术仅仅是起点,实际应用中需要根据场景对代码进行调整。

参考文章: Zakas, Nicholas C.《Speed up your JavaScript, Part 2》

相关推荐
前端郭德纲7 分钟前
深入浅出ES6 Promise
前端·javascript·es6
就爱敲代码12 分钟前
ES6 运算符的扩展
前端·ecmascript·es6
王哲晓33 分钟前
第六章 Vue计算属性之computed
前端·javascript·vue.js
究极无敌暴龙战神X39 分钟前
CSS复习2
前端·javascript·css
风清扬_jd1 小时前
Chromium HTML5 新的 Input 类型week对应c++
前端·c++·html5
Ellie陈1 小时前
Java已死,大模型才是未来?
java·开发语言·前端·后端·python
想做白天梦2 小时前
双向链表(数据结构与算法)
java·前端·算法
有梦想的咕噜2 小时前
Electron 是一个用于构建跨平台桌面应用程序的开源框架
前端·javascript·electron
yqcoder2 小时前
electron 监听窗口高端变化
前端·javascript·vue.js
Python私教2 小时前
Flutter主题最佳实践
前端·javascript·flutter