toggle事件本身不触发requestAnimationFrame;details元素的toggle事件同步触发但DOM未重绘,需在其中调用rAF获取scrollHeight并设置height以启动CSS动画。

toggle 事件本身不触发 requestAnimationFrame
HTML 没有原生的 toggle 事件。你可能指的是 details/summary 元素展开收起时的 toggle 事件,或自定义的开关按钮(如 input[type="checkbox"])的 change 事件误称为 toggle。浏览器不会在 DOM 展开/收起瞬间自动调用 requestAnimationFrame——它只是个普通事件,需要你手动接入动画循环。
-
details元素触发的toggle事件是同步的,此时 DOM 已完成切换,但样式尚未重绘;若想在展开后启动平滑动画,得在事件回调里调用requestAnimationFrame延迟到下一帧开始 - 对
input[type="checkbox"]或按钮模拟的 toggle,应监听change或click,而非等待不存在的toggle事件 - 不要在事件处理器里直接写
element.style.height = 'auto'—— 这会强制同步布局,且无法过渡;requestAnimationFrame不能绕过这个限制,只能帮你把“读取高度 → 设置动画起点 → 启动 rAF 循环”这几步安排在正确时机
在 details 元素 toggle 后启动 rAF 动画的正确时机
直接在 toggle 事件中修改样式并期望立即动画,大概率失败:浏览器还没完成 layout,offsetHeight 可能还是 0,或者 CSS transition 尚未生效。必须用 requestAnimationFrame 把操作推到重绘前。
- 先监听
details的toggle事件 - 在回调里立刻调用
requestAnimationFrame,并在其回调中读取目标区域的scrollHeight(不是offsetHeight,因为后者在折叠时为 0) - 拿到真实高度后,再设
style.height为该值,并添加 CSS transition(或进入 rAF 驱动的逐帧插值) - 示例关键片段:
details.addEventListener('toggle', () => { requestAnimationFrame(() => { const content = details.querySelector('.content'); if (details.open) { content.style.height = content.scrollHeight + 'px'; } else { content.style.height = '0'; } }); });
用 rAF 实现 checkbox toggle 的逐帧展开/收起
当你要完全绕过 CSS transition、用 JavaScript 控制每帧高度变化时,requestAnimationFrame 是唯一可靠选择。重点不是“启动”,而是“状态管理 + 时间戳驱动 + 清理”。
- 每次
change事件触发,先调用cancelAnimationFrame(animationId)中止上一个动画,避免冲突 - 记录起始高度、目标高度(
open ? scrollHeight : 0)、起始时间(performance.now()) - 动画函数接收
currentTime参数,计算deltaTime,用缓动函数(如t => t * t)算出当前高度比例 - 务必在动画结束时重置
height为'auto'(展开完毕)或'0'(收起完毕),否则影响后续scrollHeight计算 - 错误示范:
for (let h = 0; h —— 这会阻塞主线程,一帧内全执行完,毫无动画效果
容易被忽略的 DOM 读写陷阱
几乎所有 toggle + rAF 组合卡顿的根源,都来自动画帧内“读高写低”的 DOM 操作模式。
立即学习“前端免费学习笔记(深入)”;
- 不要在 rAF 回调里反复读取
element.offsetHeight或getComputedStyle(element).height—— 每次读都会触发强制同步布局 - 所有读操作(如
scrollHeight、clientWidth)必须集中在第一帧完成,并缓存为变量 - 所有写操作(如
style.height、style.transform)集中放在帧末尾,且优先用transform替代height(例如用scaleY模拟展开) - 移动端要注意:
details在 iOS Safari 上toggle事件触发时机略有延迟,建议加 16ms 的setTimeout容错(仅 iOS),或统一改用click+ 手动切换open属性
真正难的不是写对 requestAnimationFrame(animate) 这一行,而是判断哪一帧该读、哪一帧该写、哪一帧该清理,以及怎么让 DOM 状态和 JS 状态始终对齐。漏掉任意一个环节,动画就可能跳变、残留、卡死或内存泄漏。



















