第四部分:事件机制与动画系统

目录
第四部分:事件机制与动画系统
4.1. 事件处理深度解析
4.1.1. 事件绑定演进史:废弃的 .bind()/.live()/.delegate() 与现代标准 .on()
4.1.2. 统一接口:.on() 与 .off() 的全面解析与签名重载
场景一:直接绑定点击事件
场景二:事件委托 – 处理动态生成的列表项
场景三:多事件绑定与数据传递
4.1.3. 一次性绑定:.one() 的内存管理优势
4.2. 事件委托原理
4.2.1. 冒泡机制与事件委托的优势(动态绑定与性能优化)
4.2.2. .on(events, selector, handler) 签名深度剖析
4.3. 事件对象与交互
4.3.1. 事件对象修正:event.originalEvent 与原生事件对象
4.3.2. 冒泡与默认行为:stopPropagation() vs preventDefault()
4.3.3. 模拟触发:.trigger() (触发事件冒泡) vs .triggerHandler() (不冒泡)
4.4. 高级特性
4.4.1. 事件命名空间:.on('click.myPlugin') 的批量解绑与模块化管理
绑定两组不同来源的点击事件
仅解绑UI相关的事件,不影响统计功能
触发特定命名空间的事件
4.4.2. 自定义事件:发布/订阅模式的 jQuery 实现
订阅者:监听自定义的 'dataUpdated' 事件
发布者:在业务逻辑完成后触发事件
4.5. 动画与特效系统
4.5.1. 预置动画:.show(), .hide(), .toggle() 与滑动淡入淡出
带参数的动画调用
带回调函数
避免动画队列堆积的写法
4.5.2. 自定义动画 .animate():参数详解与队列机制
4.5.3. 队列控制:.stop(), .finish(), .queue(), .dequeue() 详解
常见问题:快速移入移出,菜单不停闪烁
解决方案:stop([clearQueue], [jumpToEnd])
4.6. 现代视角下的动画
4.6.1. 性能对比:jQuery 动画 vs CSS3 Transition/Animation vs Web Animations API
传统 jQuery 写法
现代 CSS 方案
JS 仅负责切换类名
现代原生 API 写法
4.6.2. 无障碍访问 (a11y):prefers-reduced-motion 媒体查询的兼容处理
4.6.3. 扩展缓动:jQuery Easing Plugin 与自定义缓动函数
4.1. 事件处理深度解析
4.1.1. 事件绑定演进史:废弃的 .bind()/.live()/.delegate() 与现代标准 .on()
jQuery 的事件绑定 API 经历了从分散到统一的演进过程。
|
方法 |
绑定对象 |
动态元素支持 |
废弃原因/现状 |
|
.bind() |
直接绑定到选定元素 |
不支持 |
性能差,每次绑定需遍历所有 DOM 元素。 |
|
.live() |
绑定到 document |
支持 |
已废弃。事件冒泡到文档根节点才触发,性能低下且易冲突。 |
|
.delegate() |
绑定到指定祖先元素 |
支持 |
被 .on() 取代,语法不够直观。 |
|
.on() |
绑定到选定元素或祖先 |
支持 |
现行标准。通过参数重载实现了上述所有方法的功能。 |
早期版本的 .bind()、.live() 和 .delegate() 各自针对不同场景,但在 1.7 版本引入 .on() 后,后者成为了唯一推荐的事件绑定方法,整合了前者的所有功能并修正了潜在的内存泄漏风险。
旧时代写法 – 仅作了解
$('.box').bind('click', function() { /* … */ }); /* 直接绑定,新增.box无效 */
$('.box').live('click', function() { /* … */ }); /* 委托给document,性能黑洞 */
现代标准写法
$('.box').on('click', function() { /* … */ }); /* 替代.bind() */
$('#container').on('click', '.box', function() { /* … */ }); /* 替代.delegate(),支持动态元素 */
这就像是交通工具的进化:
- 早期的 .bind() 是私家车,只能载特定的人(已存在的元素);
- .live() 是公交车,虽然谁都能载但路线太长效率低;
- 现代的 .on() 则是智能网约车系统,既能精准接送(直接绑定),也能通过设置集合点高效拼车(事件委托),一套系统解决所有出行需求。
4.1.2. 统一接口:.on() 与 .off() 的全面解析与签名重载
.on() 是 jQuery 事件体系的核心,通过不同的参数签名实现了直接绑定与委托绑定两种模式。
|
签名形式 |
语法 |
核心用途 |
|
基本绑定 |
.on(events, handler) |
将事件监听器直接附加到选定元素。 |
|
委托绑定 |
.on(events, selector, handler) |
利用冒泡机制,在父元素监听子元素事件,支持动态内容。 |
|
多事件绑定 |
.on(events-map) |
通过对象字面量一次性绑定多个不同事件。 |
|
数据传递 |
.on(events, [data], handler) |
在绑定事件时预置数据,供回调函数使用。 |
理解其重载形式是掌握 jQuery 事件处理的关键。
场景一:直接绑定点击事件
$('#loginBtn').on('click', function(e) {
/* 这里的this指向#loginBtn本身 */
console.log('登录触发');
});
场景二:事件委托 – 处理动态生成的列表项
$('#list').on('click', 'li.item', function(e) {
/* 这里的this指向实际点击的li.item,即使它是后来添加的 */
$(this).toggleClass('active');
});
场景三:多事件绑定与数据传递
$('.panel').on({
mouseenter: function() { $(this).addClass('hover'); },
mouseleave: function() { $(this).removeClass('hover'); }
}, { status: 'active' }); /* 传入的数据会出现在e.data中 */
.on() 就像是前端世界的“万能插座”。
无论你是要插两孔(简单绑定)还是三孔(委托绑定),亦或是需要同时供电多个设备(多事件绑定),只需要这一个接口就能满足所有需求,完全取代了过去五花八门的转接头。
4.1.3. 一次性绑定:.one() 的内存管理优势
在特定场景下,事件只需触发一次,例如表单的首次提交或引导页的首次点击。
|
特性 |
.one() |
.on() + 手动解绑 |
|
代码简洁度 |
高,一行代码搞定 |
低,需在回调中写解绑逻辑 |
|
内存安全性 |
自动释放,无残留风险 |
容易忘记解绑导致内存占用 |
|
执行效率 |
触发后立即销毁监听器 |
逻辑稍显冗余 |
.one() 方法专门用于绑定这种一次性事件,触发后立即解绑,避免了手动管理解绑逻辑的繁琐,同时有效防止内存泄漏。
/*新手引导提示仅显示一次 */
$('#guide-btn').one('click', function() {
alert('欢迎来到系统!此提示将不再显示。');
/* 此处无需调用off(),监听器已自动销毁 */
});
/* 等效的传统写法(对比) */
$('#guide-btn-old').on('click', function handler(e) {
alert('欢迎!');
$(this).off('click', handler); /* 必须手动解绑,且需保存函数引用 */
});
这就像是食品包装上的“一次性封条”。
撕开后就失效了,你不需要费心去把封条粘回去或者写个备忘录提醒自己“这个已经开过了”,系统自动帮你处理了后续的清理工作。
4.2. 事件委托原理
4.2.1. 冒泡机制与事件委托的优势(动态绑定与性能优化)
事件委托的核心依赖于 DOM 事件流的冒泡阶段。
当一个元素的事件被触发时,事件会逐级向上传递至根节点。
通过在父级节点监听事件,可以统一管理子元素的事件响应,这种机制在处理大量子元素或动态内容时尤为高效。
模拟表格删除功能 – 假设有1000行数据
/* 错误做法:给每个删除按钮绑定事件,内存消耗巨大 */
// $('tr .delete-btn').on('click', …); /* 创建1000个监听器 */
/* 正确做法:事件委托 */
$('table tbody').on('click', '.delete-btn', function(e) {
/* 只创建1个监听器,监听整个tbody */
/* 无论新增多少行,只要class匹配就能响应 */
$(this).closest('tr').remove();
});
想象一下公司前台收快递的场景。
如果每个员工都要自己下楼拿快递(直接绑定),效率极低且楼道拥堵。
事件委托就是让前台(父元素)统一签收所有快递,再根据快递单上的名字(selector)分发给对应的员工。
这样无论公司新来了多少人,前台都能处理,且只占用一个接待位。
|
对比维度 |
直接绑定 |
事件委托 |
|
内存消耗 |
高(N个元素=N个监听器) |
低(1个父级=1个监听器) |
|
动态元素 |
无法响应新增元素 |
自动支持新增子元素 |
|
代码维护 |
需在元素增删时重新绑定 |
无需额外维护 |
|
适用场景 |
静态页面、少量元素 |
列表、表格、动态内容区 |
4.2.2. .on(events, selector, handler) 签名深度剖析
实现事件委托的关键在于 .on() 的第二个参数 selector。
当该参数存在时,jQuery 会在事件触发时检查事件源是否匹配该选择器,从而决定是否执行回调函数。
|
参数位置 |
参数名 |
作用说明 |
|
第一个 |
events |
事件类型字符串,如 ‘click’ 或 ‘mouseenter mouseleave’。 |
|
第二个 |
selector |
过滤选择器。只有事件源匹配该选择器才触发,省略则为直接绑定。 |
|
第三个 |
handler |
事件处理函数,this 指向匹配 selector 的元素,而非父元素。 |
复杂场景:嵌套结构中的精确委托
$('#article').on('click', 'a.external', function(e) {
e.preventDefault(); /* 阻止默认跳转 */
/* 检查是否同时按下了Ctrl键 */
if (e.ctrlKey) {
window.open($(this).attr('href'), '_blank');
} else {
location.href = $(this).attr('href');
}
});
/* 只有class包含'external'的a标签才会触发,其他点击被忽略 */
这就像是给门卫下达的指令:
- “凡是穿着红马甲(selector)的人进门都要登记”,而不是“所有人进门都登记”。
- 门卫(父元素)站在门口,只对符合特征的人进行拦截处理,其他人自由通行。
4.3. 事件对象与交互
4.3.1. 事件对象修正:event.originalEvent 与原生事件对象
jQuery 为了解决跨浏览器兼容性问题,对原生 DOM 事件对象进行了包装。
这种包装提供了统一的属性和方法(如 e.target、e.preventDefault()),但在处理鼠标滚轮、触摸事件等特殊场景时,仍需访问原生对象。
$('.scroll-container').on('wheel', function(e) {
/* e 是 jQuery 修正后的事件对象 */
console.log(e.type); /* 输出: wheel */
/* 访问原生事件对象,获取滚轮方向等原生属性 */
var originalEvent = e.originalEvent;
/* 判断滚动方向(原生属性,jQuery未封装) */
if (originalEvent.deltaY > 0) {
console.log('向下滚动');
}
});
jQuery 的事件对象就像是一份“翻译过的外文文档”。
它把不同浏览器(不同方言)的内容统一翻译成了标准语言,方便阅读。
但有些专业术语(原生特有属性)翻译版里没有,这时候就需要查阅“原件”(originalEvent)。
4.3.2. 冒泡与默认行为:stopPropagation() vs preventDefault()
在事件处理中,阻止事件传播和阻止默认行为是两个完全不同的概念,初学者极易混淆。
jQuery 对这两个方法做了标准化封装,确保在所有浏览器中行为一致。
|
方法 |
行为描述 |
典型应用场景 |
影响范围 |
|
stopPropagation() |
阻止事件向上冒泡 |
点击弹窗内部时不关闭弹窗 |
仅影响事件流传播,不影响默认行为 |
|
preventDefault() |
阻止元素的默认动作 |
阻止表单提交、阻止链接跳转 |
仅影响默认行为,不影响事件传播 |
|
return false |
同时阻止两者 |
需要同时终止所有行为的场景 |
jQuery 特有,相当于调用了上述两个方法 |
场景:点击链接弹出提示,但不跳转,也不触发父级的点击事件
$('#link-container a').on('click', function(e) {
e.preventDefault(); /* 阻止跳转,链接URL不会打开 */
alert('链接被点击了');
/* e.stopPropagation(); 如果加上这句,父级div的点击事件将不会触发 */
});
$('#link-container').on('click', function() {
console.log('容器被点击了'); /* 依然会触发,因为未阻止冒泡 */
});
想象你在公司发通知(事件触发)。
- stopPropagation() 是让秘书拦住通知,不让老板(父元素)知道这件事;
- preventDefault() 是取消了通知里要求的行动(比如“全员开会”),大家照常工作。
- return false 则是把通知撕了,老板也不通知,会也不开。
4.3.3. 模拟触发:.trigger() (触发事件冒泡) vs .triggerHandler() (不冒泡)
有时需要在代码中主动触发事件,而非等待用户操作。.
trigger() 和 .triggerHandler() 都能触发事件,但在行为控制和返回值上存在关键差异。
场景:页面加载后自动聚焦输入框
$('#search-input').on('focus', function() {
$(this).addClass('highlight');
console.log('聚焦事件触发');
});
使用 trigger()
$('#search-input').trigger('focus');
/* 结果:输入框获得焦点,类名改变,控制台输出。事件冒泡 */
使用 triggerHandler()
var result = $('#search-input').triggerHandler('focus');
/* 结果:类名改变,控制台输出。但输入框并未真正获得焦点(无光标),事件不冒泡 */
/* 返回值:最后一个处理函数的返回值,而非jQuery对象,无法链式调用 */
这就像是消防演习。
|
特性 |
.trigger() |
.triggerHandler() |
|
浏览器默认行为 |
执行(如聚焦输入框、提交表单) |
不执行 |
|
事件冒泡 |
会冒泡到父元素 |
不会冒泡 |
|
返回值 |
返回 jQuery 对象,支持链式调用 |
返回事件处理函数的返回值 |
|
适用场景 |
模拟真实用户交互 |
纯逻辑调用,测试或执行回调 |
- .trigger() 是真的拉响了警报器,声音传遍整栋楼(冒泡),大家都要跑出去(默认行为)。
- .triggerHandler() 则是演习指挥部内部电话通知“演习开始”,只有相关责任人知道,警报器不响,大家也不用跑动,纯粹是流程逻辑的演练。
4.4. 高级特性
4.4.1. 事件命名空间:.on('click.myPlugin') 的批量解绑与模块化管理
当开发复杂的组件或插件时,可能会绑定大量相同类型的事件。
为了防止解绑时误删其他代码绑定的事件,jQuery 引入了事件命名空间机制,允许通过后缀对事件进行分组管理。
绑定两组不同来源的点击事件
$('#panel').on('click.stats', function() { console.log('统计点击'); });
$('#panel').on('click.ui', function() { console.log('UI交互'); });
$('#panel').on('click.ui.drag', function() { console.log('拖拽逻辑'); });
/* 此时点击面板,控制台输出三条记录 */
仅解绑UI相关的事件,不影响统计功能
$('#panel').off('click.ui');
/* 输出:统计点击。'UI交互'和'拖拽逻辑'均被移除 */
触发特定命名空间的事件
$('#panel').trigger('click.stats'); /* 仅触发统计逻辑 */
这就像是在文件柜里放文件。
普通的绑定是随手一扔,找起来很麻烦。
命名空间就像是给文件贴上了彩色标签(.ui、.stats)。
当你需要清理“UI相关”的所有文件时,只要找到所有蓝色标签的文件扔掉即可,完全不用担心误删了红色的“统计”文件。
|
操作 |
语法示例 |
作用范围 |
|
绑定命名空间 |
.on('click.ns', fn) |
属于 ‘ns’ 分组的点击事件 |
|
解绑命名空间 |
.off('.ns') |
解绑该分组下所有类型的事件 |
|
触发命名空间 |
.trigger('click.ns') |
仅触发该分组的处理函数 |
4.4.2. 自定义事件:发布/订阅模式的 jQuery 实现
除了浏览器内置的点击、输入等事件,jQuery 还支持完全自定义的事件。
结合 .on()(订阅)和 .trigger()(发布),可以实现模块间的解耦通信,这就是经典的“发布/订阅”模式。
订阅者:监听自定义的 'dataUpdated' 事件
$(document).on('dataUpdated', function(e, newData) {
console.log('收到数据更新:', newData);
/* 更新界面逻辑… */
});
发布者:在业务逻辑完成后触发事件
function fetchUserData() {
$.get('/api/user', function(res) {
/* 发布事件,传递数据参数 */
$(document).trigger('dataUpdated', res);
});
}
这种模式就像是广播电台。
- 组件 A(电台)只负责广播“这里有新闻了”,它不需要知道谁在听,也不需要知道听众是谁。
- 组件 B(收音机)只要调到这个频道(监听事件)就能收到消息。
- 两者互不依赖,电台坏了不影响收音机存在,收音机关了也不影响电台广播。
4.5. 动画与特效系统
4.5.1. 预置动画:.show(), .hide(), .toggle() 与滑动淡入淡出
jQuery 内置了一套简洁的动画 API,无需编写复杂的 CSS 或 JS 逻辑即可实现常见的显示/隐藏效果。
|
方法 |
效果描述 |
等效 CSS 变化 |
|
.show() / .hide() |
尺寸与透明度同时变化 |
width, height, opacity |
|
.fadeIn() / .fadeOut() |
仅透明度变化 |
opacity |
|
.slideDown() / .slideUp() |
垂直方向展开/收起 |
height |
|
.fadeToggle() / .slideToggle() |
状态切换 |
对应属性 |
这些方法在底层实际上是修改了元素的 width、height、opacity 等属性。
带参数的动画调用
$('.box').hide(1000); /* 1秒内隐藏 */
带回调函数
$('.box').show('fast', function() {
/* 动画完成后执行,this指向动画元素 */
console.log('显示完成');
});
避免动画队列堆积的写法
$('.nav').stop().slideDown(); /* 先停止当前动画,再开启新动画 */
这些预置动画就像是快餐店的套餐。
虽然不如自己点菜(自定义动画)那么灵活,但胜在标准、快速、口味有保证。
对于简单的交互反馈,直接拿来用最方便。
4.5.2. 自定义动画 .animate():参数详解与队列机制
当预置动画无法满足需求时,.animate() 提供了高度可定制的动画能力。
它允许将任意 CSS 数值属性从当前值过渡到目标值。
|
参数项 |
说明 |
示例值 |
|
properties |
包含 CSS 属性终值的对象 |
{ left: '+=50', opacity: 0.5 } |
|
duration |
动画持续时间 |
400 (默认), ‘fast’, ‘slow’ |
|
easing |
缓动函数 |
‘swing’ (默认), ‘linear’ |
|
complete |
完成后的回调函数 |
function() { … } |
/* 将元素向右移动 200px,同时改变透明度 */
$('.mover').animate({
left: '+=200px', /* 支持相对值写法 */
opacity: 0.5
}, {
duration: 800,
easing: 'swing', /* 非线性变速,更自然 */
complete: function() {
console.log('移动完成');
},
/* 每一步动画都会触发,适合做同步效果 */
progress: function(animation, progress) {
console.log('进度:' + Math.round(progress * 100) + '%');
}
});
这就像是编剧在写分镜头脚本。
你指定了“背景(CSS属性)”要从明亮变暗淡,“演员(元素)”要从舞台左边走到右边,时间控制在多少秒。
至于中间每一帧怎么演,jQuery(作为导演)会自动计算好插值,把连续的动作流畅地呈现出来。
4.5.3. 队列控制:.stop(), .finish(), .queue(), .dequeue() 详解
jQuery 的动画默认是按顺序执行的,形成了一个“动画队列”。
如果用户快速连续触发动画(如鼠标快速移入移出),队列会堆积,导致元素“抽搐”或重复播放。
队列控制方法用于管理这一行为。
常见问题:快速移入移出,菜单不停闪烁
$('.menu').hover(
function() { $(this).animate({ height: '200px' }, 500); },
function() { $(this).animate({ height: '30px' }, 500); }
);
解决方案:stop([clearQueue], [jumpToEnd])
- clearQueue: 是否清空后续队列;
- jumpToEnd: 是否跳到当前动画终态
$('.menu').hover(
function() { $(this).stop(true).animate({ height: '200px' }); }, /* 停止并清空,重开动画 */
function() { $(this).stop(true).animate({ height: '30px' }); }
);
/* finish():停止当前动画,清空队列,并让所有动画属性跳到终态 */
$('#btn').on('click', function() {
$('.box').finish(); /* 瞬间完成所有排队的动画 */
});
|
方法 |
行为 |
形象比喻 |
|
.stop(false) |
暂停当前动画,队列中下一个动画开始执行 |
唱片切歌,播放下一首 |
|
.stop(true) |
暂停当前动画,清空后续队列 |
唱片机直接关机 |
|
.finish() |
瞬间完成所有队列中的动画,跳到终态 |
唱片快进到底 |
|
.queue() |
查看或操作队列函数 |
查看播放列表 |
动画队列就像是银行柜台的排队叫号系统。
如果不加干预,每个人(动画)都会按顺序办完业务。
.stop() 就是告诉柜员“我不办了”,.finish() 则是让柜员“把剩下的人全部快速处理完”。
合理使用 .stop(true) 是解决动画卡顿的关键。
4.6. 现代视角下的动画
4.6.1. 性能对比:jQuery 动画 vs CSS3 Transition/Animation vs Web Animations API
随着浏览器技术的发展,jQuery 的动画机制因直接操作 DOM 属性且依赖 CPU 运算,在性能上已落后于现代方案。
现代前端更推荐使用 GPU 加速的 CSS 动画或原生 JS API。
|
方案 |
执行线程 |
性能表现 |
适用场景 |
|
jQuery .animate() |
主线程 |
低(频繁重绘,易掉帧) |
兼容老旧浏览器、简单非关键动画 |
|
CSS3 Transition |
合成线程 |
高(GPU加速) |
状态切换、简单过渡效果 |
|
CSS3 Animation |
合成线程 |
高 |
复杂关键帧动画、循环动画 |
|
Web Animations API |
主线程/合成线程 |
中/高 |
需要JS精细控制、高性能需求 |
传统 jQuery 写法
$('.box').animate({ left: '100px' }, 500);
现代 CSS 方案
.box { transition: transform 0.5s ease; }
.box.active { transform: translateX(100px); }
JS 仅负责切换类名
$('.box').addClass('active');
现代原生 API 写法
const box = document.querySelector('.box');
box.animate([
{ transform: 'translateX(0)' },
{ transform: 'translateX(100px)' }
], {
duration: 500,
easing: 'ease'
});
- jQuery 动画就像是老式拖拉机,虽然能跑,但在高速公路(现代浏览器)上显得力不从心且噪音大。
- CSS 动画则是高铁,利用专用轨道(GPU)飞速疾驰。
- Web Animations API 则是跑车,既有速度又能灵活控制方向盘。
4.6.2. 无障碍访问 (a11y):prefers-reduced-motion 媒体查询的兼容处理
无障碍设计已成为现代 Web 开发的标配。
部分用户(如前庭功能障碍患者)会因为动画感到眩晕。
优秀的工程实践应当检测用户的系统偏好,并据此禁用或简化动画。
/* jQuery 动画的兼容处理 */
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
$('.panel').on('click', function() {
if (prefersReducedMotion) {
/* 无障碍模式:直接切换,无动画 */
$(this).toggle();
} else {
/* 标准模式:滑入滑出动画 */
$(this).slideToggle();
}
});
/* CSS 中的处理方案 */
/*
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
*/
这就像餐厅为不同口味的顾客提供不同菜单。
普通顾客享受全套精美摆盘(动画),而对某些调料过敏(对动画敏感)的顾客,餐厅应提供简餐版(直接切换),确保所有人都能安全、舒适地享用服务。
4.6.3. 扩展缓动:jQuery Easing Plugin 与自定义缓动函数
jQuery 默认仅提供 swing 和 linear 两种缓动效果。
|
缓动类型 |
效果描述 |
体验感受 |
|
linear |
匀速运动 |
机械、生硬 |
|
swing |
慢–快–慢 |
自然、流畅 |
|
easeInOutBack |
超出目标回弹 |
活泼、有张力 |
|
easeOutBounce |
落地弹跳 |
有趣、物理感 |
通过引入 jQuery Easing Plugin 或自定义 jQuery.easing 对象,可以极大地丰富动画的运动轨迹,使交互更具弹性与活力。
/* 自定义缓动函数:简单的弹性效果 */
jQuery.easing['myElastic'] = function(x, t, b, c, d) {
/* x: 0-1的进度值 */
/* 复杂的数学公式计算弹性位置 */
return Math.pow(2, -10 * x) * Math.sin((x – 0.1) * 5 * Math.PI) + 1;
};
/* 应用自定义缓动 */
$('.ball').animate({ top: '400px' }, {
duration: 1000,
easing: 'myElastic'
});
缓动函数就像是给动画加了“性格”。
linear 是机器人走路,swing 是普通人散步,而自定义缓动则是舞蹈家在舞台上跳跃。
优秀的交互设计往往就赢在这些微妙的“微交互”细节上。
网硕互联帮助中心





评论前必须登录!
注册