日志详情

封装requestAnimationFrame对象,搜集页面执行动画,自动触发、自动销毁requestAnimationFrame

发布时间:2025-04-15

image

一、思路设计

设计模式:工厂模式 + 状态模式。

定义 set + remove 方法用于搜集和释放外部传入的动画。标记requestAnimationFrame执行状态,在插入需要执行的动画时,创建requestAnimationFrame。当所有动画结束后,清除requestAnimationFrame。


二、对象实现

1、对象的基础参数

requestAnimationId: number | null;
animations: Map<string, () => void>;
reRender?: () => void;

requestAnimationId:requestAnimationFrame执行时生成id,用于动画执行完毕的清除,以及作为状态标记防止重复创建;

animations:动画集合,类型为Map,用于搜集外部传入的动画,动画帧执行时循环调用其中的动画;

reRender:重画canvas等渲染的函数,由外部在初始化页面时传入;


2、对象的初始化

对象在构造时候需要初始requestAnimationId和animations,在外部调用构造函数后,页面需要主动传入reRender进行渲染函数的初始化。

constructor() {
  this.requestAnimationId = null;
  this.animations = new Map();
}

init = (reRender: () => void) => {
  this.reRender = reRender;
}


3、声明动画集合执行方法和动画集合检测方法

定义start方法,就是通常requestAnimationFrame的递归调用方法,当然我们要做到requestAnimationFrame的清理需要在每次执行时检测

animations集合。

start = () => {
  const render = () => {
    // 调用hasAnyAnimation函数判断是否继续执行
    if (!this.hasAnyAnimation()) return;
    this.animations.forEach(anime => anime());
    if (this.reRender) this.reRender();
    this.requestAnimationId = requestAnimationFrame(render);
  };
  render();
}


hasAnyAnimation = () => {
  // 当animations集合中没有需要执行的动画时,就清除requestAnimationFrame
  if (this.animations.size === 0) {
    if (this.requestAnimationId) {
      cancelAnimationFrame(this.requestAnimationId);
      this.requestAnimationId = null;
    };
    return false;
  }
  return true
}


4、动画的添加和移除

定义set方法,外部调用set方法添加一个动画执行函数,为该函数生成一个uuid。

定义remove方法,一般情况下动画函数执行完毕后会有onComplete回调,所以在外部的回调中执行remove方法,通过uuid移除存在于集合中的对应函数。

setAnimation = (anime: () => void) => {
  // 生成uuid的
封装方法,通过uuid库实现
  const uuid = generateRandomNumberString(12);
  this.animations.set(uuid, anime);
  // 当没有requestAnimationFrame被创建时,调用start方法,触发render的执行
  if (!this.requestAnimationId) {
    this.start();
  }
  // 返回uuid用于外部的移除调用
  return uuid;
}

removeAnimation = (uuid: string) => {
  if (this.animations.has(uuid)) {
    this.animations.delete(uuid);
  }
}


三、使用对象

react + tweenjs + threejs 中使用

1、初始化对象(reRender为threejs的重绘方法)

const animationRef = useRef<GisAnimation>(null)
const initAnimation = () => {
  const gisAnimation = new GisAnimation();
  gisAnimation.init(reRender);
  animationRef.current = gisAnimation;
}
useEffect(() => {
  initAnimation();
}, [])


2、添加动画和移除动画

这里同时实现了动画的异步调用以控制连续动画执行顺序,如模型1先执行leave动画再执行模型2的enter动画。

声明addAnimation,以tween为例:

const addAnimation = async (tween: TWEEN.Tween | undefined, onComplete?: () => void) => {
  if (!tween) return;
  return new Promise((rs) => {
    const gisAnimation = animationRef.current;
    if (gisAnimation) {
      const uuid = gisAnimation.setAnimation(() => tween.update())
      tween.onComplete(() => {
        gisAnimation.removeAnimation(uuid);
        if (onComplete) {
          onComplete();
          rs(true);
        }
      })
      tween.start();
    }
  })
}

const doAnimation = async () => {
  // earthEnter为地球模型的入场动画,返回一个tween实例
  await addAnimation(earthEnter());
  // earthLeave为地球模型的离场动画,返回一个包含tween和onComplete方法的对象
  const earthLeaveBack = earthLeave();
  await addAnimation(
    earthLeaveBack.tween,
    earthLeaveBack.onComplete,
  )
}