前言
之前一段时间一直在进行旧项目的重构,发现了很多问题,系统用的库是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')
测试结果是相同时间内 选择器的执行次数:
可以很明显的看到,一般来说,新版本比旧版本有显著的性能提升。
用好选择器
首先列举几个常用的选择器:
$('#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-...
还是先看一下测试数据:
可以非常直观的看到,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') 快很多。
再来看一个例子:
$('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...
以上 :-)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。