You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

cancelAnimationFrame失效 滚动触发动画速度异常无法停止问题

问题现象

滚动联动图片左移的效果出现三个异常:

  • 滚动次数越多,图片左移速度越快
  • 滚动停止后动画不会终止
  • 调试打印的requestAnimationFrame返回ID(变量raf)持续递增,cancelAnimationFrame(raf) 未生效
根本原因
  1. 多动画循环并行执行
    scroll事件触发频率极高,事件回调中每次调用requestAnimationFrame(onRaf)前都没有清理已存在的动画任务;加上onRaf是自递归结构——每次执行完都会主动注册下一帧回调,最终会有大量动画回调同时排队,每帧多次累加位移值,直接导致移动速度随滚动次数增加越来越快。
  2. 取消逻辑覆盖不全
    150ms防抖的setTimeout触发时,只取消了当时变量中存储的一个raf ID,但onRaf递归注册的后续帧任务已经进入浏览器调度队列,这些任务没有被取消,会一直递归执行,动画无法停止。
  3. 防重锁失效
    原代码的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 12:01:15