ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

jQuery进阶实战:动画队列、事件委托与Ajax数据看板开发

jQuery进阶实战:动画队列、事件委托与Ajax数据看板开发 1. 为什么第二篇比第一篇更考验串知识的能力1.1 从API记忆到全局思维学完jQuery第一篇的时候很多人都有一种我会了的错觉。选择器会用$(#id).css()能改样式$(p).on(click)能绑事件写个高亮、改个颜色毫无压力。但一旦接到一个稍微完整一点的需求比如页面里要放一个下拉菜单点按钮展开、点外部收起再比如要从后端接口拉数据渲染到一个表格和一张图表里不少人就卡住了。卡住的根本原因不是某个API不会用而是不知道这些知识怎么串起来。on()绑定的事件怎么和动画协作$.ajax请求回来的数据怎么塞进DOM下拉菜单打开之后为什么点了菜单项又把整个页面带跑了典型的事件冒泡问题这些都是在第一篇那种单个知识点验证demo里遇不到的。所以第二篇我建议直接换个学法——不要按文档顺序去刷API而是围绕三个真实场景来学动画与交互反馈、组件式的事件管理、数据请求与页面渲染。把知识点放进场景里它才是活的知识否则背再多的方法名遇到需求照样写不出来。1.2 第二篇真正要解决的三类问题我观察过不少刚学完第一篇的开发者他们普遍在下面三类问题上栽跟头第一类交互反馈不自然。菜单能弹出来但唰一下出现、唰一下消失没有过渡想用slideDown做展开动画又发现鼠标快速划过的时候菜单像抽风一样开开关关。这背后是动画队列和stop()没有掌握。第二类事件管理混乱。一个页面上有十几个按钮每个都单独绑click代码冗长不说动态生成的元素还绑不上事件。这背后是事件委托、事件冒泡、closest()这类技巧型API没有吃透。Bootstrap下拉菜单为什么能做得那么干净利落拆开看就是事件管理做得好。第三类数据不知道怎么流。会$.ajax的基本写法但不知道dataType该填什么接口返回{code: 0, msg: ok, data: [...]}这种结构时不知道在哪一层判断请求期间loading条忽闪忽闪。更别说把数据无缝喂给ECharts画图了。这篇文章就是冲着这三个问题来的。读完之后你能自己手写一个行为完整的下拉菜单能规范地写完一个Ajax数据请求流程还能把jQuery和ECharts组合起来做一个真实的数据看板。如果你是带项目的人也可以把这套思路直接讲给团队里的新人听。第一篇毕业水平第二篇毕业水平能改样式、绑点击事件能做菜单、Tab、轮播等交互组件知道有$.ajax这个方法能独立对接接口处理loading和异常会用hide()/show()能管理动画队列让动效不打架会照抄文档示例能说清每个API为什么这样用2. 动画与方法链jQuery动效背后的执行逻辑2.1 三类基础动画方法别再只记名字jQuery的动画方法看着很多其实本质就三类改变显示状态、改变透明度、改变高度。理解了这三类你的记忆负担会小很多。第一类是show()/hide()/toggle()。不带参数的时候它们是瞬间改变display属性根本没有动画。一旦传入速度参数比如show(200)jQuery才会在200毫秒内同时改变元素的宽度、高度和透明度形成一个展开效果。这个细节很容易被忽略——很多人以为调用show()天然有动画结果页面刷一下就出来了还以为是浏览器问题。第二类是fadeIn()/fadeOut()/fadeToggle()/fadeTo()。它们只改变透明度不碰宽高。注意fadeTo()比较特殊它需要两个参数fadeTo(400, 0.3)表示在400毫秒内把元素透明度变到0.3这个API在做半透明遮罩置灰效果时非常实用其他几个fade方法给不了你这么精确的控制。第三类是slideDown()/slideUp()/slideToggle()。它们只改变高度。菜单、折叠面板、消息提示条用的都是这一类。三种方法简单对比如下方法族作用属性典型场景show/hide/togglewidth height opacity显示隐藏模块、弹层fadeIn/fadeOut/fadeToopacity遮罩、提示、图片淡入slideDown/slideUpheight下拉菜单、手风琴、消息条2.2 animate可以做的远不止移动animate()才是jQuery动画的正规军。它允许你自定义几乎所有数值型CSS属性比如left、top、marginLeft、opacity甚至scrollTop。我印象最深的一个实际用途是回到顶部按钮$(#backTop).on(click, function () { $(html, body).animate({ scrollTop: 0 }, 500); });注意这里选择器要同时写html, body因为不同浏览器的滚动容器不一样老项目里这行代码能省掉绝大多数兼容性抱怨。animate({ scrollTop: 0 }, 500)的意思是在500毫秒内把页面的滚动高度平滑地变成0。如果你用原生JS写要自己处理requestAnimationFrame和缓动函数jQuery一行搞定了。animate()还支持链式写法比如让一个元素先往右移、再往下移、最后淡出$(.box) .animate({ left: 100px }, 400) .animate({ top: 80px }, 400) .fadeOut(300);这种链式写法的背后是jQuery的动画队列机制也就是2.3节要说的重点。2.3 动画队列机制为什么快速hover会卡很久jQuery的动画默认会按顺序排进一个队列fx queue。也就是说如果你在一个元素上连续调用了三次动画第二次不会打断第一次而是等第一次结束后才开始第三次同理。这在链式动画里是好事但在用户快速交互的场景里就成了坑。最常见的现象一个slideToggle菜单用户用鼠标快速滑入滑出菜单不会停在展开或收起的某个稳定状态而是像抽风一样反复上下滑动好一阵才停下来。原因就是每次mouseenter和mouseleave都往队列里塞了一个动画它们排着队逐个执行用户的操作被积压了。解决办法是动画执行前清空队列标准写法是stop(true, true)$(.dropdown-menu).stop(true, true).slideToggle(200);stop()的第一个参数表示是否清空后续队列第二个参数表示是否立即跳到当前动画的最终状态。实际工作中我几乎总是用stop(true, true)因为它既清空了排队积压的动画又让元素瞬间到达该有的位置视觉上最跟手。finish()也可以清空队列但它会直接把所有队列动画的最终结果应用上没有渐变过程。区别在于stop(true, true)是取消中途动画直接到当前动画终点finish()是瞬间完成所有动画。通常菜单交互用stop(true, true)就够了别记混了。3. 手写一个Bootstrap式下拉菜单事件与交互的完整链路3.1 先拆解Bootstrap下拉菜单的DOM与行为网上很多人搜jQuery模拟Bootstrap下拉菜单就是想在不用Bootstrap的前提下做出一个行为完全一致的菜单。那我们先看看Bootstrap的菜单长什么样div classdropdown button classdropdown-toggle typebutton 下拉菜单 span classcaret/span /button ul classdropdown-menu lia href#选项一/a/li lia href#选项二/a/li lia href#选项三/a/li /ul /div结构是三层外层容器dropdown、触发按钮dropdown-toggle、菜单dropdown-menu。行为有四个关键点点击按钮时菜单切换显示/隐藏再点一次按钮菜单隐藏点击菜单里的某个选项菜单隐藏同时把这个选项的文本回填到按钮上点击页面任意其他区域菜单隐藏。很多初学者只会做第1条做完发现点空白处菜单不关点菜单项页面还会跳转这是因为没有管事件冒泡也没有阻止a的默认行为。3.2 点击开关与事件冒泡处理先把开关逻辑写出来。这里我推荐用事件委托而不是直接给按钮绑事件$(document).on(click, .dropdown-toggle, function (e) { e.stopPropagation(); $(this).siblings(.dropdown-menu).stop(true, true).slideToggle(200); });用事件委托有两个好处未来动态添加的.dropdown-toggle元素不用重新绑定页面上多个下拉菜单共用这一个处理器代码更少。e.stopPropagation()是关键。如果不加这行点击按钮会触发按钮的点击事件然后事件继续冒泡到document。假如后面还绑了一个点击页面任意位置关闭菜单的监听器通常挂在document上事件冒泡到document时菜单会被立刻关闭造成点了开关菜单闪一下就没了的诡异效果。所以先阻止冒泡再切换菜单状态这个顺序不能反。3.3 点击外部区域关闭菜单closest是关键点击外部关闭是下拉菜单体验的分水岭。实现思路是给document绑定点击事件判断本次点击的目标元素是否在某个下拉菜单容器内部如果不在就关闭所有菜单。$(document).on(click, function (e) { if (!$(e.target).closest(.dropdown).length) { $(.dropdown-menu).stop(true, true).slideUp(200); } });这个closest(.dropdown)是灵魂所在。closest()方法会从当前元素自身开始逐级向上查找匹配选择器的祖先元素找到就返回找不到返回空集合。所以无论用户点的是a、span classcaret还是li只要它在一个.dropdown容器内部closest(.dropdown).length就不为0关闭逻辑不会触发。有些人会写成$(e.target).parents(.dropdown)这有个隐蔽的问题parents()只向上找祖先不包含元素自身。如果点击的恰好是.dropdown这个div本身parents(.dropdown)返回空菜单就会被误关。虽然你通常点的不是容器本身但这种边界情况没必要赌用closest()最稳妥。3.4 菜单项点击与键盘操作补充菜单项的点击逻辑也要注意默认行为$(document).on(click, .dropdown-menu a, function (e) { e.preventDefault(); var text $(this).text(); $(.dropdown-toggle).text(text span classcaret/span); $(.dropdown-menu).stop(true, true).slideUp(200); });e.preventDefault()阻止了a标签的跳转。菜单项回填到按钮时因为按钮结构里有span classcaret/span回填文本之后记得把这个小箭头补回来否则按钮样式会变。再补一个键盘操作按下Esc键关闭所有菜单。这个成本很低但对键盘用户是质的提升$(document).on(keydown, function (e) { if (e.key Escape) { $(.dropdown-menu).stop(true, true).slideUp(200); } });到这里一个行为完整、交互自然的下拉菜单就写完了。整个过程的核心不是某个酷炫效果而是事件委托 冒泡控制 目标命中判断这三个基本功。把这段逻辑吃透再去理解框架里的Dropdown组件源码就会轻松很多。4. 数据层打通$.ajax及配套方法的参数、场景与避坑4.1 $.ajax核心参数一次把配置讲清楚动画和交互只是表面功夫网页里真正有含金量的是数据。jQuery的$.ajax是数据请求的中枢参数很多但实际高频使用的不超过10个$.ajax({ url: /api/sales/list, // 请求地址 type: GET, // 请求方式GET/POST/PUT/DELETE data: { page: 1, size: 10 }, // 随请求发送的参数 dataType: json, // 期望的返回数据类型 timeout: 5000, // 超时时间单位毫秒 beforeSend: function () { // 请求发送前可显示loading $(#loading).show(); }, success: function (res) { // 请求成功回调 console.log(res); }, error: function (xhr, status, error) { // 请求失败回调 console.error(status, error); }, complete: function () { // 无论成功失败都会执行 $(#loading).hide(); } });dataType是个容易被新手忽略的关键参数。你告诉jQuery期望json它会自动帮你把返回的字符串JSON.parse成对象省去手动转换。但如果后端返回的Content-Type不对或者返回了HTML错误页jQuery解析失败会直接走error回调这时候很多人一脸懵——明明后端说接口通了为什么前端一直报错十有八九就是返回内容不是合法JSON。timeout也强烈建议加上。没有超时控制时一个挂死的接口可能让loading转圈转几分钟用户早就关掉页面了。设5000毫秒算是一个比较常见的阈值具体可以根据接口复杂度调整。4.2 快捷方法$.get、$.post、$.getJSON怎么选$.ajax是全能选手但日常很多场景用快捷方法更省事。jQuery提供了$.get、$.post、$.getJSON这几个封装好的方法。$.get和$.post适用于参数简单、不需要精细控制比如不需要设置timeout的请求$.get(/api/user, { id: 123 }, function (res) { console.log(res); }); $.post(/api/user/save, { name: 张三, age: 18 }, function (res) { console.log(res); });$.getJSON的适用场景我多说一句它不仅是一个期望返回JSON的GET请求还是老项目中实现跨域的一种常见方式。通过动态创建script标签加载接口地址利用JSONP回调拿到数据。虽然现在主流方案是CORS跨域但维护老项目时仍然可能碰到只能靠JSONP的接口。$.getJSON的JSONP用法长这样$.getJSON(https://api.example.com/data?callback?, function (res) { console.log(res); });URL末尾的callback?是JSONP的触发器jQuery会自动生成一个回调函数名替换掉问号。这个知识点现在很多教程不提了但它在老系统维护里依然有实战价值。4.3 Promise风格回调done/fail/always如果你用的是jQuery 3.x我建议尽量少写success/error/complete这种回调写法改用Promise风格。原因不是新就一定好而是Promise风格在处理多个请求的组合、串行、并行时代码组织要干净得多。var request $.ajax({ url: /api/sales/list, dataType: json }); request .done(function (res) { // 成功处理 }) .fail(function (xhr, status, error) { // 失败处理 }) .always(function () { // 最终处理类似 complete });尤其是两个请求都成功后再做某件事这种场景传统写法要嵌套回调层级很深用$.when包一下就能并行处理$.when( $.ajax({ url: /api/sales/list }), $.ajax({ url: /api/user/list }) ).done(function (salesRes, userRes) { // 两个请求都成功后才执行 console.log(salesRes[0], userRes[0]); });这里要注意done回调拿到的参数是数组结构[data, textStatus, jqXHR]取数据要取xxx[0]新手第一次用容易踩坑。4.4 联调阶段的高频问题缓存、JSON格式与loading和真实后端联调时问题主要集中在三处。第一处浏览器缓存。GET请求在部分浏览器里会被缓存导致你改了后端代码前端看到的还是旧数据。解决办法有两种请求URL后面拼一个时间戳参数或者让后端在响应头里设置禁止缓存。前端最简单的是$.ajax({ url: /api/sales/list?_t Date.now() });第二处返回结构不统一。我遇到过的后端接口有的直接返回数组有的返回{code:0,data:[...]}有的返回{status:success, result:{}}。前端要做的是在进入业务渲染前统一做一次数据解包。我习惯定义一个公共的请求封装函数在done回调里统一判断function request(url, params) { return $.ajax({ url: url, data: params, dataType: json }) .done(function (res) { if (res.code ! 0) { alert(res.msg || 请求失败); return; } }); }这样页面里调用时只需要关心res.data长什么样不用每处都写一遍错误判断。第三处loading闪烁。如果一个页面同时发三个请求三个都在beforeSend里显示loading、在complete里隐藏loading就会出现第一个请求完成把loading关了另外两个还没回来的闪烁问题。简单方案是用一个计数器var loadingCount 0; function showLoading() { loadingCount; $(#loading).show(); } function hideLoading() { loadingCount--; if (loadingCount 0) { loadingCount 0; $(#loading).hide(); } }思路是把显示/隐藏改成计数加/减只有计数归零时才真正隐藏。这个小细节能省掉很多视觉上的尴尬。5. 综合实战原生JS jQuery Ajax ECharts打造数据面板5.1 需求与页面结构先画清楚看板有哪些模块理论讲再多不如做一个完整的东西。我们做一个销售数据看板页面拆成四块顶部一个筛选区域包含年份下拉框和地区下拉框左侧三个KPI卡片分别展示总销售额、订单数、客单价主体右侧一个销售趋势折线图ECharts下方一个品类占比饼图ECharts。页面结构用HTML先搭好div classdashboard div classfilter-bar select idyearSelect option value20232023年/option option value20242024年/option /select select idregionSelect option valueeast华东/option option valuenorth华北/option /select button idsearchBtn查询/button /div div classkpi-row div classkpi-cardh3总销售额/h3p idtotalSales-/p/div div classkpi-cardh3订单数/h3p idtotalOrders-/p/div div classkpi-cardh3客单价/h3p idavgPrice-/p/div /div div classchart-row div idtrendChart stylewidth: 100%; height: 350px;/div /div div classchart-row div idcategoryChart stylewidth: 100%; height: 350px;/div /div div idloading styledisplay:none;数据加载中.../div /div这一步不要急着写代码先在纸上或编辑器里把DOM结构和每个模块的id列清楚。否则后面数据回来了一堆根本不知道该塞到哪里。5.2 三种技术各自的职责边界很多人在这个环节会把三种技术混在一起写代码乱成一锅粥。我的建议是严格划分职责原生JS负责纯逻辑计算比如千分位格式化、时间戳转日期、计算环比增长率。这些功能不需要碰DOM用原生JS反而更轻。jQuery负责三个任务——筛选表单的取值与事件监听、Ajax请求的发送与回调分发、KPI卡片和下拉菜单的DOM更新。jQuery在这套组合里是交互总管。ECharts负责图表实例的初始化、配置项编写、数据变化时的setOption更新。ECharts和jQuery不直接交互中间通过数据对象解耦。用代码来说就是jQuery请求到数据后把res.data这个纯对象分别传给渲染KPI的函数jQuery操作DOM和更新图表的函数ECharts操作canvas各管各的互不干扰。5.3 数据请求与图表刷新的完整代码流程完整流程如下// 初始化图表页面加载后执行一次 var trendChart echarts.init(document.getElementById(trendChart)); var categoryChart echarts.init(document.getElementById(categoryChart)); // 加载数据的总入口 function loadDashboard() { var params { year: $(#yearSelect).val(), region: $(#regionSelect).val() }; $.ajax({ url: /api/sales/dashboard, data: params, dataType: json, beforeSend: function () { $(#loading).show(); }, success: function (res) { if (res.code ! 0) { alert(res.msg || 加载失败); return; } renderKPI(res.data.kpi); renderTrendChart(res.data.trend); renderCategoryChart(res.data.category); }, error: function () { alert(网络异常请稍后重试); }, complete: function () { $(#loading).hide(); } }); } // 渲染KPI卡片使用原生JS做数值格式化 function renderKPI(kpi) { $(#totalSales).text(formatNumber(kpi.totalSales)); $(#totalOrders).text(kpi.totalOrders); $(#avgPrice).text(formatNumber(kpi.avgPrice)); } function formatNumber(num) { return Number(num).toLocaleString(zh-CN); } // 更新折线图 function renderTrendChart(trend) { trendChart.setOption({ xAxis: { type: category, data: trend.months }, yAxis: { type: value }, series: [{ data: trend.values, type: line, smooth: true }] }); } // 更新饼图 function renderCategoryChart(category) { categoryChart.setOption({ series: [{ type: pie, data: category.data }] }); } // 事件绑定查询按钮和下拉框变化时重新加载 $(#searchBtn).on(click, loadDashboard); $(#yearSelect, #regionSelect).on(change, loadDashboard); // 首次加载 loadDashboard();这套流程有几点值得讲透。trendChart和categoryChart放在最外层初始化而不是每次查询时都echarts.init一次。ECharts在同一个DOM上重复init会报thERE is a chart instance already initialized on the dom页面会直接有报错体验极差。正确的做法就是一进来初始化一次后续更新只调setOption。后端返回的数据格式也是联调时要提前约定的。我这里假设的是{ code: 0, msg: ok, data: { kpi: { totalSales: 1280000, totalOrders: 3200, avgPrice: 400 }, trend: { months: [1月, 2月], values: [120, 132] }, category: { data: [{ name: 电子产品, value: 45 }, { name: 服饰, value: 32 }] } } }前后端各写各的很容易对接不上所以这个结构应该在一开始就固定下来。前端拿数据只认这一个结构后端也照着这个结构返回能省掉大量扯皮。5.4 图表二次加载与resize的坑实际使用中还有两个高频问题必须提。第一个是图表容器尺寸变化。如果页面里有侧边栏折叠、或者用户缩放了浏览器窗口ECharts不会自动感知容器尺寸变化图表会变形被裁切。需要在窗口resize时调用图表的resize()方法$(window).on(resize, function () { trendChart.resize(); categoryChart.resize(); });第二个是按条件筛选后的数据残留。如果某次查询返回的分类数据里没有服饰这个品类而饼图还在用上一次的series.data旧数据不会自动清掉图表会出现查了华东却还残存华北品类的错觉。解决办法是在setOption前把图表的配置重置一遍最简单的是用chart.clear()再重新setOption或者始终让后端的返回结构保持完整。这个坑很隐蔽我在自己项目里踩过一次排查了很久才发现是旧数据没清。6. 用得久了才明白jQuery的性能陷阱与选型边界6.1 性能问题通常不在jQuery本身而在用法说到性能老前端圈有一句吐槽不是jQuery慢是你把jQuery用慢了。这话是有道理的。jQuery每次调用$()都会做一次完整的选择器解析和DOM查询如果在一个循环里反复调用性能会很难看。最典型的反面教材for (var i 0; i 100; i) { $(#list).append(li i /li); }每次循环都执行一次$(#list)选择器查询又都触发一次DOM重排100次就是100次查询加100次重排。正确的做法是缓存选择器 一次性拼接插入var $list $(#list); var html ; for (var i 0; i 100; i) { html li i /li; } $list.append(html);只查询一次DOM只触发一次插入。这个习惯在jQuery时代是基本功放到现在用原生JS操作DOM也是一样的道理。另一个高频性能隐患是事件绑得太多。比如一个表格有200行每行都给按钮绑一个click内存里就有200个处理函数。用事件委托直接在table上绑一个就行通过closest()判断点的是不是按钮。这恰好是第3节下拉菜单里用到的思路。6.2 与原生库共存noConflict怎么用还有一个页面维护中常遇到的问题一个老页面上同时挂了Prototype、Zepto或者其他也把$当作全局变量的库再引入jQuery时就会冲突轻则某个库失效重则两个库报错。解决办法是jQuery提供的noConflict()在引入jQuery之后、使用jQuery之前立即调用var jq $.noConflict(); jq(function () { jq(#app).hide(); });调用noConflict()之后$这个符号会归还给之前占用它的库而jQuery本身仍然可以通过jq也就是变量名你随便起来使用。如果你不想完全放弃$也可以这样写var jq $.noConflict(); jq(function ($) { $(#app).hide(); // 在这个回调内部$ 仍然是jQuery });这种写法在引入多个前端库的老旧系统里非常实用至少我接手过的不少历史项目都靠这一行代码解决了库打架的尴尬。6.3 现在学jQuery还值不值得一个过来人的看法这一节写给正在纠结的人。现在的趋势确实是用Vue、React这类框架开发新项目HTML里到处都是{{}}和JSXjQuery的直接用武之地在变少。但你别急着把它归为过时技术我的看法有三个层面。第一层存量市场依然巨大。我在实际工作中接触过大量老系统内部管理系统、运营后台、活动专题页很多还是jQuery写的。每年都有人维护它们、给它们加功能。会jQuery意味着你有能力接手这类项目而不是只会说这个老项目我不熟。第二层jQuery是DOM编程的最佳教学工具。它把复杂的DOM操作、事件处理、异步请求封装成了简单方法让初学者不用一开始就面对浏览器API的各种差异。你用jQuery理解了事件委托是什么异步回调怎么组织之后再学Vue或React会顺畅很多因为这些思想是共通的只是表达形式不同。第三层它帮你建立查源码的安全感。jQury的源码是开源的当你对一个方法的行为产生疑问时可以直接翻源码弄清楚。这种搞明白底层到底发生了什么的能力比背诵某个框架的API值钱得多。回到这一篇的主题。第二篇不是在教你背动画参数和Ajax配置而是帮你在头脑中搭起三条线动画队列与交互反馈、事件委托与组件逻辑、数据请求与页面渲染。这三条线一旦串起来你就能把一个静态HTML页面变成真正活的交互应用。我个人的体会是带新人的时候与其让他们刷一百个API文档不如让他们完整做一遍本文第5节那种小看板——做完之后很多概念自然就通了。
返回列表