cancelAnimationFrame失效 滚动触发动画速度异常无法停止问题
问题现象
滚动联动图片左移的效果出现三个异常:
- 滚动次数越多,图片左移速度越快
- 滚动停止后动画不会终止
- 调试打印的
requestAnimationFrame返回ID(变量raf)持续递增,cancelAnimationFrame(raf)未生效
根本原因
- 多动画循环并行执行
scroll事件触发频率极高,事件回调中每次调用requestAnimationFrame(onRaf)前都没有清理已存在的动画任务;加上onRaf是自递归结构——每次执行完都会主动注册下一帧回调,最终会有大量动画回调同时排队,每帧多次累加位移值,直接导致移动速度随滚动次数增加越来越快。 - 取消逻辑覆盖不全
150ms防抖的setTimeout触发时,只取消了当时变量中存储的一个raf ID,但onRaf递归注册的后续帧任务已经进入浏览器调度队列,这些任务没有被取消,会一直递归执行,动画无法停止。 - 防重锁失效
原代码的isAnimating标记在raf回调执行的同步阶段才会置为true,但重复注册raf的操作是在scroll事件回调中同步执行的,此时上一个注册的回调还未到执行时机,锁仍为false,完全无法阻止重复注册。
修复方案
重构动画启动/停止逻辑,从根源避免重复注册,保证停止时能彻底终止递归循环:
- 动画启动前先取消已存在的raf任务,避免多循环并行
- 给动画循环加明确的运行状态判断,不需要运行时直接退出,不再注册下一帧回调
- 修正防抖逻辑,滚动停止时直接终止整个动画循环
修复后的核心JS代码:
"use strict"; const FRICTION = 0.9; const VELOCITY = 0.08; const MODE_CSS = 'css'; const MODE_JS = 'js'; const MODE_SCROLL = 'scroll'; const DIRECTION_LEFT = -1; const DIRECTION_RIGHT = 1; const DIRECTION_BOTH = 0; const textTicker = document.querySelector(".text-ticker"); const texts = document.querySelectorAll(".text-ticker__text"); const mobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); const lerp = (s, e, m) => s * (1 - m) + e * m; let mode = getMode(); let direction = getDirection(); let velocity = { target: VELOCITY * direction, current: VELOCITY * direction }; let progress = 0; let raf = null; let inView = true; let isAnimating = false; function onRaf() { if (!texts || !isAnimating) return; velocity.target *= FRICTION; if (mode === MODE_JS) { if (velocity.target > 0) velocity.target = Math.max(VELOCITY, velocity.target); else velocity.target = Math.min(VELOCITY * -1, velocity.target); } velocity.current = lerp(velocity.current, velocity.target, 0.2); progress += velocity.current; if (progress < -100) progress = progress + 100; else if (progress > 0) progress = progress - 100; if (inView) texts.forEach(text => (text.style.transform = `translate3d(${progress}%, 0, 0)`)); raf = requestAnimationFrame(onRaf); } function startAnim() { if (isAnimating) return; isAnimating = true; if (raf) cancelAnimationFrame(raf); raf = requestAnimationFrame(onRaf); } function stopAnim() { isAnimating = false; if (raf) { cancelAnimationFrame(raf); raf = null; } } function getDirection() { if (textTicker.classList.contains('--direction-left')) return DIRECTION_LEFT; else if (textTicker.classList.contains('--direction-both')) return DIRECTION_BOTH; else if (textTicker.classList.contains('--direction-right')) return DIRECTION_RIGHT; } function getMode() { if (mobile) return MODE_CSS; switch (textTicker.dataset.textTicker) { case MODE_CSS: return MODE_CSS; case MODE_SCROLL: return MODE_SCROLL; default: return MODE_JS; } } let timer = null; window.addEventListener('scroll', function() { if (timer !== null) clearTimeout(timer); startAnim(); timer = setTimeout(stopAnim, 150); }, false);
内容的提问来源于stack exchange,提问作者goodcontext
相关产品推荐
相关产品推荐

