讲解 JavaScript 动画 Web Animations API

3,142 阅读9分钟

前言

之前写过一篇《讲解 CSS 过渡和动画 transition/animation》 文章,其中提到,在现如今用户越来越注重 Web应用 使用体验的常态下,Web 动画 的发展不再是 Flash Player 为主,早已是 HTML5 的标准大放异彩的世界。 而在浏览器中主要由这两种 CSS 动画JS 动画 进行实现:

  • CSS 动画

    • CSS Transition:
      CSS 过渡,属于 补间动画,即设置关键帧的初始状态,然后在另一个关键帧改变这个状态,比如大小、颜色、透明度等,浏览器将自动根据二者之间帧的值创建的动画。

    • CSS Animation
      CSS 动画,可以理解是 CSS Transition 的加强版,它既可以实现 补间动画 的动画效果,也可以使其以 逐帧动画 的方式进行绘制。

  • JS 动画 (本篇主题)

    • setTimeout / setInterval / requestAnimationFrame
      我们最常用的是 setTimeoutsetInterval 这两个API。但是这两个 API 设定的时间会因为浏览器当前工作负载而有所偏差,而且无法与浏览器的绘制帧保持同步。所以才有了 与浏览器的绘制帧同步 的原生 API requestAnimationFrame,以取代 setTimeoutsetInterval 实现动画。

    • Animations API
      浏览器动画引擎 API,通过 JavaScript 操作。这些 API 被设计成 CSS TransitionCSS Animation 的接口,很容易通过 JS 的方式实现 CSS 动画,可以理解成 CSS 动画的 JS版本。它是目前对动画化的支持最有效的方式之一。

其中 CSS 动画 更多细节内容在之前的文章中已进行了分析和讲解。而本篇将主要以 JS 动画 的视角对如下问题进行分析和探讨:

  • JS 动画 相关的 API 有哪些?
  • JS 动画CSS 动画 相比,性能真的比不上吗?
  • JS 动画CSS 动画 相比,都有哪些优势和劣势?
  • JS 动画 有了 requestAnimationFrame API, 为什么还需要 Animations API
  • JS 动画Animations API 的定义和使用

JS 动画 API 有哪些

setTimeout / setInterval API

设定定时器,通过周期性的触发重复执行绘制动画的函数,来实现 “逐帧动画” 的效果。

  // 定时 id
  let id = null
  
  // 动画函数
  const draw = () => {
    /* 动画绘制... */
  }
  
  
  // setTimeout
  const start = () => {
    draw()
    clearTimeout(id)
    id = setTimeout(start, 16.7)
  }
  const stop = () => { clearTimeout(id) }
  
  
  // setInterval
  const start = () => {
    clearInterval(id)
    id = setInterval(draw, 16.7)
  }
  const stop = () => { clearInterval(id) }
  
  • 优势

    1. 具有很好的浏览器兼容性,所有浏览器都支持这两个 API
    2. 这两个API 通过回调函数的定义,可以很方便的实现动画效果
  • 劣势

    1. 只能接近设备屏幕刷新率,无法做到和浏览器同步,所以可能会存在卡顿、丢帧、抖动的现象
    2. 由于浏览器单线程机制,存在队列中回调函数被阻塞的可能,所以无法保证每一次调用的时间间隔都相同,某次回调可能会被跳过,导致跳帧。
    3. 即使将浏览器最小化,或者切换到另一个 Tab 页面,定时器还依旧会继续在后台执行,浪费了 CPU 的资源。

requestAnimationFrame API

为了弥补 setTimeout / setInterval 在动画方面的不足,所以浏览器提供了为动画而生的 API,它可以让 DOM 动画、Canvas 动画、 SVG 动画、WebGL 动画等有一个统一的刷新机制,随着浏览器的屏幕刷新,统一绘制动画帧。

  // 定时 id
  let id = null
  
  // 动画函数
  const draw = () => {
    /* 动画绘制... */
  }
  
  
  // requestAnimationFrame
  const start = () => {
    draw()
    cancelAnimationFrame(id)
    id = requestAnimationFrame(start)
  }
  const stop = () => { cancelAnimationFrame(id) }
  
  • 优势

    1. 由系统来决定回调函数的执行时机, 它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次, 这样就不会引起丢帧现象, 也不会导致动画出现卡顿的问题。
    2. 在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU的开销。
  • 不足

    1. 同 setTimeout/setInterval 一样,它是以逐帧动画的方式进行绘制,无法做到像 CSS 动画,让游览器自动根据两帧之间的差异创建插值,以实现补间动画的过渡效果。

Animations API

既然已经有了为动画而生的 requestAnimationFrame,为什么还需要 Animation API 呢?我想原因大致如下:

  1. requestAnimationFramesetTimeout/setInterval 都是以逐帧绘制的方式实现动画, 而 Animations API 不仅可以 “逐帧动画”,还可以实现 “补间动画” 的效果。

  2. CSS 动画有一定的局限性,需要事先预设动画样式,而且无法与 JS 进行交互。相比之下,Animations API 可以随时定义并使用动画,自然是更加灵活方便。

有关 Animation API 的定义和使用,后面会在 JS Animations API 章节中讲解。


JS 动画 相关几个问题

JS 动画 性能如何?

其实比较 JS 动画 和 CSS 动画的性能,两者并没有绝对的高低。就应用场景而言,简单的动画通过 CSS 语法去实现可能更好,复杂的动画通过 CSS 去实现,则代码冗余不说,性能可能远不如 JS 语法实现。

那为什么会有 CSS 动画性能比 JS 动画性能更好的说法呢?我列了下如下的几种可能供大家参考:

  • 由于 setTimeout/setInterval 无法与浏览器每帧绘制保持同步,所以可能会存在卡顿、丢帧、抖动的现象,导致动画体验不如 CSS3 动画。

  • 由于 setTimeout/setInterval/requestAnimationFrame 的动画实现属于 逐帧动画,需要计算出每一帧的动画位置或状态值。相比 CSS3 的 补间动画,则是由浏览器根据两帧之间的差异自动计算并绘制动画,性能更胜一筹。

  • CSS 部分属性不触发 pain/layout(即重绘和重排), 例如 opacity、transform 只涉及到浏览器的 compositor thread 的合成工作,性能大幅度提升。

  • CSS 部分属性能够启动3D加速和GPU硬件加速,例如使用 transform 的 translateZ 进行3D变换时。

如上所述,这也许就是有了 requestAnimationFrame 之后,还出现了 Animations API 的原因之一吧。

JS/CSS 动画 优缺点?

  • JS 动画

    优点

    1. JS 动画控制能力很强, 可以在动画播放过程中对动画进行控制:开始、暂停、回放、终止、取消都是可以做到的。
    2. JS 动画效果比 CSS 动画丰富,有些动画效果(比如复杂曲线运动, 视差滚动效果),由 JS 实现更加容易和定制。

    缺点

    1. JavaScript 在浏览器的主线程中运行,而由于主线程中还有其它工作(样式计算、布局、绘制等), 对其干扰导致线程可能出现阻塞,从而造成丢帧的情况。
    2. 动画的实现复杂度高于 CSS 动画
  • CSS 动画

    优点

    1. 浏览器可以对 CSS 动画进行优化。
    2. 通过 transformtranslateZ 等属性开启硬件加速 (使用 GPU 提高性能)。

    缺点

    1. CSS 动画的控制力较弱,只能对其进行暂停或恢复,不能在半路反转动画,也无法对其动画中的关键帧添加事件加以监听。
    2. 用 CSS 实现稍微复杂一点的动画,会相当困难,且代码会变得异常笨重。

JS Animations API

概述/简介

浏览器动画引擎 API,通过 JavaScript 操作。这些 API 被设计成 CSS Transition 以及 CSS Animation 的接口,很容易通过 JS 的方式实现 CSS 动画,可以理解成 CSS 动画的 JS版本。它是目前对动画化的支持最有效的方式之一。

语法定义

  • Animation

      /**
       * effect: KeyframeEffect 对象
       * timeline: 指定与动画关联的时间轴,默认 document.timeline
       */
      var animation = new Animation(effect, timeline);
      
    
    • animation 属性

      1. currentTime:动画的当前时间值(以毫秒为单位)。
      2. effect:获取或设置与此动画相关联的 KeyframeEffect。
      3. finished:返回此动画的当前完成的状态(只读)。
      4. id:获取或设置用于标识动画的字符串。
      5. playState:返回描述动画播放状态的枚举值。
      6. playbackRate:返回或设置动画的播放速率。
      7. ready:返回此动画的当前就绪状态 (只读)。
      8. startTime:获取或设置动画播放应开始的预定时间。
      9. timeline:获取或设置与此动画相关联的 timeline。
    • animation 方法

      1. cancel():清除此动画的所有 keyframeEffects 并中止播放。
      2. finish():结束动画播放。
      3. pause():暂停播放动画。
      4. play():开始或恢复播放动画,或者如果之前完成,则重新开始动画。
      5. persist():显式保留动画,防止在另一个动画替换它时自动删除。
      6. reverse():反转播放动画,直到播放到动画开始时停止。
    • animation 事件

      1. cancel:获取并设置取消事件的事件处理程序。
      2. finish:获取并设置完成事件的事件处理程序。
      3. remove: 当浏览器自动删除动画时触发。
  • KeyframeEffect

      /**
       *   target: 要进行动画处理的 DOM 元素
       * 
       *   keyframes: 关键帧对象
       * 
       *   options: 表示动画持续时间整数(ms),或包含以下一项或多项的对象
       * 
       *     delay: 延迟动画开始的毫秒数
       *     direction: 动画运行播放方向
       *     easing: 动画随时间变化的速率
       *     endDelay: 动画结束后延迟的毫秒数
       *     fill: 设置 CSS 动画在执行之前和之后如何将样式应用于其目标
       *     iterationStart: 描述动画应在迭代中的哪个点开始
       *     iterations: 动画应重复的次数
       *     pseudoElement: 伪元素选择器 如 ::before
       *     iterationComposite: 确定在此动画中如何从迭代到迭代生成值
       *     composite:确定此动画与其他单独的值之间的组合方式
       *     
       *   sourceKeyFrames: 要克隆的关键帧效果对象。
       *  
       */
      const keyframeEffect = new KeyframeEffect(target, keyframes)
      const keyframeEffect = new KeyframeEffect(target, keyframes, options)
      const keyframeEffect = new KeyframeEffect(sourceKeyFrames)
      
    
    • keyframeEffect 属性

      1. target:获取和设置由此对象进行动画处理的伪元素的原始元素。
      2. pseudoElement:获取和设置此对象要进行动画处理的伪元素的选择器。
      3. iterationComposite:获取和设置迭代复合操作,解析关键帧效果属性值。
      4. composite:获取和设置迭代复合操作,解析关键帧效果属性值。
    • keyframeEffect 方法

      1. getComputedTiming:返回此关键帧效果的计算的当前计时值。
      2. getKeyframes:返回构成此效果的计算关键帧及其计算的关键帧偏移量。
      3. getTiming:返回与包含所有动画计时值的动画关联的对象。
      4. updateTiming:更新指定的计时属性。
      5. setKeyframes:替换构成此效果的关键帧集。

    其中 options 参数选项,请参考 CSS animation-* 属性

语法范例

  • new Animation

      const element = document.getElementById("container");
    
      const keyframes = new KeyframeEffect(
        element,
        [
          { transform: "translateY(0%)" },
          { transform: "translateY(100%)" },
        ],
        { duration: 3000, fill: "forwards" }
      );
    
      const animation = new Animation(
        keyframes,
        document.timeline
      );
    
      // Auto Play
      
    
  • Element.animate (简写方式,推荐)

      const element = document.getElementById("container");
    
      const animation = element.animate(
        [
          { transform: "translateY(0%)" },
          { transform: "translateY(100%)" },
        ],
        { duration: 3000, fill: "forwards" }
      );
    
      // Auto Play
      
    

代码范例演示


Web 动画 思维导图

Web 动画.png