前言

之前一段时间一直在进行旧项目的重构,发现了很多问题,系统用的库是jQuery, 主要是用着方便,其他同事,包括一些外包同事,对这个库也比较熟悉 。这里就对一些常见的问题,结合查到的资料,进行一些简单的总结。

为什么使用jQuery

这个问题,答案是显而易见的。还能因为啥,当然是方便呗。没错,它本身就是一个为dom而生的库。
jQuery的思想,实际上很简单,八个字可以概括: 选择元素,进行操作。

各类强大的选择器极大的简化了dom操作,我们最常使用的也是它的选择器。
下面就对经常使用的一些功能,给出一些建议。

使用最新版本

https://jsperf.com/jquery-1-4...
新版本会改进性能,还有很多新功能。

为了直观的看到版本对性能的影响,我们来看一些测试用例:

以三个常见的选择器为例:

<div class="wrapper">
     <div class="innerWrapper">
          <div class="button">
               <a href="" id="button" class="buttonRef">
                    <span class="buttonText">Text</span>
               </a>
          </div>
     </div>
</div>

$('.elem')
$('.elem', context)
context.find('.elem')

测试结果是相同时间内 选择器的执行次数:

clipboard.png

clipboard.png

可以很明显的看到,一般来说,新版本比旧版本有显著的性能提升。

用好选择器

首先列举几个常用的选择器:

$('#xxx') // ID选择器 
$('.xxx') // 类选择器
$(input)  // 标签选择器
$(':xxx') // 伪类选择器 比如 $(':hidden')
$('[attribute = value]') //属性选择器

使用上面几个选择器 来进行测试:

<div id="test"> 
        <div class="testlink">
                <a href="#" id="linkone" title="test">test link</a>
        </div> 
        <input type="text" val="test"/>
        <div class="testmenu">
                <ul>
                        <li class="menuitem itemone">
                                <a href="#" title="item 1">menu item 1</a>
                        </li>
                        <li class="menuitem itemtwo">
                                <a href="#" title="item 2">menu item 2</a>
                        </li>
                        <li class="menuitem itemthree">
                                not clickable item 3
                        </li>
                </ul>
        </div>
</div>

https://jsperf.com/dh-jquery-...
还是先看一下测试数据:

clipboard.png

clipboard.png

可以非常直观的看到,ID选择器的速度最快,远超其他选择器。

根据以上信息,简单的总结一下:


 1. ID选择符应该是唯一的,不需要添加额外的选择符。 

   不推荐的: $('div#id'), $('#a #id'), $('div#id span.class') 
   正确的姿势: $('#id') ,$('#id .class')
   
 2. 避免使用隐式通用选择符

   不推荐的:  $('.class :radio')
   正确的姿势: $('.class input:radio')
   
 3. 避免使用通用选择器`*` , 性能很差。
  
  不推荐的: $('.container > *') 
  正确的姿势: $('.container').children()

遵循以上规则, 选择器的使用上基本就没什么问题了,之后,我们还可以继续优化。

利用缓存

频繁操作dom浪费性能,所以我们可以把重复使用的的元素缓存起来。

    
   // 在变量前加$前缀,便于识别出jQuery对象 

   var $a =  $('#a'),
       $container =  $('#container'),
       $container_item = $container.find('li'), 
                       // 利用缓存,比 $('#container li') 好一些
       // ...
  
   //如果你想更方便的管理,也可以使用对象字面量的方式:
   
   var DOM = {
        $a : $('#a'),
        $container :  $('#container'),
        $container_item : $container.find('li'), 
        // ...
        
} 

后面使用的时候,就直接可以使用我们定义的DOM对象了。
DOM.$a.on('click',function(){
    //xxx
})

选择器也好了,下面我们看dom的操作。

链式操作

采用链式写法,jQuery自动缓存每一步的结果,因此比非链式写法要快。


$('div').find('span').eq(2).html('Hello World');
        
// 如果操作很多,会导致链很长,这时可以添加适当的换行来 增加可读性 :

$('div')
       .find('span')
       .eq(2)
       .html('Hello World');
       
同样适用于一些样式的操作:

$target.on('click',function(){
    $target.css({
        'border':'1px solid #ddd',
        'color':'#fff',
         // ...
    })
    
})
       

非空判断

如果要进行非空判断 或者 未定义判断的时候 使用 === 而不要使用==.

如果你使用jsHint ,会有这样的提示:

     // Use '===' to compare with 'null'
     // Use '===' to compare with ''
     // Use '===' to compare with 'undefined'
     
    在分支判断的时候, 可以直接利用其本身的含义就可以了:
    
    if ( $something !== null) {
        // xxx
    }
    
    // 更好的方式:
     if( something) {
         // xxx
     }
     
     同样的,还有其他类似的判断,比如长度。
     
     if(list.length > 0 ){
         
         // xxx
     }
     
     就可以写成 
     
     if(list.length){
         // xxx
     }

事件的委托处理

javascript的事件模型,采用"冒泡"模式,也就是说,子元素的事件会逐级向上"冒泡",成为父元素的事件。利用这一点,可以大大简化事件的绑定。

比如,有一个表格(table元素),里面有100个格子(td元素),现在要求在每个格子上面绑定一个点击事件(click):


$("td").on("click", function(){
    $(this).toggleClass("active");
  });

这种做法是不推荐的,,因为td元素发生点击事件之后,这个事件会"冒泡"到父元素table上面,从而被监听到。所以,这个事件只需要在父元素绑定1次即可。

$("table").on("click", "td", function(){
    $(this).toggleClass("active");
  });

//更好的写法,则是把事件绑定在document对象上面。
$(document).on("click", "td", function(){
    $(this).toggleClass("active");
  });

如果要取消事件的绑定,就使用off()方法。

$(document).off("click", "td");

使用'on'

在新版jQuery中,更短的 on(“click”) 用来取代类似 click() 这样的函数。
在之前的版本中 on() 就是 bind()。
自从jQuery 1.7版本后,on() 附加事件处理程序的首选方法。
然而,出于一致性考虑,可以简单的全部使用 on()方法。
    
$('#id').click(function(){
    //xxx
});

替换为:

$('#id').on('click', function(){
    //xxx
})

使用短路求值

短路求值是一个从左到右求值的表达式,用 && 或 || 操作符。

    
if(!$value){
    $value = $('#id').val();
}

可以写成:

$value = $value || $('#id').val();    

//也可以用 || 来填充默认值 
var age = myAge || 18 ;

避免过度使用jQuery

每当你使用一次选择器(比如$('#id')),就会生成一个jQuery对象。jQuery对象是一个很庞大的对象,带有很多属性和方法,会占用不少资源。所以,尽量少生成jQuery对象。

以最简单的选择器为例:

document.getElementById("foo") 就要比 $('#foo') 快很多。

clipboard.png

clipboard.png

再来看一个例子:


$('a').on ('click' ,function(){
  alert($(this).attr('id'));
})

//点击a元素后,弹出该元素的id属性。
//为了获取这个属性,必须连续两次调用jQuery,第一次是$(this),第二次是attr('id')。
 
//事实上,这种处理完全不必要。更正确的写法是,直接采用javascript原生方法,调用this.id:

$('a').on ('click' ,function(){
 alert(this.id);
})

上面简单说了一些简单的优化写法,我们再看看如何更好的组织你的逻辑

逻辑组织

如果遇到抄起键盘就开干的小伙伴,没有组织逻辑,那写的代码可能是这样的:



var a = xxx;
var b = xxx;

$('#id').on('click', function(){
    // xxx
})

$('.class').on('click', function(){
    // xxx
})

function xxx() {
    // xxx
}


// 直到最后

如果整个页面页面的逻辑比较复杂,那到最后的代码,估计只有你自己清楚是怎么走的,别人要去看,要不断的查找你的写的各种方法,简直痛苦。

这里就说一种 或许是更好的方式。

下面就给个实际的例子吧:

/**
 * Created by jf on 2015/9/11.
 * Modified by bear on 2016/9/7.
 */
$(function () {
    var pageManager = {
        $container: $('#container'),
        _pageStack: [],
        _configs: [],
        _pageAppend: function(){},
        _defaultPage: null,
        _pageIndex: 1,
        setDefault: function (defaultPage) {
            this._defaultPage = this._find('name', defaultPage);
            return this;
        },
        setPageAppend: function (pageAppend) {
            this._pageAppend = pageAppend;
            return this;
        },
        init: function () {
            var self = this;

            $(window).on('hashchange', function () {
                var state = history.state || {};
                var url = location.hash.indexOf('#') === 0 ? location.hash : '#';
                var page = self._find('url', url) || self._defaultPage;
                if (state._pageIndex <= self._pageIndex || self._findInStack(url)) {
                    self._back(page);
                } else {
                    self._go(page);
                }
            });

            if (history.state && history.state._pageIndex) {
                this._pageIndex = history.state._pageIndex;
            }

            this._pageIndex--;

            var url = location.hash.indexOf('#') === 0 ? location.hash : '#';
            var page = self._find('url', url) || self._defaultPage;
            this._go(page);
            return this;
        },
        push: function (config) {
            this._configs.push(config);
            return this;
        },
        go: function (to) {
            var config = this._find('name', to);
            if (!config) {
                return;
            }
            location.hash = config.url;
        },
        _go: function (config) {
            this._pageIndex ++;

            history.replaceState && history.replaceState({_pageIndex: this._pageIndex}, '', location.href);

            var html = $(config.template).html();
            var $html = $(html).addClass('slideIn').addClass(config.name);
            $html.on('animationend webkitAnimationEnd', function(){
                $html.removeClass('slideIn').addClass('js_show');
            });
            this.$container.append($html);
            this._pageAppend.call(this, $html);
            this._pageStack.push({
                config: config,
                dom: $html
            });

            if (!config.isBind) {
                this._bind(config);
            }

            return this;
        },
        back: function () {
            history.back();
        },
        _back: function (config) {
            this._pageIndex --;

            var stack = this._pageStack.pop();
            if (!stack) {
                return;
            }

            var url = location.hash.indexOf('#') === 0 ? location.hash : '#';
            var found = this._findInStack(url);
            if (!found) {
                var html = $(config.template).html();
                var $html = $(html).addClass('js_show').addClass(config.name);
                $html.insertBefore(stack.dom);

                if (!config.isBind) {
                    this._bind(config);
                }

                this._pageStack.push({
                    config: config,
                    dom: $html
                });
            }

            stack.dom.addClass('slideOut').on('animationend webkitAnimationEnd', function () {
                stack.dom.remove();
            });

            return this;
        },
        _findInStack: function (url) {
            var found = null;
            for(var i = 0, len = this._pageStack.length; i < len; i++){
                var stack = this._pageStack[i];
                if (stack.config.url === url) {
                    found = stack;
                    break;
                }
            }
            return found;
        },
        _find: function (key, value) {
            var page = null;
            for (var i = 0, len = this._configs.length; i < len; i++) {
                if (this._configs[i][key] === value) {
                    page = this._configs[i];
                    break;
                }
            }
            return page;
        },
        _bind: function (page) {
            var events = page.events || {};
            for (var t in events) {
                for (var type in events[t]) {
                    this.$container.on(type, t, events[t][type]);
                }
            }
            page.isBind = true;
        }
    };

    function fastClick(){
        var supportTouch = function(){
            try {
                document.createEvent("TouchEvent");
                return true;
            } catch (e) {
                return false;
            }
        }();
        var _old$On = $.fn.on;

        $.fn.on = function(){
            if(/click/.test(arguments[0]) && typeof arguments[1] == 'function' && supportTouch){ // 只扩展支持touch的当前元素的click事件
                var touchStartY, callback = arguments[1];
                _old$On.apply(this, ['touchstart', function(e){
                    touchStartY = e.changedTouches[0].clientY;
                }]);
                _old$On.apply(this, ['touchend', function(e){
                    if (Math.abs(e.changedTouches[0].clientY - touchStartY) > 10) return;

                    e.preventDefault();
                    callback.apply(this, [e]);
                }]);
            }else{
                _old$On.apply(this, arguments);
            }
            return this;
        };
    }

    function preload(){
        $(window).on("load", function(){
            var imgList = [
                "./images/layers/content.png",
                "./images/layers/navigation.png",
                "./images/layers/popout.png",
                "./images/layers/transparent.gif"
            ];
            for (var i = 0, len = imgList.length; i < len; ++i) {
                new Image().src = imgList[i];
            }
        });
    }

    function androidInputBugFix(){
        // .container 设置了 overflow 属性, 导致 Android 手机下输入框获取焦点时, 输入法挡住输入框的 bug
        // 相关 issue: https://github.com/weui/weui/issues/15
        // 解决方法:
        // 0. .container 去掉 overflow 属性, 但此 demo 下会引发别的问题
        // 1. 参考 http://stackoverflow.com/questions/23757345/android-does-not-correctly-scroll-on-input-focus-if-not-body-element
        //    Android 手机下, input 或 textarea 元素聚焦时, 主动滚一把
        if (/Android/gi.test(navigator.userAgent)) {
            window.addEventListener('resize', function () {
                if (document.activeElement.tagName == 'INPUT' || document.activeElement.tagName == 'TEXTAREA') {
                    window.setTimeout(function () {
                        document.activeElement.scrollIntoViewIfNeeded();
                    }, 0);
                }
            })
        }
    }



    function init(){
        preload();
        fastClick();
        androidInputBugFix();
    
        window.pageManager = pageManager;
        window.home = function(){
            location.hash = '';
        };
    }
    init();
});

如果想使用另一种更直观的方式,可以接着往下看。

另一种组织方式


var Module = function() {
  this.init();
};

// 初始化
Module.prototype.init = function() {
  this.fetchData(function() {
    // do something
  });
};

// 绑定事件
Module.prototype.bindEvent = function() {
  // ...
};

// 获取数据
Module.prototype.fetchData = function(cb) {
  var self = this;
  ajax({}).then(function(data) {
    self.renderData(data);
  }).catch(function() {
    self._fetchDataFailed();
  }).fin(function() {
    cb && cb();
  });
};

// 渲染数据
Module.prototype.renderData = function(data) {
  data = this._resolveData(data);
  // ...
  this.bindEvent();
};

// 处理数据
Module.prototype._resolveData = function() {
  // ...
};

// 加载失败
Module.prototype._fetchDataFailed = function() {
  // ...
};

当然,你也可以将这两种形式混合起来用,具体怎么用,就看个人习惯了。

这里仅仅介绍一些方法,抛砖引玉。如果你觉得这种方式不好,也可以提出来,以供大家参考学习。

结语

啰哩啰嗦 终于到头了..
类似的这种博文,简单搜一下就有很多,各种 指南 ,最佳实践。我这干脆就简单起个小结吧。

说到底,这篇文字就当是对前几天做的事情的一个简单总结,结合其他资料, 写出来的一点东西。

回头看看 其实也没什么。

查资料,写出来 的过程其实也是一个自我学习的过程,这大概就是写博客的意义所在吧。
不多说了,还有很多需求要做,都排到四月去了 (容我做个悲伤的表情)。就到这里吧。

如果想了解更多类似的内容,可以查看这两篇:

https://segmentfault.com/p/12...

http://www.ruanyifeng.com/blo...

以上 :-)


皮小蛋
8k 声望12.8k 粉丝

积跬步,至千里。