实现元素进入界面的平滑效果

先看效果:
在这里插入图片描述
实现思路:获取页面中需要加载动画的节点,用元素的animate()方法创建一个动画对象,并传入两个关键帧,接着使用IntersectionObserverAPI创建观察对象,用于观察元素进入页面。当元素进入界面时,执行动画,最后取消元素的观察即可。

具体实现:

第一步:

<div class="container">
   <div class="module">模块1</div>
   <div class="module">模块2</div>
   <div class="module">模块3</div>
   <div class="module">模块4</div>
   <div class="module">模块5</div>
   <div class="module">模块6</div>
   <div class="module">模块7</div>
   <div class="module">模块8</div>
   <div class="module">模块9</div>
   <div class="module">模块10</div>
 </div>


<script>
const DISTANCE = 100;
onload = function() {
// 页面加载完毕,获取到所有需要加载动画的节点
   document.querySelectorAll('.module').forEach(module => {
   // 循环调用每个模块的animate方法。用法:animate(keyframes, options)
   // keyframes 关键帧对象数组,或一个关键帧对象(其属性为可迭代值的数组)
   // options 代表动画持续时间的整数(以毫秒为单位),或者一个包含一个或多个时间属性的对象
     let animation = module.animate([
       {
         transform: `translateY(${DISTANCE}px)`,
         opacity: 0
       },
       {
         transform: 'translateY(0)',
         opacity: 1
       }
     ], {
       duration: 1000,
       easing: 'ease-in-out'
     })
     animation.pause() // 创建好animation对象后,首先暂停动画的执行,待会儿用监听器监听元素进入界面后开启动画
   })
 }
</script>

第二步:

// 创建一个WeakMap对象,用于存储节点及动画对象,WeakMap是为了防止内存泄漏,当元素消失时,元素会自动销毁在对象中的存储
let map = new WeakMap()
// 创建一个监视器
let observer = new IntersectionObserver((entries, observer) => {
// entries是获取到的所有监听对象
  entries.forEach(entry => {
  // 判断当元素进入到界面
    if (entry.isIntersecting) {
    // 获取每个元素上的动画对象
      const animation = map.get(entry.target)
      // 当动画存在时,执行动画,并取消元素进入界面的观察
      if(animation) {
        animation.play()
        observer.unobserve(entry.target)
      }
    }
  });
});

onload = function() {
   document.querySelectorAll('.module').forEach(module => {
     let animation = module.animate([
       {
         transform: `translateY(${DISTANCE}px)`,
         opacity: 0
       },
       {
         transform: 'translateY(0)',
         opacity: 1
       }
     ], {
       duration: 1000,
       easing: 'ease-in-out'
     })
     animation.pause()
     // 用上方创建的监听器 观察每一个模块节点
     observer.observe(module)
     // 将每个节点及动画存储对象中
     map.set(module, animation)
   })
 }

最后一步:

解决关键性问题,当滚动条滚动到界面中间时,刷新界面后,不论是往上滚还是往下滚,再次进入界面的元素都会执行动画。当然我们所需要的是往下滚动时下面的元素进入界面需要加载动画,而上方的元素进入界面不需要动画。那么我们就可以这样写:

let map = new WeakMap()

    let observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const animation = map.get(entry.target)
          if(animation) {
            animation.play()
            observer.unobserve(entry.target)
          }
        }
      });
    });

    const DISTANCE = 100
    function isViewport(element) {
      const rect = element.getBoundingClientRect();
      return rect.top - DISTANCE > window.innerHeight
    }

    onload = function() {
      document.querySelectorAll('.module').forEach(module => {
        // 判断节点是否在视口内
        if(!isViewport(module)) return;
        let animation = module.animate([
          {
            transform: `translateY(${DISTANCE}px)`,
            opacity: 0
          },
          {
            transform: 'translateY(0)',
            opacity: 1
          }
        ], {
          duration: 1000,
          easing: 'ease-in-out'
        })
        animation.pause()
        observer.observe(module)
        map.set(module, animation)
      })
    }

结尾:上面的示例是在html文件中完成的,当然你也可以在Vue中实现,在Vue中,你可以自定义指令,通过获取到的el对象绑定动画、观察,甚至是通过绑定的指令传入动画的参数,来动态执行动画。

为什么需要使用animate方法,而不是在节点的style中添加动画?

因为我们是要封装公用指令、组件,如果我们继续使用style,向其添加动画,有可能和节点原有的动画冲突,为了不对元素本身的动画照成影响,我们可以使用AnimationAPI,它的好处就在于 不会改变元素的DOM树,不会改变元素本身的属性,这样就可以避免两者的冲突。