(转)优化js脚本设计,防止浏览器假死

    在Web开发的时候常常会遇到浏览器不响应事件进入假死状态,甚至弹出“脚本运行时间过长“的提示框,若是出现这种状况说明你的脚本已经失控了,必须进行优化。javascript

为何会出现这种状况呢,咱们先来看一下浏览器的内核处理方式:java

 
    浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。
 
  1. JavaScript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来而后加以处理,浏览器不管再何时都只有一个JS线程在运行JS程序。
  2. GUI 渲染线程负责渲染浏览器界面,当界面须要重绘(Repaint)或因为某种操做引起回流(reflow)时,该线程就会执行。但须要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时当即被执行。
  3. 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其余线程如鼠标点击、AJAX异步请求等,但因为JS的单线程关系全部这些事件都得排队等待JS引擎处理。
 

了解了浏览器的内核处理方式就不难理解浏览器为何会进入假死状态了,当一段JS脚本长时间占用着处理机就会挂起浏览器的GUI更新,然后面的事件响应也被排在队列中得不处处理,从而形成了浏览器被锁定进入假死状态。另外JS脚本中进行了DOM操做,一旦JS调用结束就会立刻进行一次GUI渲染,而后才开始执行下一个任务,因此JS中大量的DOM操做也会致使事件响应缓慢甚至真正卡死浏览器,如在IE6下一次插入大量的HTML。而若是真的弹出了“脚本运行时间过长“的提示框则说明你的JS脚本确定有死循环或者进行过深的递归操做了。算法

Nicholas C. Zakas认为不论什么脚本,在任什么时候间、任何浏览器上执行都不该该超过100毫秒,不然必定要将脚本分解成若干更小的代码段。那么咱们该如何来作呢:shell

第一步,优化你的循环,循环体中包含太多的操做和循环的次数过多都会致使循环执行时间过长,并直接致使锁死浏览器。若是循环以后没有其余操做,每次循环只处理一个数值,并且不依赖于上一次循环的结果则能够对循环进行拆解,看下面的chunk的函数:数组

 

function chunk(array, process, context) {浏览器

    setTimeout(function() {缓存

    var item = array.shift();多线程

    process.call(context, item);闭包

    if (array.length > 0) {异步

        setTimeout(arguments.callee, 100);

    }), 100);

}

 

chunk()函数的用途就是将一个数组分红小块处理,它接受三个参数:要处理的数组,处理函数以及可选的上下文环境。每次函数都会将数组中第一个对象取出交给process函数处理,若是数组中还有对象没有被处理则启动下一个timer,直到数组处理完。这样可保证脚本不会长时间占用处理机,使浏览器出一个高响应的流畅状态。

其实在我看来,借助JS强大的闭包机制任何循环都是可拆分的,下面的版本增长了callback机制,使可再循环处理完毕以后进行其余的操做。

 

function chunk(array,process,cbfun){

    var i=0,len = array.length;    //这里要注意在执行过程当中数组最好是不变的

    setTimeout(function(){

        process( array[i] , i++ );    //循环体要作的操做

        if( i < len ){

            setTimeout(arguments.callee,100);

        }else{

            cbfun()                //循环结束以后要作的操做

        }

    }

}

 

第二步,优化你的函数,若是函数体内有太多不相干但又要一块儿执行的操做则能够进行拆分,考虑下面的函数:

 

function dosomething(){

    dosomething1();

    dosomething2();

}

 

dosomething1和dosomething2互不相干,执行没有前后次序,可用前面提到的chunk函数进行拆分:

 

function dosomething(){

    chunk([dosomething1,dosomething2],function(item){item();})

}

 

或者直接交给浏览器去调度

function dosome(){

    setTimeout(dosomething1,0);

    setTimeout(dosomething2,0);

}

 

第三步,优化递归操做,函数递归虽然简单直接可是过深的递归操做不但影响性能并且稍不注意就会致使浏览器弹出脚本失控对话框,必须当心处理。

看如下斐波那契数列的递归算法:

function fibonacci(n) {

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

};

 

fibonacci(40)这条语句将重复调用自身331160280次,在浏览器中执行必然致使脚本失控,而采用下面的算法则只须要调用40次

fibonacci = function(n){

    var memo = {0:0,1:0};         //计算结果缓存

    var shell = function(n){

        var result = memo[n];

        iftypeof result != 'number' )    //若是值没有被计算则进行计算

            memo[n] = shell(n-1) + shell(n -2);

            return memo[n];

        }

        return shell(n);

}

 

这项技术被称为memoization,他的原理很简单就是一样的结果你不必计算两次。另外一种消除递归的办法就是利用迭代,递归和迭代常常会被做为互相弥补的方法。

第四步,减小DOM操做,DOM操做的代价是至关昂贵的,大多数DOM操做都会触发浏览器的回流(reflow)操做。例如添加删除节点,修改元素样式,获取须要通过计算的元素样式等。咱们要作的就是尽可能少的触发回流操做。

 

el.style.width = '300px' el.style.height = '300px'

el.style.backgroundColor = 'red'

 

上面的操做会触发浏览器的三次回流操做,再看下面的方式:

 

el.className = 'newStyle'

 

经过设置改元素的className一次设置多个样式属性,将样式写再CSS文件中,只触发一次回流,达到了一样是效果并且效率更高。由于浏览器最擅长的就是根据class设置样式。

还有不少能够减小DOM操做的方法,在此就很少说了,可是一个基本的原则就是让浏览器去作它本身擅长的事情,例如经过class来改变元素的属性。

相信通过上面的优化的过程一定能够大大提升用户体验,不会出现浏览器被锁死和弹出脚本失控的对话框,使你的浏览器从繁重的任务中解放出来。须要指出的是上面这些优化并非必须的,只有当一段脚本的执行时间真的影响到了用户体验才须要进行。虽然它们让用户以为脚本的执行变快了,但其实完成同一个操做的时间可能被延长了,这些技术只是让浏览器处于一个快速响应的状态,使用户浏览更流畅。

 

最后送一句忠告:过早优化是万恶之源。

相关文章
相关标签/搜索