ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

jQuery实战笔记:从选择器到DataTables与事件委托

jQuery实战笔记:从选择器到DataTables与事件委托 这两年面试前端偶尔还会遇到候选人问“现在还有必要学 jQuery 吗”我的回答通常是如果你只做新项目确实可以不学但如果哪天你接手了一个 2015 年上线的后台管理系统里面全是$(#xxx).click(...)和$.ajax(...)你就知道 jQuery 有多“顽强”了。我自己这两年维护的老系统里表格清一色是 jQuery DataTables光“单元格内容过长怎么优雅展示”这个需求就做了不下五次。所以这份学习文档不是 API 大全而是我从实际项目里整理出来的操练笔记从选择器到底层原理从事件机制到 Ajax 交互再到 DataTables 的高频实战与调试速查表。适合维护老项目的前端、准备去传统行业面试的开发者以及想弄明白 jQuery 设计思路的框架党。为什么到了现在还要整理一份 jQuery 学习文档1.1 存量项目的真实分布现实层面讲一个系统使用什么技术栈取决于它是什么时候上线、由谁维护。2010 到 2018 年之间上线的那批 Web 系统前端基建基本都是 jQuery Bootstrap 一堆插件比如 DataTables、layer、zTree、select2。这些系统今天还在运行而且每年都在加需求、修 bug。你问为什么不用 React 重写业务逻辑复杂、数据关系拧巴、领导不接受长时间停服重写基本就是一句空话。所以对不少人来说掌握 jQuery 不是“考古”而是“接手”。还有一个容易被忽视的场景WordPress 后台、各种 CMS 的主题开发、部分自动化测试脚本至今仍然依赖 jQuery。哪怕你现在写的是 Vue打开某些系统的登录页看一眼源码里是不是有一行script srcjquery.min.js。只要这行还在你就绕不开它。1.2 学 jQuery 到底在学什么很多人以为学 jQuery 就是背方法名比如hide()、show()、click()其实这些只是最表层的东西。真正值得沉淀的是四件事。第一是选择器思维CSS 选择器拿来就能用本质是帮你在 DOM 树里快速定位节点。第二是链式调用每个修改型方法都返回 jQuery 对象所以可以.addClass().attr().css()一路点下去这个设计思路你在 dayjs、d3 等现代库里还能看到。第三是事件委托.on(click, .item, fn)能解决动态元素的绑定问题React 的合成事件本质上也在做类似的事情。第四是插件封装$.fn.myPlugin function(){}这套扩展机制直到现在还有很多 UI 组件在这么写。1.3 什么时候真的别用它把话说透新项目、强交互、移动端性能敏感的场景别用 jQuery。jQuery 适合的是“页面里有交互但整体还是服务端渲染”的传统 Web 应用。一旦你的业务状态复杂界面需要根据数据频繁变化数据驱动视图的框架才是正解。能够判断“哪儿该用、哪儿不该用”比会写一百个 API 重要得多。我见过有人在新项目里用 jQuery 手搓了一个分页表格代码两百行换到 Vue 里一个组件就完了。选型不是跟风是想清楚成本和收益。选择器体系与“第一个子元素”到底怎么选2.1 $() 的底层逻辑jQuery 的核心入口是$()它本质上是个工厂函数接收选择器、DOM 节点、HTML 字符串、函数四种输入。在早期jQuery 自己实现了一套选择器引擎叫 Sizzle用来兼容低版本浏览器在现代浏览器里它底层就是querySelectorAll。理解这一点有什么好处一是你不用背选择器语法CSS 里怎么写jQuery 里就怎么写二是你心里会有个性能预期——能用 ID 和 class 精确命中就别堆:nth-child(2n1)这种复杂伪类。不过说实话普通业务页面的选择器性能差异感知不强真正影响体验的是大量 DOM 写操作这个后面专门讲。2.2 “第一个子元素”的四种写法这个需求高频出现我要拿到某个列表里第一个 li、表格里第一行、或者某个容器下的第一个子节点。至少四种写法都有人用但语义完全不一样。写法含义典型场景$(ul li:first-child)匹配“作为父元素第一个子节点”的 li只想找真正排行老大的元素中间如果夹着 span 就选不中$(ul li:first)匹配整个结果集合中的第一个 li不关心位置只要第一个命中的元素$(ul li).eq(0)先拿集合再按下标取第 0 个需要配合变量下标做随机访问$(ul li).first()等价于eq(0)的语义化版本链式调用里读起来更顺口比如这段结构ul span提示/span li第一项/li li第二项/li /ul$(ul li:first-child)选中的是“第一项”因为它是第一个 li 子元素如果结构改成下面这样ul li classitem零号/li li classitem一号/li li classitem二号/li /ul那$(.item:first)、$(.item).eq(0)、$(.item).first()结果都指向“零号”但$(.item:first-child)同样指向“零号”因为它恰好是第一个子节点。反过来如果第一个子节点不是 li:first-child就会落空而:first不受影响。这个区别如果不做两个对比 demo很容易踩坑。2.3 筛选和遍历的黄金组合实际业务里选择器通常不是一次性定位就完事而是要在已经选中的集合上继续找。我常用的组合是find()向下找后代closest()向上找最近的匹配祖先包含自身parents()向上找所有祖先filter()对集合做二次过滤not()排除某些项children()只找直接子元素siblings()找兄弟节点next()和prev()找相邻节点。举个例子页面上有卡片结构div classcard idcard-1 div classheader订单标题/div div classbody p classprice¥199/p button classbtn-close关闭卡片/button /div /div点关闭按钮要移除整张卡片正确写法是$(this).closest(.card).remove()。如果你用$(this).parent().parent().remove()一旦中间多包一层 div代码就崩了。而closest()是语义化的“向上找卡片”结构怎么变都不怕。这类遍历方法就是用来应对 DOM 层级变化的理解了之后写起来会顺手很多。2.4 为什么动态添加的元素选不中新手最困惑的问题明明代码写在后面$(.delete-btn).click(...)也绑了事件为什么 Ajax 返回后动态生成的按钮点了没反应原因很简单——事件绑定发生在元素被创建之前你绑了个寂寞。jQuery 遇到选择器匹配不到任何元素时不会报错只是静默地什么都不做。要解决这个问题核心是事件委托把监听器挂到不会消失的祖先节点上$(document).on(click, .delete-btn, function() { console.log(动态按钮也能触发); });原理是事件冒泡子元素的 click 会一路冒泡到 documentjQuery 在冒泡过程中判断事件来源是否匹配.delete-btn匹配就执行回调。这样一来不管元素是渲染时就有还是后来异步加进来的都能被捕捉到。这个认知极其重要后面 DataTables 的所有行内交互基本都靠它。DOM 操作与事件机制3.1 链式调用的真相链式调用是最让初学者感到“魔法”的特性$(#box) .addClass(active) .attr(data-id, 123) .css(color, red) .text(更新成功);为什么能一直点下去因为除了取值方法比如html()不传参数时返回字符串其他方法都返回当前 jQuery 对象。每个方法执行完把 this 原样返回于是下一个方法可以在同一个对象上继续操作。这是 jQuery 的经典设计也影响了后来很多库的 API。理解这个机制后两条实战经验就顺理成章了第一别在链式调用中间搞“取值”——一旦调用了取值方法链就断了后面的.addClass()会报错第二链式太长影响调试想中断看中间状态可以临时插一个.each(function(){ console.log($(this)); })。3.2 text、html、val、attr、prop 的边界要分清这几个方法各有分工用错很常见。text()读写的都是纯文本内部会做转义动态内容插进去是安全的。html()读写的则是 HTML 字符串如果往里拼用户输入必须自己做转义否则有 XSS 风险。val()专门操作表单元素的 valueinput、select、textarea 都靠它。attr()和prop()的坑最多。前者操作的是 HTML 属性后者操作的是 DOM 属性。一个很经典的例子checkbox 的选中状态。用$(#check).attr(checked, true)在某些版本的浏览器里设置之后没办法通过attr(checked, false)取消因为 checked 是 DOM 属性不是静态 HTML 属性。正确做法是$(#check).prop(checked, true); $(#check).prop(checked, false);方法操作对象适用场景典型坑点text()纯文本内容安全展示用户内容无法插入标签html()HTML 字符串渲染富文本未转义会引发 XSSval()表单 valueinput、select、textarea对 div 无效attr()HTML 属性自定义属性>var $box $(#box); $box.text(1); $box.attr(data-x, 1);第二批量操作 DOM。读布局属性比如 width、offsetTop会触发浏览器重新计算布局reflow高频读写会卡页面。往里插入一组数据时别在循环里一次一次append先拼成一个字符串或 DocumentFragment再一次性插进去。我排查过一个列表页打开要 5 秒的问题最后发现就是 for 循环里不断$(#list).append(...)改成先拼字符串再 append页面加载直接降到 1 秒以内。第三小心“强制同步”。代码里先读$(#list).height()又立刻改样式再读一次高度浏览器被迫多次计算布局。最好做法是一次性读完数据再集中修改样式。这个原则在今天的高频交互页面里依然适用。Ajax 与异步交互4.1 $.ajax 的参数逐行拆解老项目里到处是$.ajax()不理解参数的人改起来很容易翻车。拿一个真实表单提交来拆$.ajax({ url: /api/user/save, type: POST, dataType: json, data: $(#user-form).serialize(), timeout: 10000, beforeSend: function() { $(#submit-btn).prop(disabled, true); }, success: function(res) { if (res.code 0) { alert(保存成功); window.location.href /user/list; } else { alert(res.message || 保存失败); } }, error: function(xhr, status, err) { console.log(status, err); alert(网络异常); }, complete: function() { $(#submit-btn).prop(disabled, false); } });这里有几个容易理解错的地方。success只代表 HTTP 请求成功不代表业务成功。很多接口在业务失败时依然返回 HTTP 200只是响应体里带一个错误码所以必须在 success 里判断类似res.code 0的逻辑。error是网络层报错超时、404、500 都会走到这里。complete无论成功失败都会执行适合在这里恢复按钮状态、关掉 loading。beforeSend可以拿到 XMLHttpRequest 实例适合做请求前的统一处理比如加 tokenbeforeSend: function(xhr) { xhr.setRequestHeader(Authorization, Bearer getToken()); }还有一个容易忽略的async: false可以发起同步请求代码看起来“顺了”但会完全阻塞页面浏览器还会提示脚本运行缓慢。除非万不得已比如登录态同步校验否则一律异步。4.2 serialize 与 FormData 的边界$(#form).serialize()可以把表单里的输入框全部变成查询字符串格式比如nameabcage18非常适合普通表单提交。.serializeArray()返回的是对象数组结构更清晰。如果涉及文件上传serialize()就无能为力了必须用 FormDatavar formData new FormData($(#upload-form)[0]); $.ajax({ url: /api/upload, type: POST, data: formData, processData: false, contentType: false, success: function(res) { console.log(res); } });processData: false是为了阻止 jQuery 把 data 自动转成查询字符串contentType: false是让浏览器自动带上 multipart 的 boundary这两项缺一不可否则文件上传会失败。4.3 看懂简写也看清迁移方向$.get()、$.post()、$.getJSON()都是$.ajax()的简写。老代码里看到这些心里要能自动翻译成对应的 ajax 请求。比如$.getJSON(/api/list, function(data){...})就是 GET 请求且期望 JSON 返回。异步回调里另一个常见坑是 this 丢失。比如在某个对象方法里发 Ajax回调里想访问对象自己的属性结果 this 指向了全局。老代码的通用解决办法是提前存一下var self this; $.get(/api/list, function(res) { self.renderList(res); // 用 self 而不是 this });换成现在的写法就是箭头函数本质原理一样闭包捕获外层 this。新项目里我推荐用 fetch 或 axios拦截器统一处理 token 和错误码比 jQuery 的散装回调清晰得多。但维护老项目时请尊重原有风格不要一半 jQuery 一半 axios出问题排查起来更累。DataTables 实战单元格内容过长鼠标悬浮展示全部数据5.1 先还原真实场景后台管理表格里地址、备注、日志详情这类字段经常有几十上百个字符。直接全部展示表格会被撑得乱七八糟用 CSS 截断用户看不到完整内容再加上 DataTables 刷新、排序、分页之后普通的事件绑定会失效。综合下来需求通常是单元格默认截断显示鼠标悬停时展示完整内容。这个需求我做了不止五次每次都能遇到新坑所以单独开一节写透。5.2 方案一纯 CSS 截断 title 原生提示最轻量的做法是让单元格内容单行截断然后用浏览器原生的 title 属性做悬停提示。CSS 部分td.ellipsis { max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }数据渲染时把完整内容放到 title 里$(#table).DataTable({ columns: [ { data: name }, { data: address, render: function(data, type, row) { return div classellipsis title escapeHtml(data) escapeHtml(data) /div; } } ] });这个方案的优点是代码量极小不依赖任何额外插件。缺点是浏览器原生 title 有显示延迟样式不可控长文本不能换行紧急情况下能用但离“体验好”还有距离。另外要注意title 属性里的内容一定要做 HTML 转义否则数据里带了引号属性会提前闭合轻则提示内容残缺重则被注入标签。5.3 方案二DataTables render 自定义悬浮层想完全控制展示效果方案二是更稳的选择。思路分三步第一渲染单元格时把完整内容存在一个>$(document) .on(mouseenter, .cell-ellipsis, function(e) { var $this $(this); // 如果文本没超长就不需要悬浮 if ($this[0].scrollWidth $this[0].clientWidth) { return; } var text $this.attr(data-full) || $this.text(); var $tip $(div classcell-popover/div) .text(text) .css(left, e.pageX 12 px) .css(top, e.pageY 12 px); $(body).append($tip); }) .on(mouseleave, .cell-ellipsis, function() { $(.cell-popover).remove(); }) .on(scroll, function() { $(.cell-popover).remove(); });CSS 部分.cell-ellipsis { max-width: 240px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; cursor: default; } .cell-popover { position: absolute; z-index: 9999; max-width: 400px; background: #fff; border: 1px solid #d9d9d9; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); border-radius: 4px; padding: 8px 10px; font-size: 13px; line-height: 1.6; word-break: break-all; white-space: normal; }为什么要判断scrollWidth clientWidth因为有些数据本来就不长不需要悬浮层提前返回能省掉无意义的 DOM 创建。为什么用.text(text)而不是.html(text)因为悬浮内容来自后端用 text 方法天然把script之类的标签当作文本处理杜绝 XSS。为什么监听 scroll 时移除悬浮层因为表格滚动后原本定位在光标旁的悬浮层会停留在半空位置错乱用户体验很怪。这个方案的优点是完全可控视觉风格可以跟项目 UI 对齐逻辑也容易扩展成点击展示。缺点是首次实现要写一点代码但做完一次就能沉淀成公共函数后续页面直接复用。5.4 方案三项目里已有 Bootstrap用 Tooltip如果你的项目本来就在用 Bootstrap还有更省力的路子让 Bootstrap Tooltip 做委托显示。$(document).tooltip({ selector: .cell-ellipsis, title: function() { return $(this).attr(data-full) || $(this).text(); }, placement: top, html: false });selector选项让 tooltip 对动态渲染的元素也生效。html: false是为了安全默认当纯文本。这个方案代码最少但样式受 Bootstrap 主题限制如果只是内部管理系统完全够用。5.5 移动端和其他容易翻车的细节第一个坑是移动端没有 hover。手机上手指点击不会触发 mouseenter所以要加一个 fallback点击单元格时切换悬浮层显隐。简单做法是在 click 事件里 toggle同时点击其他区域时收起否则悬浮层会一直挂着。第二个坑是渲染性能。列表页如果有几百行每次 mouseenter 都新建和销毁悬浮层会产生大量 DOM 操作。优化方式全局只创建一个悬浮层实例显示时复用替换文本和位置。这样不仅减少卡顿也降低内存占用。第三个坑是中文长文本的换行。英文 URL 和连续数字容易把悬浮层撑破CSS 里要加word-break: break-all;或overflow-wrap: break-word;。中文本身可以正常换行如果混了很多连续英文不处理就会出现横向滚动条。第四个坑是 DataTables 重绘后事件失效。如果不用委托、而是直接用$(.cell-ellipsis).hover(...)绑定分页切换后新行不会绑定事件。解决方式就是我在方案二里写的监听器挂在 document 上事件委托天然免疫重绘问题。这个原则适用于一切 DataTables 行内交互包括“删除”“编辑”“查看详情”。常见问题与调试排查速查表6.1 jQuery 对象和 DOM 对象混用$(#id)返回的不是元素本身而是一个类数组的 jQuery 对象。想拿原生节点两种写法$(#id)[0]或$(#id).get(0)。反过来原生节点想调 jQuery 方法必须包一层$(domNode).val()。最常见的报错场景document.getElementById(name).val(); // TypeError: .val is not a function解决方式要么$(document.getElementById(name)).val()要么直接用$(#name).val()。还有一个容易忽略的点jQuery 对象和 DOM 对象混用时$(#id) document.getElementById(id)永远是 false判断要用$(#id)[0] domNode或者domNode.isSameNode($(#id)[0])。6.2 ready、版本冲突与引入顺序$(function(){ ... })是$(document).ready(...)的简写意思是 DOM 结构加载完成后执行不需要等图片加载完。这在老项目里几乎是标准入口。要注意的是jQuery 文件必须先于业务代码加载如果页面底部才引 jQuery顶部却在执行$()控制台会报$ is not defined。另一个高频问题是多版本冲突。有些老系统同时引入了 jQuery 1.x 和 3.x两个版本的插件互相不买账。可以用jQuery.noConflict()把$的控制权让出来var $j jQuery.noConflict(); $j(#id).text(ok);这样$可能是其他库的而$j明确指当前版本的 jQuery。当然最彻底的解决方案是只保留一个版本但老系统牵一发动全身临时用 noConflict 过渡是常见手段。6.3 each 回调里的 this 与作用域$.each(array, function(i, item){ ... })的回调里this 指向当前遍历的元素。如果是对象数组this 就是当前对象如果是普通数组this 就是当前值。所以下面的写法是等价的$.each(items, function(i, item) { console.log(item.name); console.log(this.name); // 同上 });但一旦在回调里嵌套了 Ajaxthis 就不再指向当前元素了因为新的回调函数有独立上下文$.each(items, function(i, item) { $.get(/api/detail, function(res) { // 这里的 this 不是 item console.log(this); }); });老代码的标准修法是在外层先存var self this或者在 $.each 里改用箭头函数。但要注意老项目浏览器兼容性要求高的话箭头函数需要经过 Babel 转译否则低版本浏览器直接语法报错。稳妥起见var self this永远不过时。6.4 一张表理清排查思路现象排查步骤高频原因选择器选不到元素控制台先$(选择器).length看是否为 0元素还没渲染、选择器写错事件绑了不触发检查元素是否动态生成没使用事件委托Ajax 不执行打开 Network 看请求是否发出、看响应码URL 拼错、跨域被拦Ajax 成功但数据不显示打印 res 看字段名是否匹配columns.data 与返回字段不一致插件不初始化检查插件文件是否在 jQuery 之后加载引入顺序反了那个“Ajax 成功但数据不显示”的案例我印象很深DataTables 表格一直空白控制台也没有报错Network 里请求状态 200响应数据也正常查了半天发现 columns 里写的data: userName而后端返回的字段叫name。字段名对不上表格自然空白。这类问题靠“看网络请求返回结构”和“对比 columns 配置”两步就能定位比盲目改代码高效得多。学习路线与和现代前端的衔接7.1 哪些源码最值得读直接从头到尾读 jquery.js 不现实压缩后几万行容易劝退。但它的整体骨架可以拆开看外层是一个立即执行函数避免污染全局变量最后把 jQuery 挂到window上jQuery.fn是 jQuery 对象的原型所有实例方法都挂在这里$.extend用来扩展静态方法$.fn.extend用来扩展实例方法。理解这个骨架之后很多插件源码就能看懂了。比如一个插件开头常写(function($) { $.fn.myPlugin function() { ... }; })(jQuery);这就是在 jQuery 的原型上挂了一个新方法。闭包里的$是局部变量即使页面里$被别的库占了也不影响插件内部使用。这个模式对封装组件特别有用。7.2 jQuery 到原生 JS 的对应关系把 jQuery 的方法翻译成原生 API其实就一张表的事$(#id)对应document.querySelector(#id).on(click, fn)对应.addEventListener(click, fn).text()对应.textContent.html()对应.innerHTML.val()对应.value.addClass()对应.classList.add()。真要在老项目里局部重写这个对应表能让你少查很多文档。但也要清醒原生写法没有 jQuery 那种跨浏览器一致性比如innerHTML和textContent的差异、addEventListener在非常老的浏览器里的兼容问题。不是所有旧代码都值得迁移只有那些频繁改动、性能瓶颈明显的部分才值得动。我的原则是能跑的老代码不乱动新增代码再考虑现代写法。7.3 封装第一个 jQuery 插件练手想验证自己是不是真学明白了最好的方式是封装一个小插件。就拿前面的悬浮提示需求来练写一个极简版本(function($) { $.fn.cellTip function(options) { var settings $.extend({ maxWidth: 240 }, options); this.each(function() { var $el $(this); $el.addClass(cell-tip); $el.attr(data-full, $el.text()); $el.css(max-width, settings.maxWidth px); }); return this; // 关键返回 this 保持链式调用 }; })(jQuery); $(.ellipsis).cellTip({ maxWidth: 300 });这段代码虽然简单却包含了插件开发的三个核心概念闭包避免全局污染、$.extend合并默认配置、return this维持链式调用。能独立写出来说明你对 jQuery 的对象模型和扩展机制已经有手感了。最后再分享一点个人体会用过 React 再回头看 jQuery你会发现它解决的是“无框架时代”的 DOM 操作效率问题。技术新旧的判断标准不应该是“有没有人在吹”而是“它还在替你解决什么问题”。老系统里的 jQuery 代码虽然看着不够现代但那些 event 委托、链式调用、插件封装手法放到今天依然是好设计。真去维护一套老系统最实用的技能组合其实是扎实的 jQuery 功底、浏览器调试能力和一点点后端接口视野。把这份文档里的内容吃透至少能让你接老项目时不再头皮发麻。
返回列表