jQuery 那些容易被忽略的问题

伴随VueAngularReact等编译式前端框架的崛起,前端开发人员逐渐从繁琐的 DOM 操作当中解脱出来。但是在项目实践过程当中,依然存有诸多问题需要通过直接操作 DOM 来解决,虽然现代化浏览器已经支持selectAll()等 HTML5 新特性,但是针对一些强调页面兼容性的场景,为了屏蔽各款浏览器解析引擎所遵循规范的差异,依然需要借助于jQuery来完成 DOM 文档操作。

因此jQuery这款诞生于 2006 年的 JavaScript 库,依然在现代化前端开发当中扮演着较为重要角色。本文结合笔者 Web 前端开发工作当中积累的实践经验,较为系统的总结了jQuery实践过程当中一些比较容易被开发人员所忽略的问题。例如 jQuery 对象与 DOM 对象的相互转换、jQuery 选择器性能、异步对象$.Deferred()以及 JavaScript 性能优化相关的话题。

jQuery 对象和 DOM 对象

  • DOM 对象:通过原生 javascript(如getElementsByTagNamegetElementId)获取的 html 节点。
1
2
var dom = document.getElementById("app"); // 获取DOM对象
var html = dom.innerHTML; // 获取DOM元素内的HTML代码

  • jQuery 对象:被 jQuery 包装过的 DOM 对象。
1
2
var jq = $("#app"); // 获取jQuery对象
var html = jq.html(); // 获取jQuery对象内的HTML代码

不能交换使用 jQuery 对象、DOM 对象上的属性,例如上面代码中的innerHTMLhtml()

jQuery 对象->DOM 对象

jQuery 提供了 2 种转换 DOM 对象的方法:

(1)jQuery 对象是一个类似于数组的对象,因此可以通过数组运算符[]获取指定索引的 DOM 对象。

1
2
var jq = $(".test"); // jQuery对象
var dom = jq[0]; // DOM对象

(2)通过 jQuery 本身提供的get(index)方法来获取指定 index 的对象。

1
2
var jq = $(".test"); // jQuery对象
var dom = jq.get(0); // DOM对象

DOM 对象->jQuery 对象

将 DOM 对象通过$()函数包装起来,就可以获得 jQuery 对象。

1
2
var dom = document.getElementById("test");
var jq = $(dom);

可以考虑使用$作为前缀为 jQuery 对象进行命名,例如上面代码中的变量jq可以命名为$jq

jQuery 选择器性能

提升选择器性能的有效途径是为选择器指定上下文,并以上下文为基础使用first()last()find()filter()hasClass()等 jQuery 筛选 API。

下面对 jQuery 选择器的性能由高向低进行排序:

1、ID 选择器

底层通过调用document.getElementById()实现。

1
$("#id");

2、标签选择器

底层通过调用document.getElementsByTagName()实现。

1
$("input");

3、类选择器

底层通过调用document.getElementsByClassName()实现。

1
$(".class");

4、属性及其它选择器

底层通过对 HTML 字符串进行正则表达式匹配来实现。

1
$("[contenteditable]");

jQuery 底层有使用原生document.querySelectorAll(),可以有效提升 IE8 及以上浏览器当中选择器的性能。

jQuery 作者已经将选择器引擎独立为Sizzle库,而 Sizzle 会按照从右到左的顺序来解析选择器字符串,从而提高查询效率,缩小查找范围和遍历次数。

缓存常用的 jQuery 选择器对象

将需要常用的 jQuery 选择器对象赋值给一个局部或全局变量,是有效提升 jQuery 运行性能的良好开端。

1
2
3
4
var $jq = $("#app");
$jq.find("div.tree").append("string");
$jq.find("[contenteditable].test").html();
$jq.find(".demo.test").html();

减少循环时的 DOM 操作

forwhile$.each()等循环语句中,尽量减少 DOM 操作的次数,最好先将模板在循环中组装完成之后再一次性插入 DOM。

1
2
3
4
5
6
var $app = $("#app");
var template = "";
for (var i = 0; i <= 100; i++) {
template += "<p>This is a paragraph!<p>";
}
$app.find("div").html(template);

使用原生方式处理 jQuery 数组

jQuery 选择器的结果是一个数组类型的对象,建议使用forwhile等原生语法对其进行处理,而非 jQuery 封装过的$.each()

1
2
3
4
5
6
7
8
9
<body>
<div id="app">
<div class="demo">1</div>
<div class="demo">2</div>
<div class="demo">3</div>
<div class="demo">4</div>
<div class="demo">5</div>
</div>
</body>
1
2
3
4
5
var $demo = $(".demo");
for (var i = 0; i < $demo.length; i++) {
// 通过数组索引提取后,jQuery对象转换成DOM对象,所以这里使用了DOM对象的innerHTML属性
console.info($demo[i].innerHTML);
}

可以通过关键字length检查数组长度,从而判断 jQuery 对象是否存在。

1
2
3
4
var $demo = $(".demo");
if ($demo.length !== 0) {
// do something
}

尽可能使用事件委托

jQuery3.x 版本继续简化了事件委托函数,仅剩下on()off()one()trigger()triggerHandler()五个事件处理函数。

1
2
3
4
5
6
7
8
9
<div id="app">
<div id="parent">
<p>parent</p>
<div id="current">
<p>current</p>
<div id="child">child</div>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
/* on可以用于处理冒泡事件,但是无法捕获事件 */
$("#current").on("click", function () {
console.info("current is clicked!");
});

$("#child").trigger("click"); // on可以处理向上冒泡的click事件

$("#parent").trigger("click"); // on无法捕获父级元素的click事件

单页面场景下绑定的on事件,必须在路由切换时通过off解除事件绑定,否则会造成大量无用的事件句柄堆积在内存。

通过 extend()封装可复用代码

  • jQuery.extend():拓展全局对象$,例如下面例子中的$.test()$.demo();

  • jQuery.fn.extend():拓展jQuery 对象数组,例如下面例子中的$("div").test()$("div").demo();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function ($) {
/* 标准写法 */
$.extend({
test: function () {
alert("$.extend");
},
});
/* 简便写法 */
$.demo = function () {
// 返回全局对象$,便于链式调用
return this;
};
/* 标准写法 */
$.fn.extend({
test: function () {
alert("$.fn.extend");
},
});
/* 简便写法 */
$.fn.demo = function () {
// 返回jQuery对象数组,便于链式调用
return this;
};
})(jQuery);

两种方式的最大区别在于自定义方法所属的宿主对象不同。

使用 HTML5 的 data 属性绑定数据

通过 HTML5 提供的 data 属性可以更加方便的完成数据绑定,特别是在不借助handlebarlodash.template()等模板引擎的时候。

1
2
3
4
5
6
7
8
9
10
<div
id="app"
data-number="13.2"
data-string="uinika"
data-boolean="true"
data-null="null"
data-undefine="undefine"
data-object='{"name": "hank"}'
data-array='["demo1", "demo2"]'
></div>
1
2
3
4
5
6
7
$("#app").data("number");
$("#app").data("string");
$("#app").data("boolean");
$("#app").data("null");
$("#app").data("undefine");
$("#app").data("object").name;
$("#app").data("array")[1];

尽量使用原生 JavaScript

在不影响浏览器兼容性的情况下,尽量使用 JavaScript 原生 API。

1
2
3
4
5
6
7
8
9
10
var $demo = $(".demo"); // 缓存选择器

$demo.is(":checked"); // 通过jQuery封装过的is()方法
$demo[0].checked; // 通过DOM原生的checked属性

$demo.css({ color: "red" }); // 通过jQuery封装过的css()方法
$demo[0].style.color = "red"; // 通过DOM原生的style.color属性

$("<p></p>"); // 通过jQuery新建<p>标签
$(document.createElement("p")); // 通过DOM原生的createElement方法

$(document).ready()

该函数内的代码会在 DOM 加载完毕后,内容(如图片)加载完成前执行;生产环境中,尽可能在每个 js 文件下使用该函数。

1
2
3
4
5
6
7
$(document).ready(function () {
// 标准写法
});

$(function () {
// 简化写法
});

JavaScript 原生的window.onload()只会在 DOM 和图片等资源全部加载完成之后才执行。

延迟对象$.Deferred()

Deferred()是一个工厂函数,用来建立新的 deferred 对象(deferred [dɪ'fɜ:d] adj.延缓的),该对象上可以注册多个回调函数队列,这些函数的执行依赖于任意同步或异步函数的执行结果(sucessfailure)。该对象可以视为 jQuery 版本的Promise实现,可以更加优雅的解决 JavaScript 回调嵌套的问题。

jQuery 的 Deferred 对象是基于CommonJS Promises/A规范设计的。

  • deferred.notify() 触发 Deferred 上progress相关的回调函数。

  • deferred.resolve() Resolve 一个 Deferred 对象,并触发resolve状态相关的回调函数。

  • deferred.reject() Reject 一个 Deferred 对象,并触发reject状态相关的回调函数。

  • deferred.progress() 该函数在 Deferred 对象生成progress通知时被调用。

  • deferred.done() 该函数在 Deferred 对象被resolve时调用。

  • deferred.fail() 该函数在 Deferred 对象被rejecte时调用。

  • deferred.catch() 该函数在 Deferred 对象被rejecte时调用。

  • deferred.then() Deferred 对象resolvedrejectedprogress时,都会被触发的回调函数。

  • deferred.promise() 返回一个延迟的 Promise 对象。

  • &.when() 提供一种基于零个或多个 Thenable 对象执行回调函数的方式,其参数是一个代表异步事件的 Deferred 对象。

  • $("selector").promise() 返回一个 Promise 对象去观察所有绑定到集合、队列的确定类型行为是否已经完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var deferred = $.Deferred();

var demo = function (deferred) {
var task = function () {
deferred.resolve();
// deferred.notify();
// deferred.reject();
};
setTimeout(task, 2000);
return deferred;
};

$.when(demo(deferred))
.progress(function () {
console.info("progress");
})
.then(function () {
console.info("then");
})
.done(function () {
console.info("done");
})
.fail(function () {
console.info("fail");
})
.catch(function () {
console.info("catch");
});

/*
输出结果:
then
done
*/

$.ajax()返回的就是一个 deferred 对象。

jQuery 那些容易被忽略的问题

http://www.uinio.com/Web/JQuery/

作者

Hank

发布于

2010-08-07

更新于

2011-12-31

许可协议