如何编写高性能JavaScript

    作者:课课家教育更新于: 2015-11-26 14:55:08

    本学期的教学,小编为大家来讲解一下。课课家将带给您与众不同的知识。面对这些问题最开始(天真)的做法是使用对象存储数据并放入数组中,使用遍历数据绘制表格并append到DOM中,最后使用事件绑定我们期望地点击行为。
    很多Script引擎,如Google的V8引擎(被Chrome和Node所用),是专门为需要快速执行的大型JavaScript应用所的。如果你是一个开发者,并且关心内存使用情况与页面性能,你应该了解用户浏览器中的JavaScript引擎是如何运作的。无论是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其他引擎,这样做可以帮助你更好地优化你的应用程序。这并不是说应该专门为某一浏览器或引擎做优化,千万别这么做。
    但是,你应该问自己几个问题:
    •在我的代码里,是否可以使代码更高效一些
    •主流的JavaScript引擎都做了哪些优化
    •什么是引擎无法优化的,垃圾回收器(GC)是否能回收我所期望的东西                                            垃圾回收器尝试回收内存
    在JavaScript中,是不可能强制进行垃圾回收的。你不应该这么做,因为垃圾收集过程是由运行时控制的,它知道什么是最好的清理时机。
    “消除引用”的误解
    网上有许多关于JavaScript内存回收的讨论都谈到delete这个关键字, java基础教程虽然它可以被用来删除对象(map)中的属性(key),但有部分开发者认为它可以用来强制“消除引用”。建议尽可能避免使用delete,在下面的例子中delete o.x 的弊大于利,因为它改变了o的隐藏类,并使它成为一个"慢对象"。
    var o = { x:1 };
    delete o.x; // true
    o.x; // undefined
    你会很容易地在流行的JS库中找到引用删除——这是具有语言目的性的。这里需要注意的是避免在运行时修改”hot”对象的结构。JavaScript引擎可以检测出这种“hot”的对象,并尝试对其进行优化。如果对象在生命周期中其结构没有较大的改变,引擎将会更容易优化对象,而delete操作实际上会触发这种较大的结构改变,因此不利于引擎的优化。
    对于null是如何工作也是有误解的。将一个对象引用设置为null,并没有使对象变“空”,只是将它的引用设置为空而已。使用o.x= null比使用delete会更好些,但可能也不是很必要。
    var o = { x:1 };
    o = null;
    o; // null
    o.x // TypeError
    如果此引用是当前对象的最后引用,那么该对象将被作为垃圾回收。如果此引用不是当前对象的最后引用,则该对象是可访问的且不会被垃圾回收。
    另外需要注意的是,全局变量在页面的生命周期里是不被垃圾回收器清理的。无论页面打开多久,JavaScript运行时全局对象作用域中的变量会一直存在。
    var myGlobalNamespace = {};
    全局对象只会在刷新页面、导航到其他页面、关闭标签页或退出浏览器时才会被清理。函数作用域的变量将在超出作用域时被清理,即退出函数时,已经没有任何引用,这样的变量就被清理了。
    经验法则
    为了使垃圾回收器尽早收集尽可能多的对象,不要hold着不再使用的对象。这里有几件事需要记住:
    •正如前面提到的,在合适的范围内使用变量是手动消除引用的更好选择。即一个变量只在一个函数作用域中使用,就不要在全局作用域声明它。这意味着更干净省心的代码。
    •确保解绑那些不再需要的事件**,尤其是那些即将被销毁的DOM对象所绑定的事件**。
    •如果使用的数据缓存在本地,确保清理一下缓存或使用老化机制,以避免大量不被重用的数据被存储。
    函数
    接下来,我们谈谈函数。正如我们已经说过,垃圾收集的工作原理,是通过回收不再是访问的内存块(对象)。为了更好地说明这一点,这里有一些例子。
    function foo(){
    var bar = new LargeObject();
    bar.someCall();
    }
    当foo返回时,bar指向的对象将会被垃圾收集器自动回收,因为它已没有任何存在的引用了。
    对比一下:
    function foo(){
    var bar = new LargeObject();
    bar.someCall();
    return bar;
    }
    // somewhere else
    var b = foo();
    现在我们有一个引用指向bar对象,这样bar对象的生存周期就从foo的调用一直持续到调用者指定别的变量b(或b超出范围)。
    闭包(CLOSURES)
    当你看到一个函数,返回一个内部函数,该内部函数将获得范围外的访问权,即使在外部函数执行之后。这是一个基本的闭包 —— 可以在特定的上下文中设置的变量的表达式。例如:
    function sum (x){
    function sumIt(y){
    return x + y;
    };
    return sumIt;
    }
    // Usage
    var sumA = sum(4);
    var sumB = sumA(3);
    console.log(sumB); // Returns 7
    在sum调用上下文中生成的函数对象(sumIt)是无法被回收的,它被全局变量(sumA)所引用,并且可以通过sumA(n)调用。
    让我们来看看另外一个例子,这里我们可以访问变量largeStr吗?
    var a = function (){
    var largeStr = new Array(1000000).join(‘x’);
    return function (){
    return largeStr;
    };
    }();
    是的,我们可以通过a()访问largeStr,所以它没有被回收。下面这个呢?
    var a = function (){
    var smallStr = ‘x’;
    var largeStr = new Array(1000000).join(‘x’);
    return function (n){
    return smallStr;
    };
    }();
    我们不能再访问largeStr了,它已经是垃圾回收候选人了。
    定时器
    最糟的内存泄漏地方之一是在循环中, java基础或者在setTimeout()/ setInterval()中,但这是相当常见的。思考下面的例子:
    var myObj = {
    callMeMaybe:function (){
    var myRef = this;
    var val = setTimeout(function (){
    console.log(‘Time is running out!’);
    myRef.callMeMaybe();
    },1000);
    }
    };
    如果我们运行myObj.callMeMaybe();来启动定时器,可以看到控制台每秒打印出“Time is running out!”。如果接着运行myObj = null,定时器依旧处于激活状态。为了能够持续执行,闭包将myObj传递给setTimeout,这样myObj是无法被回收的。相反,它引用到myObj的因为它捕获了myRef。这跟我们为了保持引用将闭包传给其他的函数是一样的。
    同样值得牢记的是,setTimeout/setInterval调用(如函数)中的引用,将需要执行和完成,才可以被垃圾收集。
    当心性能陷阱
    永远不要优化代码,直到你真正需要。现在经常可以看到一些基准测试,显示N比M在V8中更为优化,但是在模块代码或应用中测试一下会发现,这些优化真正的效果比你期望的要小的多。                                       Speed trap. 做的过多还不如什么都不做. 
    比如我们想要创建这样一个模块:
    •需要一个本地的数据源包含数字ID
    •绘制包含这些数据的表格
    •添加事件处理程序,当用户点击的任何单元格时切换单元格的css class
    这个问题有几个不同的因素,虽然也很容易解决。我们如何存储数据,如何高效地绘制表格并且append到DOM中,如何更优地处理表格事件?
    面对这些问题最开始(天真)的做法是使用对象存储数据并放入数组中,使用遍历数据绘制表格并append到DOM中,最后使用事件绑定我们期望地点击行为。
    注意:这不是你应该做的
    var moduleA = function (){
    return {
    data:dataArrayObject,
    init:function (){
    this.addTable();
    this.addEvents();
    },
    addTable:function (){
    for (var i = 0; i
    $tr = $(‘’);
    for (var j = 0; j
    $tr.append(‘’ + this.data[j][‘id’]+ ‘’);
    }
    $tr.appendTo($tbody);
    }
    },
    addEvents:function (){
    $(‘table td’).on(‘click’,function (){
    $(this).toggleClass(‘active’);
    });
    }
    };
    }();
    这段代码简单有效地完成了任务。
    但在这种情况下,我们遍历的数据只是本应该简单地存放在数组中的数字型属性ID。有趣的是,直接使用DocumentFragment和本地DOM方法比使用jQuery(以这种方式)来生成表格是更优的选择,当然,事件代理比单独绑定每个td具有更高的性能。
    要注意虽然jQuery在内部使用DocumentFragment,但是在我们的例子中,代码在循环内调用append并且这些调用涉及到一些其他的小知识,因此在这里起到的优化作用不大。希望这不会是一个痛点,但请务必进行基准测试,以确保自己代码ok。
    对于我们的例子,上述的做法带来了(期望的)性能提升。事件代理对简单的绑定是一种改进,可选的DocumentFragment也起到了助推作用。

    var moduleD = function (){
    return {
    data:dataArray,
    init:function (){
    this.addTable();
    this.addEvents();
    },
    addTable:function (){
    var td,tr;
    var frag = document.createDocumentFragment();
    var frag2 = document.createDocumentFragment();
    for (var i = 0; i
    tr = document.createElement(‘tr’);
    for (var j = 0; j
    td = document.createElement(‘td’);
    td.appendChild(document.createTextNode(this.data[j]));
    frag2.appendChild(td);
    }
    tr.appendChild(frag2);
    frag.appendChild(tr);
    }
    tbody.appendChild(frag);
    },
    addEvents:function (){
    $(‘table’).on(‘click’,‘td’,function (){
    $(this).toggleClass(‘active’);
    });
    }
    };
    }();

    接下来看看其他提升性能的方式。你也许曾经在哪读到过使用原型模式比模块模式更优,或听说过使用JS模版框架性能更好。有时的确如此,不过使用它们其实是为了代码更具可读性。对了,还有预编译!让我们看看在实践中表现的如何?
    moduleG = function (){};
    moduleG.prototype.data = dataArray;
    moduleG.prototype.init = function (){
    this.addTable();
    this.addEvents();
    };
    moduleG.prototype.addTable = function (){
    var template = _.template($(‘#template’).text());
    var html = template({‘data’ :this.data});
    $tbody.append(html);
    };
    moduleG.prototype.addEvents = function (){
    $(‘table’).on(‘click’,‘td’,function (){
    $(this).toggleClass(‘active’);
    });
    };
    var modG = new moduleG();
    事实证明,在这种情况下的带来的性能提升可以忽略不计。模板和原型的选择并没有真正提供更多的东西。也就是说,性well 能并不是开发者使用它们的原因,给代码带来的可读性、继承模型和可维护性才是真正的原因。
    更复杂的问题包括高效地在canvas上绘制图片和操作带或不带类型数组的像素数据。
    在将一些方法用在你自己的应用之前,一定要多了解这些方案的基准测试。也许有人还记得JS模版的shoot-off和随后的扩展版。你要搞清楚基准测试不是存在于你看不到的那些虚拟应用,而是应该在你的实际代码中去测试带来的优化。
    V8优化技巧
    详细介绍了每个V8引擎的优化点在本文讨论范围之外,当然这里也有许多值得一提的技巧。记住这些技巧你就能减少那些性能低下的代码了。
    •特定模式可以使V8摆脱优化的困境,比如说try-catch。欲了解更多有关哪些函数能或不能进行优化,你可以在V8的脚本工具d8中使用–trace-opt file.js命令。
    •如果你关心速度,尽量使你的函数职责单一,即确保变量(包括属性,数组,函数参数)只使用相同隐藏类包含的对象。举个例子,别这么干:
    function add(x,y){
    return x+y;
    }
    add(1,2);
    add(‘a’,‘b’);
    add(my_custom_object,undefined);
    •不要加载未初始化或已删除的元素。如果这么做也不会出现什么错误,但是这样会使速度变慢。
    •不要使函数体过大,这样会使得优化更加困难。
    更多内容可以去看Daniel Clifford在Google I/O的分享Breaking the JavaScript Speed Limit with V8。Optimizing For V8 — A Series也非常值得一读。
    对象VS数组:我应该用哪个?
    •如果你想存储一串数字,或者一些相同类型的对象,使用一个数组。
    •如果你语义上需要的是一堆的对象的属性(不同类型的),使用一个对象和属性。这在内存方面非常高效,速度也相当快。
    •整数索引的元素,无论存储在一个数组或对象中,都要比遍历对象的属性快得多。
    •对象的属性比较复杂:它们可以被setter们创建,具有不同的枚举性和可写性。数组中则不具有如此的定制性,而只存在有和无这两种状态。在引擎层面,这允许更多存储结构方面的优化。特别是当数组中存在数字时,例如当你需要容器时,不用定义具有x,y,z属性的类,而只用数组就可以了。
    JavaScript中对象和数组之间只有一个的主要区别,那就是数组神奇的length属性。如果你自己来维护这个属性,那么V8中对象和数组的速度是一样快的。
    使用对象时的技巧
    •使用一个构造函数来创建对象。这将确保它创建的所有对象具有相同的隐藏类,并有助于避免更改这些类。作为一个额外的好处,它也略快于Object.create()
    •你的应用中,对于使用不同类型的对象和其复杂度(在合理的范围内:长原型链往往是有害的,呈现只有一个极少数属性的对象比大对象会快一点)是有没限制的。对于“hot”对象,尽量保持短原型链,并且少属性。
    对象克隆
    对于应用程序开发人员,对象克隆是一个常见的问题。虽然各种基准测试可以证明V8对这个问题处理得很好,但仍要小心。复制大的东西通常是较慢的——不要这么做。JS中的for..in循环尤其糟糕,因为它有着恶魔般的规范,并且无论是在哪个引擎中,都可能永远不会比任何对象快。
    当你一定要在关键性能代码路径上复制对象时,使用数组或一个自定义的“拷贝构造函数”功能明确地复制每个属性。这可能是最快的方式:
    function clone(original){
    this.foo = original.foo;
    this.bar = original.bar;
    }
    var copy = new clone(original);
    模块模式中缓存函数
    使用模块模式时缓存函数,可能会导致性能方面的提升。参阅下面的例子,因为它总是创建成员函数的新副本,你看到的变化可能会比较慢。
    另外请注意,使用这种方法明显更优,不仅仅是依靠原型模式(经过jsPerf测试确认)。                                        空字面量与预分配数组在不同的浏览器进行测试
    Nitro (Safari)对预分配的数组更有利。而在其他引擎(V8,SpiderMonkey)中,预先分配并不是高效的。
    预分配数组测试
    // Empty array
    var arr = [];
    for (var i = 0; i <1000000; i++){
    arr[i]= i;
    }
    // Pre-allocated array
    var arr = new Array(1000000);
    for (var i = 0; i <1000000; i++){
    arr[i]= i;
    }
    优化你的应用
    在Web应用的世界中,速度就是一切。没有用户希望用一个要花几秒钟计算某列总数或花几分钟汇总信息的表格应用。这是为什么你要在代码中压榨每一点性能的重要原因。                                        Chrome开发者工具的分析面板
    分析过程开始获取代码性能基线,然后以时间线的形式体现。这将告诉我们代码需要多长时间运行。“Profiles”选项卡给了我们一个更好的视角来了解应用程序中发生了什么。JavaScript CPU分析文件展示了多少CPU时间被用于我们的代码,CSS选择器分析文件展示了多少时间花费在处理选择器上,堆快照显示多少内存正被用于我们的对象。
    利用这些工具,我们可以分离、调整和重新分析来衡量我们的功能或操作性能优化是否真的起到了效果。                                 Chrome开发者工具中的内存统计
    内存统计出我们团队所关心的私有内存使用、JavaScript堆的大小、DOM节点数量、存储清理、事件**计数器和垃圾收集器正要回收的东西。推荐阅读Loreena Lee的“3快照”技术。该技术的要点是,在你的应用程序中记录一些行为,强制垃圾回收,检查DOM节点的数量有没有恢复到预期的基线,然后分析三个堆的快照来确定是否有内存泄漏。
    单页面应用的内存管理
    单页面应用程序(例如AngularJS,Backbone,Ember)的内存管理是非常重要的,它们几乎永远不会刷新页面。这意味着内存泄漏可能相当明显。移动终端上的单页面应用充满了陷阱,因为设备的内存有限,并在长期运行Email客户端或社交网络等应用程序。能力愈大责任愈重。
    有很多办法解决这个问题。在Backbone中,确保使用dispose()来处理旧视图和引用(目前在Backbone(Edge)中可用)。这个函数是最近加上的,移除添加到视图“event”对象中的处理函数,以及通过传给view的第三个参数(回调上下文)的model或collection的事件**。dispose()也会被视图的remove()调用,处理当元素被移除时的主要清理工作。Ember 等其他的库当检测到元素被移除时,会清理**以避免内存泄漏。
    Derick Bailey的一些明智的建议:
    与其了解事件与引用是如何工作的,不如遵循的标准规则来管理JavaScript中的内存。如果你想加载数据到的一个存满用户对象的Backbone集合中,你要清空这个集合使它不再占用内存,那必须这个集合的所有引用以及集合内对象的引用。一旦清楚了所用的引用,资源就会被回收。这就是标准的JavaScript垃圾回收规则。
    在文章中,Derick涵盖了许多使用Backbone.js时的常见内存缺陷,以及如何解决这些问题。
    Felix Geisendörfer的在Node中调试内存泄漏的教程也值得一读,尤其是当它形成了更广泛SPA堆栈的一部分。
    减少回流(REFLOWS)
    当浏览器重新渲染文档中的元素时需要重新计算它们的位置和几何形状,我们称之为回流。回流会阻塞用户在浏览器中的操作,因此理解提升回流时间是非常有帮助的。                                          一个JavaScript内存泄漏检测工具
    有完整的文章介绍了如何使用这个工具,建议你自己到内存泄漏探测器项目页面看看。
    如果你想知道为什么这样的工具还没集成到我们的,其原因有二。它最初是在Closure库中帮助我们捕捉一些特定的内存场景,它更适合作为一个外部工具。
    V8优化调试和垃圾回收的标志位
    Chrome支持直接通过传递一些标志给V8,以获得更详细的引擎优化输出结果。例如,这样可以追踪V8的优化:
    “/Applications/Google Chrome/Google Chrome” --js-flags=“--trace-opt --trace-deopt”
    用户可以这样运行chrome.exe –js-flags=”–trace-opt –trace-deopt”
    在开发应用程序时,下面的V8标志都可以使用。
    •trace-opt —— 记录优化函数的名称,并显示跳过的代码,因为优化器不知道如何优化。
    •trace-deopt —— 记录运行时将要“去优化”的代码。
    •trace-gc —— 记录每次的垃圾回收。
    V8的处理脚本用*(星号)标识优化过的函数,用~(波浪号)表示未优化的函数。
    如果你有兴趣了解更多关于V8的标志和V8的内部是如何工作的,强烈建议阅读Vyacheslav Egorov的excellent post on V8 internals。
    HIGH-RESOLUTION TIME 和NAVIGATION TIMING API
    高精度时间(HRT)是一个提供不受系统时间和用户调整影响的亚毫秒级高精度时间接口,可以把它当做是比new Date 和Date.now()更精准的度量方法。这对我们编写基准测试帮助很大。                                     显示在控制台中的时间信息
    我们可以从上面的数据获取很多有用的信息,例如网络延时为responseEnd – fetchStart,页面加载时间为loadEventEnd – responseEnd,处理导航和页面加载的时间为loadEventEnd – navigationStart。
    正如你所看到的,perfomance.memory的属性也能显示JavaScript的内存数据使用情况,如总的堆大小。
    更多Navigation Timing API的细节,阅读Sam Dutton的Measuring Page Load Speed With Navigation Timing。
    ABOUT:MEMORY 和ABOUT:TRACING
    Chrome中的about:tracing提供了浏览器的性能视图,记录了Chrome的所有线程、tab页和进程。                                           谨记关注优化,但为了便利可以舍弃一些很小的优化。例如,有些开发者选择.forEach和Object.keys代替for和for..in循环,尽管这会更慢但使用更方便。要保证清醒的头脑,知道什么优化是需要的,什么优化是不需要的。
    同时注意,虽然JavaScript引擎越来越快,但下一个真正的瓶颈是DOM。回流和重绘的减少也是重要的,所以必要时再去动DOM。还有就是要关注网络,HTTP请求是珍贵的,特别是移动终端上,因此要使用HTTP的缓存去减少资源的加载。
    记住这几点可以保证你获取了本文的大部分信息,希望对你有所帮助!
    如果还有疑问,大家可以来课课家官网查询一下呀

javascript 更多推荐

课课家教育

未登录