在上一篇文章中,我们讨论了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》