原本在那片寫可維護性程式碼文章後就要總結這篇程式碼效能文章的,耽擱了幾天,本來也是決定每天都要更新一篇文章的,因為以前欠下太多東西沒總結,學過的東西沒去總結真的很快就忘記了,記錄一下在你腦力留下更深的印象,特別是這些可維護性代碼,性能什麼的,當在你腦子裡形成一種習慣了,那你就牛了!這裡也要給初學者一個建議:多總結你學過的東西,因為這其實也是在學習新知識! 好,進入我們的主題:如何提高JS程式碼的效能。
1.最佳化DOM互動
DOM與我們的頁面緊密相關,瀏覽器渲染頁面也就是在渲染解析後的DOM元素,DOM操作與互動要消耗大量的時間,因為它們往往需要重新渲染整個頁面或一部分。進一步說,看似細微的一些操作也可能需要花很多時間來執行,因為DOM要處理的資訊非常多,因此我們應該盡可能地優化與DOM相關的操作,加快瀏覽器對頁面的渲染!為什麼有些DOM操作會影響頁面效能,可以查看我寫的一些關於瀏覽器原理的文章:
ok,最佳化DOM操作,我們主要有一些幾種方式:
1.1 最小化現場更新
什麼是DOM的現場更新:需要對DOM部分已經顯示的頁面的一部分的顯示立即更新。但是,每一個更改,不管是插入單個字符,還是一處整個片段,都有一定的性能懲罰,因為瀏覽器需要重新計算無數尺寸以進行更新(相關知識請閱讀:)。所以,現場更新進行的越多,程式碼執行所花的時間就越長,反之程式碼執行越快,如下:
var list = document.getElementById('mylist'), item, i; for(i = 0; i < 10; i++){ item = document.creatElement('li'); list.appendChild(item); item.appendChild(document.creatTextNode('item' + i)); }
這段程式碼為清單mylist增加了10個項目,沒新增一個項目都要進行2次的現場更新:新增元素和新增文字節點,所以這個作業一個需要完成20個現場更新,每個更新都會損失效能,可見這樣的程式碼運作起來是相對緩慢的。
解決的方法是使用文件碎片間接地更改DOM元素:
var list = document.getElementById('mylist'), fragment = document.creatDocumentFragment(), item, i; for(i = 0; i < 10; i++){ item = document.creatElement('li'); fragment .appendChild(item); item.appendChild(document.creatTextNode('item' + i)); } list.appendChild(fragment);
像這樣的程式碼只需進行一次的現場更新。記住,當給appendChild()傳入文件碎片是,只有文檔碎片中的子節點才會被加入到目標元素,碎片本身不會被加入。
現在,你應該明白你用迴圈直接進行DOM節點的增刪查改是多麼對不起瀏覽器的事了吧 `(∩_∩)′ 。
1.2 使用 innerHTML
除了上面程式碼中使用的creatElement() 和 appendChild()結合的方法創建DOM元素之外,還有透過給innerHTML賦值來創建。對於小的DOM更改而言,兩種方法的效率其實差不多,但對於大量的DOM節點的更改,後者要比前者快得多!為啥捏?
因為當我們賦值innerHTML時,後台會建立一個HTML解析器,然後使用內部的DOM調用來建立DOM結構,而非基於Javascript的DOM調用,由於內部方法是編譯好的而非解釋執行的,所以執行程式碼的速度快很多!
用innerHTML改寫上面的範例:
var list = document.getElementById('mylist'), html = '', //声明一个空字符串 i; for(i = 0; i < 10; i++){ html += '<li>item' + i + '</li>'; } list.innerHTML = html; // 这里记得innerHTML后面的HTML四个字母都要大写!
这种方式同样也只进行了一次的现场更新,并且性能要比上一种方式要好!虽然在字符串的链接上有点性能损失。
1.3 使用事件代理/事件委托
事件处理程序为web应用提供交互能力,因此许多开发人员会不分青红皂白地向页面中添加大量的处理程序,有个问题就是一个页面上的事件处理程序数量将直接关系到页面的整体运行性能。为什么捏?
首先,事件处理程序对应至少一个函数,JS中每个函数都是对象,都会占用内存,内存中的对象越多,性能就越差。
其次,我们必须事先指定所有事件处理程序,这就导致了DOM访问次数增多,会延迟整个页面的交互就绪时间,页面响应用户操作变得相对缓慢。
所以减少事件处理程序同样也可以让我们的页面更牛畅!使用事件委托势在必得啊!
事件委托的原理其实就是事件冒泡,只指定一个事件处理程序就可以管理某一类型操作的所有事件。例如:click事件会一直冒泡到document层次,也就是说我们不必为每个元素添加事件,只需在较高的层次的元素上添加事件处理程序即可,然后利用事件对象(event)的属性或方法去判断当前点击的元素,然后做出相应的响应。这个我就不展开讲了,初学者可以自行查阅事件冒泡知识。
2.作用域很重要
说到作用域啊就很容易想到作用域链(scope chain),我们知道要搜索一个变量,所在的执行环境都要沿着这条作用域向上搜索这个变量,作用域链上有很多的变量,那么我们就得遍历,遍历就需要时间啊,而且你越往上查找所需时间越多,如果我们能减少这个时间,我们代码执行效率不是可以提高了吗?
好聪明啊,ok,我看看有哪些方式可以减少这个时间:
2.1 避免全局查找
这是性能优化的一重点,上面也说了,越往上查找时间越多,也就是说查找全局变量和函数比局部要多!看代码:
function updateUI(){ var imgs = document.getElementByTagName('img'); for(var i = 0 ,lng = imgs.length;i < lng;i ++){ imgss[i].title = document.title + 'image' + i; } var msg = docuement.getElementById('msg'); msg.innerHTML = 'update complete.'; }
这代码很正常呀!我之前也经常这么做滴。但是我们细心可以发现,这段代码有三处引用了全局变量document,如果我们的页面很多图片,那么在for循环中的document就会被执行上百次,而每次都要需要在作用域链中查找,时间都去哪了,我还没......停!。
我们可以通过在函数中创建一个局部变量保存对document的引用,这样,我们在函数里任何地方引用document都不用跑到全局变量去找了。这样就改进了代码的性能,看代码:
function updateUI(){ var doc = document; // 将document保存在局部变量doc中 var imgs = doc.getElementByTagName('img'); for(var i = 0 ,lng = imgs.length;i < lng;i ++){ imgss[i].title = doc.title + 'image' + i; } var msg = doc.getElementById('msg'); msg.innerHTML = 'update complete.'; }
所以啊,我们在开发中,如果在函数中会经常用到全局变量,把它保存在局部变量中!
2.2 避免使用with语句
用with语句延长了作用域,查找变量同样费时间,这个我们一般不会用到,所以不展开了。解决方法还是和上面的例子一样,将全局变量保存在局部变量中!
3.优化循环
循环在编程中可谓家常便饭,在js中也随处可见,循环体会反复地执行同一段代码,执行时间一直累加,所以能够对循环体的代码进行优化也可以大大减少执行时间!如何优化?四种方式。
3.1 减值迭代
我们写迭代器(循环条件)的时候一般都这样(var i = 0;i < 10;i ++),从0开始,增加到某个特定值。然而在很多情况下,如果在循环中使用减值迭代器效率更高。我测试了下,如果循环体不复杂的话,两者差不多!
//增值迭代 --效率较低 for(var i = 0;i < items.length;i++){ doSomething(items[i]); } //减值迭代 --效率较高 for(var i = items.length - 1;i >= 0;i--){ doSomething(items[i]); }
3.2 简化终止条件
由于每次循环都会计算终止条件,所以必须保证它的执行尽可能地块。这里主要是避免其他DOM元素及其属性的的查找。
//看终止条件,每次循环都需要查询items及其length属性 for(var i = 0;i < items.length;i++){ doSomething(items[i]); } //将items.length的值保存在局部变量lng中。 for(var i = 0,lng = items.length;i < lng;i++){ doSomething(items[i]); }
3.3 简化循环体
原因和上面以上的,所以在循环体内避免大量的密集的操作。
这其实和上面讲的:1.1 最小化现场更新 。是一样的优化方式。可以倒回去看看。
4.基本的算法优化
在计算机中,算法的复杂度用O表示。下面是javascript中几种常见的算法类型:
O(1) :常数,不管有多少值,执行的时间都是恒定的,比如简单值和存储在变量中的值。
O(log n):对数,总的执行时间和数量有关,但不一定要获取每一个值,如:二分法查找
O(n) :线性,总执行时间和数量直接相关,如:遍历
O(n*n) :平方,总执行时间和数量有关,每个值至少获取N次,如:插入排序
ok,有了上面的知识,我们就可以对javascript进行一些算法上的优化了。看代码:
var value = 5; var sum = value + 10; alert(sum);
这段代码进行了4次常量值的查找:数字5,变量value,数字10,变量sum,这段代码的算法复杂度就是O(1)。又如:
var value = [10,5]; var sum = value[0] + value[1]; alert(sum);
在javascript中访问数组元素也是一个O(1)操作,和简单的变量查找效率一样。再看:
var value = {one:10,two:10}; var sum = value.one + value.two; alert(sum);
要表达的是访问对象上的属性要比访问数组和变量的效率低。因为这是一个O(n)操作。你需要在对象的原型链中查找该属性,所花时间较多。
好了,看完这个是不是感觉眼前一片光明啊。其实我们前面所讲的要把经常用到的全局属性保存在一个局部变量中就是根据这个原理了,访问全局属性是一个O(n)的操作,而访问变量是一个O(1)的操作,大声告诉我,挖掘机哪家强啊!
5.最小化语句数
前面讲的优化差不多都是和精简优化语句有关的,是的,我觉得代码的质量和数量就是性能的评判标准。前面讲了一些代码质量相关的优化,这里就讲讲代码数量的优化。
5.1 精简变量声明
//用了5条语句声明5个变量 var count = 5; var color = 'red'; var values = [1,2,3]; var now = new Date(); //用了1条语句声明5个变量,注意每个变量用逗号隔开 var count = 5, color = 'red', values = [1,2,3], now = new Date();
5.2 使用数组和对象字面量
// 创建两个对象 ----不好的方式 //one 四条语句 var values = new Array(); values[0] = 123; values[1] = 456; values[2] = 789; //two 四条语句 var person = new Object(); person.name = 'jozo'; person.age = 21; person.sayName = function(){ alert(this.name); }; // 创建两个对象 ----推荐的方式 //one 1条语句 var values = [123,456,789] //two 1条语句 var person = { name : 'jozo', age : 21, sayName : function(){ alert(this.name); };
6.其他
写累了,如有不正确的地方请指正哦,还有一些其他的优化,下次文章继续!