js编写的可维护与性能优化

可维护

解耦HTML/JavaScript

1、采用引入js文件的方式取代在html页面写js代码
2、避免在js中创建大量html
(1)当js用于插入数据时,尽量不要直接插入标记。一般可以在页面中直接包含并隐藏标记,然后等到整个页面渲染好之后,就可以用js显示该标记,而非生成它
(2)也可以通过Ajax请求获得更多要显示的html,这个方法可以让同样的渲染层(PHP、JSP、Ruby等等)来输出标记,而不是直接嵌在js

解耦CSS/JavaScript

尽量不要在js中更改样式,而是采用动态更改样式类,从而做到对于样式的问题应只查看CSS文件来解决,例:

element.style.color = 'red';
element.style.backgroundColor = 'blue';

更改后:
element.className = 'edit';

解耦应用逻辑/事件处理程序

每个web应用一般都有相当多的事件处理程序,监听这无数不同的事件,然而,很少有能仔细得将应用逻辑从事件处理程序中分离的,如下:

function handleKeyPress (event) {
    if(event.keyCode == 1){
        let target = event;
        let value = 5 * parseInt(target.value);
        if(value > 10){
            document.getElementById('tip-msg').style.display = 'block';
        }
    }
}

这个事件处理程序处理包含了应用逻辑,还进行了事件处理,这种方式的问题有其双重性
(1)、处理通过事件之外就没有办法执行应用逻辑
(2)、调试困难,如果没有发生预想的结果,并不知道是事件没被调用还是应用逻辑失败
(3)、如果后续的事件需要执行相同的应用逻辑,那么就必须复制功能代码或将代码抽到一个单独的函数中所以就好进行两者的解耦,即一个事件处理程序应该从事件对象中提取相关信息,并将这些信息传送到处理应用逻辑的某个方法中,前面例子重写如下:

function validateValue (value) {
    value = 5 * parseInt(value);
    if(value > 10){
        document.getElementById('tip-msg').style.display = 'block';
    }
}

function handleKeyPress (event) {
    if(event.keyCode == 13){
        let target = event;
        validateValue(target.value);
    }
}

从而使validateValue()中没有任何东西会依赖于任何事件处理程序逻辑,他只接收一个值,并根据该值进行其他处理
好处:如果事件最初只有鼠标事件触发,那么现在只需少量修改就可以实现按键触发

避免全局变量

最多创建一个全局变量,让其他对象和函数存在其中,如:

//两个全局变量——避免!!
let name= 'bad';
function sayName() {
    alert(name);
}    

//一个全局变量——推荐
let good = {
    name: 'nice',
    sayName: function () {
        alert(this.name);
    }
}

多人协作开发可以使用命名空间

//创建全局对象
let wrox = {};

//为Tony创建命名空间
wrox.Tony = {};

//为Tony(可以以人名划分)创建方法
wrox.Tony.sayName = function () {
    ...
};

上述例子,wrox是全局变量,其他命名空间在此之上创建,只要所有人都按这样写,那么就不用当心不同开发者创建相同的方法等,因为它存在于不同的命名空间

避免与null进行比较

应该让条件与预想的类型进行比较而不是与null

//bad
function sortArray(values) {
    if(values != null){
        ...
    }
}
//good
function sortArray(values) {
    if(values instanceof Array){
        ...
    }    
}

抽离常量

const constants = {
    INVALID_VALUE_MSG:'Invalid value!',
    INVALID_VALUE_URL:'/errors/invalid.php'
}

性能优化

因为访问全局变量要比访问局部变量慢,因为要遍历作用域链,所以减少花费在作用域链上的时间,就可以增加脚本的整体新能

避免全局查找

//bad
function updateUI () {
    let imgs = document.getElementsByTagName('img');
    for(let i=0, len=imgs.length; i<len; i++){
        imgs[i].title = document.title + 'imge' + i;
    }
    let msg = document.getElementById('msg');
    msg.innerHTML = 'Upadate complete.';
}
//good
function updateUI () {
    let doc = document;
    let imgs = doc.getElementByTagName('img');
    for(let i=0, len=imgs.length; i<len; i++){
        imgs[i].title = doc.title + 'imge' + i;
    }
    let msg = doc.getElementById('msg');
    msg.innerHTML = 'Upadate complete.';
}

上面将document对象保存在doc变量中,然后替换原来的document,与原来相比,只需进行一次全局查找,速度肯定更快

循环优化

循环优化基本步骤如下:
1、减值迭代——大多数循环使用一个从0开始、增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中的迭代器更加高效。
2、简化终止条件——由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或者其他O(n)的操作。
3、简化循环体——循环体是执行最多的,所以要确保其被最大限地优化,确保没有某些可以被很容易移除循环的密集计算。
4、使用后侧试循环——最常用的for循环和while循环都是前测试循环。而入do—while这种后侧试循环,可以避免最初终止条件的计算

//基本for循环
for(let i=0;i<values.lenght;i++){
    process(values[i]);
}
//将终止条件从values.length的O(n)调用简化成了0的O(1)调用
for(let i=values.length -1; i>=0; i--){
    process(values[i]);
}
//再进行改造成后测试循环,此处主要的优化是将终止条件和自减操作符组合成了单个语句
let i = values.length - 1;
if(i> -1){
    do{
        process(valeus[i]);
    }while(--i>=0)
}

最小语句数

1、多个声明变量

//bad
let count = 5;
let color = 'blue';
let values = [1,2,3];
let now = new Date();
//good
let count = 5,
    color = 'blue',
    values = [1,2,3],
    now = new Date();

2、插入迭代值

//bad
let name = values[i];
i++;
//good
let name = values[i++];

3、使用数组和对象字面量

//bad
let values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//bad
let person = new Object();
person.name = 'Tony';
person.age = 18;
person.sayName = function () {
    alert(this.name);
}

//good
let values = [123,456,789];
let person = {
    name: 'Tony',
    age: 18,
    sayName: function () {
        alert(this.name);
    }
}

优化DOM交互

1、最小现场更新

//bad
let list = document.getElementById('myList'),
    item,
    i;
for(i=0; i<10; i++){
    item = document.createElement('li');
    list = appendChilde(item);
    item.appendChild(document.createTextNode('Item' + i);
}

上述代码为列表添加了10个项目,每添加个项目时,都有2个现场更新:一个添加<li>元素,另一个给他添加文本节点,这样添加10个项目,这个操作总共要完成20个现场更新。
解决方法:
1、将列表从页面上移除,最后进行更新,最后在将列表插回到同样的位置,看似很美好,但这样做会导致在每次更新的时候它会不必要的闪烁
2、使用文档碎片来构建DOM结构,接着将其添加到list元素中,这个方式避免了闪烁,如下:

//good,只有一次现场更新
let list = document.getElementById('myList');
    framgent = document.createDocumentFragment(),
    itme,
    i;
for(i=0; i<10; i++){
    item = document.createElement('li');
    fragment.appendChild(item);
    item.appendChild(document.createTextNode('Item' + i));
}
list.appendChild(fragment);

2、使用innerHTML
有两种方式在页面上创建DOM节点:使用诸如createElement()appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM而言,两者效率差不多,对于大的DOM改动,使用innerHTML要快得多。
原因:当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于jsDOM调用,由于内部方法是编译好的而非解释执行的,所以执行快得多,所以上面例子还可以优化

let list = document.getElementById('myList'),
    html = '',
    i;
for(i=0; i<10; i++){
    html += `<li>Item${i}</li>`;
}    
list.innerHTML = html;
//tip同样要避免多次调用innerHTML,即要做到构建好一个字符串然后一次性调用innerHTML

使用事件代理

页面上的事件处理程序的数量和页面的响应用户交互的速度之间是负相关的
任何冒泡的事件都不仅仅可以在事件的目标上进行处理,目标的任何祖先节点上也能处理,如果可能,在文档级别附加事件处理程序,这样就可以处理整个页面的事件

注意HTMLCollection

任何时候要访问HTMLCollection,不管是一个属性还是一个方法,都是在文档上进行的一个查询,这个查询开销很昂贵,尔等消费不起

let images = document.getElementsByTagName('img');
    imags,
    i,
    len;
for(i=0; len=images.length; i<len; i++){
    image = images[i];      //这里保存了当前的image,在这之后就无须再访问image的HTMLCollection了
}

tip:发生以下情况会返回HTMLCollection对象:
(1)、进行了对getElementsByTagName()的调用;
(2)、获取了元素的childNodes属性;
(3)、获取了元素的attribute属性;
(4)、访问了特殊的集合,如document.forms、document.images

展开循环(Duff)

当循环的次数是确定的,消除循环并使用多次函数调用往往更快,

//low
for(let i=0;i<3;i++){
    process(values[i]);
}
//fast,消除建立循环和处理终止条件的额外开销
process(values[0]);
process(values[1]);
process(values[2]);

如果迭代次数事项不能确定,可以使用Duff装置的技术
Duff:通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句

let iterations = Math.ceil(values.length / 8); //向上取整确保结果是整数
let startAt = values.length % 8; //获取无法通过上面进行处理的项,如果values.length为10,那么startAt为2
let i = 0;

do {
    switch(startAt) {
        case 0: process(values[i++]);
        case 7: process(values[i++]);
        case 6: process(values[i++]);
        case 5: process(values[i++]);
        case 4: process(values[i++]);
        case 3: process(values[i++]);
        case 2: process(values[i++]);
        case 1: process(values[i++]);
    }
    startAt = 0;
} while (--iterations > 0);

上面运行的结果是先运行startAt次的process(),然后startAt设置为0,后面循环都执行8次process(),展开循环可以提升大数据集的处理速度。
do-while循环分成2个单独的循环可以让Duff更快

let iterations = Math.ceil(values.length / 8); //向上取整确保结果是整数
let startAt = values.length % 8; //获取无法通过上面进行处理的项,如果values.length为10,那么startAt为2
let i = 0;

if(leftover > 0){           
    do{
        process(values[i++]);
    } while (--leftover > 0);
}
do {
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
    process(values[i++]);
} while (--iterations > 0);

上面代码让剩余的计算部分不会在实际循环中处理,而是在一个初始化循环中进行除以8的操作,当处理掉了额外的元素,继续执行每次调用8次process()的主循环,这个方法几乎比原始的Duff装置快40%
tip:以上只适用于处理大数据集

其他性能优化注意事项

下面并非主要影响性能的主要问题,应用得当,会有相当大的提升
1、原生方法较快——只要有可能,使用原生方法而不是自己用js重写一个,因为原生方法是用注入C/C++之类的编译型语言写出来的,所以要比js快得多,比如要用Math对象的数学运算
2、Swithc语句较快——如果有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码,还可以通过将case语句按照最可以能的到最不可能的顺序进行组织,来进一步优化switch语句。
3、位运算符较快——当进行数学运算时,位运算符操作要比任何布尔运算或者算数运算快,诸如取模,逻辑与和逻辑或都可以考虑用位运算替换

以上内容参考自《JavaScript 高级程序设计(第三版)》
终于写完了(~ ̄▽ ̄)~

相关推荐