如何提高JavaScript的运行速度(函数篇)

这篇是Nicholas讨论若是防止脚本失控的第二篇,主要讨论了如何重构嵌套循环、递归,以及那些在函数内部同时执行不少子操做的函数。基本的思 想和上一节trunk()那个例子一致,若是几个操做没有特定的执行顺序,并且互相不是依赖关系,咱们就能够经过异步调用的方式加以执行,不止能够减小执 行的次数,还能够防止脚本失控。本文还介绍了经过memoization技术取代递归的方法。javascript

【原文标题】Speed up your JavaScript, Part 2
【原文做者】Nicholas C. Zakas

如下是对原文的翻译:

上周我在《too much happening in a loop》(译文:如何提高JavaScript的运行速度(循环篇))这篇文章中介绍了JavaScript运行时间过长的第一个缘由。类似的状况有时也出如今函数的定义上,函数也可能由于使用不当而过载使用。一般状况是函数内包含了过多的循环(不是在循环中执行了过多的内容),太多的递归,或者只不过是太多不相干但又要一块儿执行的操做。

太 多的循环常常是以嵌套的形式出现,这种代码会一直占用JavaScript引擎直至循环结束。这方面有一个很是著名的例子,就是使用冒泡算法排序。因为 JavaScript有内置的sort()方法,咱们没有必要使用这种方式进行排序,但咱们能够借助这个算法理解嵌套循环占用资源的症结所在,从而避免类 似状况的发生。下面是一个在JavaScript使用冒泡排序法的典型例子:
html

function bubbleSort(items) {
for (var i = items.length - 1; i >= 0; i--) {
  for (var j = 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写了一篇题为《Running CPU Intensive JavaScript Computations in a Web Browser》的文章,在这篇文章中做者阐述了如何将很大的javaScript操做分解成若干小部分。其中一个例子就是将冒泡排序法分解成多个步骤,每一个步骤只遍历一次数组。我对他的代码作了改进,但方法的思路仍是同样的:
java

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 = data[j];
          data[j] = data[j - 1];
          data[j - 1] = value;
      }
  }
  pos++;
  if (pos < array.length) {
      setTimeout(arguments.callee, 10);
  } else {
      onComplete();
  }
})();
}

这个函数借助一个异步管理器来实现了冒泡算法,在每次遍历数组之前暂停一下。onComplete()函数会在数组排序完成后触发,提示用户数据已经准备 好。bubbleSort()函数使用了和chunk()函数同样的基本技术(参考个人上一篇帖子),将行为包装在一个匿名函数中,将 arguments.callee传递给setTimeout()以达到重复操做的目的,直至排序完成。若是你要将嵌套的循环拆解成若干个小步骤,以达到 解放浏览器的目的,这个函数提供了不错的指导意见。

类似的问题还包括过多的递归。每一个额外的递归调用都会占用更多的内存,从而减慢浏览器的运行。恼人的是,你可能在浏览器发出脚本失控警告以前,就耗尽了系统的内存,致使浏览器处于中止响应的状态。Crockford在博客上曾经对这个问题进行过深刻的讨论。他当时使用的例子,就是用递归生成一个斐波那契数列。
web

function fibonacci(n) {
return n < 2 ? n: fibonacci(n - 1) + fibonacci(n - 2);
};

按照Crockford的说法,执行fibonacci(40)这条语句将重复调用自身331160280次。避免使用递归的方案之一就是使用memoization技术,这项技术能够获取上一次调用的执行结果。Crockford介绍了下面这个函数,能够为处理数值的函数增长这项功能:
算法

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;
};

他接下来将这个函数应用在斐波那契数列生成器上:
shell

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

这时若是咱们再次调用fibonacci(40),只会重复调用40次,和原来相比提升得很是多。memoization的原理,归纳起来就一句话,一样的结果,你没有必要计算两次。若是一个结果你可能会再次使用,把这个结果保存起来,总比从新计算一次来的快。

最后一个可能让函数执行缓慢的缘由,就是咱们以前提到过的,函数里面执行了太多的内容,一般是由于使用了相似下面的开发模式:
数组

function doAlot() {
 doSomething();
 doSomethingElse();
 doOneMoreThing();
}

在这里要执行三个不一样的函数,请注意,不管是哪一个函数,在执行过程当中都不依赖其余的函数,他们在本质是相对独立的,只是须要在一个特定时间逐一执行而已。一样,你可使用相似chunk()的方法来执行一系列函数,而不会致使锁定浏览器。
浏览器

function schedule(functions, context) {
setTimeout(function() {
  var process = functions.shift();
  process.call(context);
  if (functions.length > 0) {
      setTimeout(arguments.callee, 100);
  }
},
100);
}

schedule函数有两个参数,一个是包含要执行函数的数组,另一个是标明this所属的上下文对象。函数数组以队列方式实现,Timer事件每次触发的时候,都会将队列最前面的函数取出并执行,这个函数能够经过下面的方式执行一系列函数:
app

schedule([doSomething, doSomethingElse, doOneMoreThing], window);

很但愿各个JavaScript的类库都增长相似这样的进程处理函数。YUI在3.0时就已经引入了Queue对象,能够经过timer连续调用一组函数。

无 论现有的技术能够帮助咱们将复杂的进程拆分到什么程度,对于开发者来讲,使用这种方法来理解并肯定脚本失控的瓶颈是很是重要的。不管是太多的循环、递归还 是其余的什么,你如今应该知道若是处理相似的状况。但要记住,这里提到的技术和函数只是起到抛砖引玉的做用,在实际的应用中,你应该对它们加以改进,这样 才能发挥更大的做用。异步

相关文章
相关标签/搜索