jQuery 那些容易被忽略的问题
伴随Vue
、Angular
、React
等编译式前端框架的崛起,前端开发人员逐渐从繁琐的
DOM
操作当中解脱出来。但是在项目实践过程当中,依然存有诸多问题需要通过直接操作
DOM 来解决,虽然现代化浏览器已经支持selectAll()
等 HTML5
新特性,但是针对一些强调页面兼容性的场景,为了屏蔽各款浏览器解析引擎所遵循规范的差异,依然需要借助于jQuery
来完成
DOM 文档操作。
因此jQuery
这款诞生于 2006 年的 JavaScript
库,依然在现代化前端开发当中扮演着较为重要角色。本文结合笔者
Web
前端开发工作当中积累的实践经验,较为系统的总结了jQuery
实践过程当中一些比较容易被开发人员所忽略的问题。例如
jQuery 对象与 DOM 对象的相互转换、jQuery
选择器性能、异步对象$.Deferred()
以及 JavaScript
性能优化相关的话题。
jQuery 对象和 DOM 对象
- DOM 对象:通过原生
javascript(如
getElementsByTagName
或getElementId
)获取的 html 节点。
1 | var dom = document.getElementById("app"); // 获取DOM对象 |
- jQuery 对象:被 jQuery 包装过的 DOM 对象。
1 | var jq = $("#app"); // 获取jQuery对象 |
不能交换使用 jQuery 对象、DOM 对象上的属性,例如上面代码中的
innerHTML
与html()
。
jQuery 对象->DOM 对象
jQuery 提供了 2 种转换 DOM 对象的方法:
(1)jQuery
对象是一个类似于数组的对象,因此可以通过数组运算符[]
获取指定索引的
DOM 对象。
1 | var jq = $(".test"); // jQuery对象 |
(2)通过 jQuery 本身提供的get(index)
方法来获取指定
index 的对象。
1 | var jq = $(".test"); // jQuery对象 |
DOM 对象->jQuery 对象
将 DOM 对象通过$()
函数包装起来,就可以获得 jQuery
对象。
1 | var dom = document.getElementById("test"); |
可以考虑使用$作为前缀为 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 | var $jq = $("#app"); |
减少循环时的 DOM 操作
在for
、while
、$.each()
等循环语句中,尽量减少
DOM 操作的次数,最好先将模板在循环中组装完成之后再一次性插入 DOM。
1 | var $app = $("#app"); |
使用原生方式处理 jQuery 数组
jQuery
选择器的结果是一个数组类型的对象,建议使用for
或while
等原生语法对其进行处理,而非
jQuery 封装过的$.each()
。
1 | <body> |
1 | var $demo = $(".demo"); |
可以通过关键字length
检查数组长度,从而判断 jQuery
对象是否存在。
1 | var $demo = $(".demo"); |
尽可能使用事件委托
jQuery3.x
版本继续简化了事件委托函数,仅剩下on()
、off()
、one()
、trigger()
、triggerHandler()
五个事件处理函数。
1 | <div id="app"> |
1 | /* on可以用于处理冒泡事件,但是无法捕获事件 */ |
单页面场景下绑定的
on
事件,必须在路由切换时通过off
解除事件绑定,否则会造成大量无用的事件句柄堆积在内存。
通过 extend()封装可复用代码
jQuery.extend()
:拓展全局对象$,例如下面例子中的$.test()
,$.demo()
;jQuery.fn.extend()
:拓展jQuery 对象数组,例如下面例子中的$("div").test()
,$("div").demo()
;
1 | (function ($) { |
两种方式的最大区别在于自定义方法所属的宿主对象不同。
使用 HTML5 的 data 属性绑定数据
通过 HTML5 提供的 data
属性可以更加方便的完成数据绑定,特别是在不借助handlebar
、lodash.template()
等模板引擎的时候。
1 | <div |
1 | $("#app").data("number"); |
尽量使用原生 JavaScript
在不影响浏览器兼容性的情况下,尽量使用 JavaScript 原生 API。
1 | var $demo = $(".demo"); // 缓存选择器 |
$(document).ready()
该函数内的代码会在 DOM 加载完毕后,内容(如图片)加载完成前执行;生产环境中,尽可能在每个 js 文件下使用该函数。
1 | $(document).ready(function () { |
JavaScript 原生的
window.onload()
只会在 DOM 和图片等资源全部加载完成之后才执行。
延迟对象$.Deferred()
Deferred()
是一个工厂函数,用来建立新的 deferred
对象(deferred [dɪ'fɜ:d]
adj.延缓的),该对象上可以注册多个回调函数队列,这些函数的执行依赖于任意同步或异步函数的执行结果(sucess
或failure
)。该对象可以视为
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 对象resolved、rejected、progress时,都会被触发的回调函数。deferred.promise()
返回一个延迟的 Promise 对象。&.when()
提供一种基于零个或多个 Thenable 对象执行回调函数的方式,其参数是一个代表异步事件的 Deferred 对象。$("selector").promise()
返回一个 Promise 对象去观察所有绑定到集合、队列的确定类型行为是否已经完成。
1 | var deferred = $.Deferred(); |
$.ajax()
返回的就是一个 deferred 对象。
jQuery 那些容易被忽略的问题