首先,性能优化分好几个方面,本章我们从js方面来优化。
1:垃圾收集
日常中的某些情况下垃圾收集器无法回收无用变量,导致的一个结果就是——内存使用率不断增高,以下为对应的情况以及处理方法。
①对象相互引用会导致引用计数始终为2,所以用完对象后应将引用设为null,例子如下
let element = document.getElementById("test"); let myObject = new Object(); myObject.element = element; element.someObject = myObject;
//....用完后需要加如下代码
myObject.element = null;
element.someObject = null;
②当数据不再有用时,需要通过将值设为null来解除引用,该做法适用于大多数全局变量和全局对象属性,例子如下
function createPerson(name){ let localPerson = new Object(); localPerson.name = name; return localPerson } let globalPerson = createPerson("test") //...用完后手动解除 globalPerson = null
③关于与闭包相关的内存泄漏如下
function assignHandler(){ let element = document.getElementById("test"); element.onclick = function(){ alert(element.id) } } //以上会导致element的引用数无法被回收,更改如下 function assignHandler(){ let element = document.getElementById("test"); let id = element.id; element.onclick = function(){ alert(id) } element = null; }
2:事件委托
在js中,添加到页面上的事件处理程序数量会直接关系到页面整体运行运行性能。导致这一问题的原因是多方面的。首先函数都是对象,都会占用内存;内存中对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。以下为对应的情况以及处理方法
①同类型的事件处理函数过多时,应该结合为一个,例子如下:
//html代码 <ul id="myLinks"> <li id="goSomeWhere">Go somewhere</li> <li id="sayHi">Say hi</hi> </ul> //分别加上事件处理-JS代码 let item1 = document.getElementById("goSomeWhere"); let item2 = document.getElementById("sayHi"); EventUtil.addHandler(item1, "click", function(event){ console.log("goSomeWhere") } EventUtil.addHandler(item2, "click", function(event){ console.log("sayHi"); } //改善点即将click事件结合在一起 let list = document.getElementById("myLinks") EventUtil.addHandler(list, "click", function(event){ event = EventUtil.getEvent(event); let target = EventUtil.getTarget(event); switch(target.id){ case "goSomeWhere": console.log("goSomeWhere"); break; case "sayHi": console.log("sayHi"); break; } }
②内存留有过时不用的“空事件处理程序”也是造成性能问题的主因,两种情况下会造成该问题。运用removeChild()和replaceChild()方法去除节点时;在使用innerHTML替换页面某一部分时,如果带有事件处理程序的元素被innerHTML删除了,那么原有事件处理函数极有可能无法被回收,例子如下
//例子中id为myBtn的点击事件变为了空事件处理程序 <div id="myDiv"> <input type="button" value="Click Me" id="myBtn"> </div> <script type="text/javascript"> let btn = document.getElementById("myBtn"); btn.onclick = function(){ document.getElementById("myDiv").innerHTML = "xxxx"; }; </script> //改善点即需要手工移除事件处理程序 <div id="myDiv"> <input type="button" value="Click Me" id="myBtn"> </div> <script type="text/javascript"> let btn = document.getElementById("myBtn"); btn.onclick = function(){ btn.onclick = null; document.getElementById("myDiv").innerHTML = "xxxx"; }; </script>
3:注意作用域
关于作用域链,我们明白访问全局变量会比访问局部变量要慢
①若某处循环使用全局变量时,我们可以略做修改,例子如下
//假设有多个img标签的内容,循环中引用了多次document全局变量 function updateUI(){ let imgs = document.getElementsByTagName("img") for (let i = 0; len = imgs.length; i < len; ++i){ imgs[i].title = document.title + " image “ + i } let msg = document.getElementById("msg"); msg.innerHTML = "Update"; } //改善点 function updateUI(){ let doc = document let imgs = doc.getElementsByTagName("img") for (let i = 0; len = imgs.length; i < len; ++i){ imgs[i].title = doc.title + " image “ + i } let msg = doc.getElementById("msg"); msg.innerHTML = "Update"; }
②尽量少用with,因为with会增加其中执行代码的作用域链的长度
4:选择正确方法
首先,我们要了解JS中算法的复杂度
标记名称 | 描述 | |
O(1) | 常数 | 不管有多少值,执行的时间都是恒定的。一般表示简单值和存储在变量中的值 |
O(log n) | 对数 | 总的执行时间和值的数量相关,但是要完成算法并不一定要获取每个值。例如:二分查询 |
O(n) | 线性 | 总执行时间和值的数量直接相关。例如:遍历某个数组中的所有元素 |
O(n^2) | 平方 | 总执行时间和值的数量有关,每个值至少要获取n次。例如:插入排序 |
常数值和访问数组元素操作都是O(1)操作;对象属性查找操作是O(n)操作;
如let values = [5, 10]; let sum = values[0] + values[1]属于O(1)操作;let values = window.location.href属于O(2)操作
①遇到有多次属性查询的场合,可以考虑是否能做优化,例子如下
//这里总共做了6次属性查询,其中window.location.href.substring与window.location.href.indexOf分别为3次 let query = window.location.href.subsring(window.location.href.indexOf("?")) //改善, 第一次访问时复杂度会是O(n),但该版本只有4次属性查询,相对于原始版本节省了33% let url = window.location.href; let query = url.substring(url.indexOf("?"));
②循环优化,这里其实用后测试循环代替前测试循环会更好,不过本地不采用,例子如下
//原有复杂度为O(n) for (let i = 0; i < values.length; ++i){ process(values[i]); } //更改后复杂度为O(1) for (let i = values.length - 1; i >= 0; --i){ process(values[i]) }
③最小化语句数相关
例如进行多个声明时,我们可以进行组合,例子如下
//多个声明 let count = 5; let color = "blue"; let values = [1, 2, 3]; //组合成一个 let count = 5, color = ”blue", values = [1, 2, 3]
例如插入迭代值时,例子如下
//修改前 let name = values[i]; i++; //修改后 let name = values[i++]
使用数组和对象字面量时,例子如下
//修改前 let values = new Array(); values[0] = 123; values[1] = 456; values[2] = 789; let person = new Object(); person.name = "Eric"; person.age = 20; //修改后 let values = [123, 456, 789] let person = { name: "Eric", age:20, }
④创建DOM节点最好使用innerHTML方法,因为innerHTML设置值时,后台会创建HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JS的DOM调用。
调用一次innerHTML,就会进行一次现场刷新,循环插入DOM结构时,应注意尽量调用少次数的innerHTML,代码如下
//错误方法,做了很多次现场刷新 let list = document.getElementById("myList"), i; for (i = 0; i < 10; ++i){ list.innerHTML = html+= "<li>Item " + i + "</li>" } //正确方法,尽管在字符串连接上有性能损失,但却只做了一次现场刷新 let list = document.getElementById("myList"), html = "", i; for (i = 0; i < 10; ++i){ html += "<li>Item " + i + "</li>" } list.innerHTML = html
⑤其他如有多个if-else语句时,应尽可能转为Switch语句;用appendChild()插入元素时,应采用自上而下插入;面向对象编程时,应合理释放内存,设object为null。