2019-04-09 | 前端 | UNLOCK

吸顶效果和防抖节流

position:sticky

简介 : 粘性定位 sticky 相当于相对定位 relative 和固定定位 fixed 的结合;在页面元素滚动过程中,某个元素距离其父元素的距离达到 sticky 粘性定位的要求时;元素的相对定位 relative 效果变成固定定位 fixed 的效果。

使用条件:

  • 父元素不能 overflow:hidden 或者 overflow:auto 属性
  • 必须指定 top、bottom、left、right 4 个值之一,否则只会处于相对定位
  • 父元素的高度不能低于 sticky 元素的高度
  • sticky 元素仅在其父元素内生效
<template>
  <div id="app">
    <div class="container">
      <div class="box"></div>
      <div class="header sticky">使用 `position:sticky` 实现</div>
      <div class="box_two"></div>
    </div>
  </div>
</template>
<style>
  .box {
    width: 100%;
    height: 2rem;
    background: #333;
  }
  .box_two {
    width: 100%;
    height: 20rem;
    background: -webkit-linear-gradient(
      top,
      #333333 20%,
      #999999 40%,
      #333333 80%
    );
  }
  .header {
    width: 100%;
    height: 0.4rem;
    text-align: center;
    line-height: 0.4rem;
    background: #999;
    color: #fff;
    font-size: 0.16rem;
  }
  .sticky {
    position: -webkit-sticky;
    position: sticky;
    top: 0;
  }
</style>

使用原生的 offsetTop 实现

<template>
  <div class="color_box"></div>
    <div class="title_box" ref="pride_tab_fixed">
      <div class="title" :class="titleFixed == true ? 'isFixed' :''">
        使用原生的 `offsetTop` 实现
      </div>
    </div>
    <div class="color_box_two"></div>
  </div>
</template>
<script>
  data() {
    return {
      titleFixed: '',
      startTime: new Date()
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    init() {
      window.addEventListener('scroll', this.throttle(this.handleScroll, 100))
    },
    throttle(fn, time = 300) {
      if (typeof fn !== 'function') {
        throw new Error('必须传入一个函数作为参数')
      }
      const currentTime = new Date()
      if (currentTime - this.startTime > time) {
        this.startTime = currentTime
        fn()
      }
    },
    handleScroll () {
      let self = this
      let scrollTop =
        window.pageYOffset ||
        document.documentElement.scrollTop ||
        document.body.scrollTop
      let offsetTop = self.getOffset(self.$refs.pride_tab_fixed)
      self.titleFixed = scrollTop > offsetTop
    },
    getOffset(obj, direction) {
      let offsetL = 0
      let offsetT = 0
      while (obj !== window.document.body && obj !== null) {
        offsetL += obj.offsetLeft
        offsetT += obj.offsetTop
        obj = obj.offsetParent
      }
      if (direction === 'left') {
        return offsetL
      } else {
        return offsetT
      }
    }
  }
</script>

<style>
  .isFixed {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 99;
  }
</style>

getBoundingClientRect().top 来实现

handleScroll () {
    let self = this
    let offsetTop = self.$refs.pride_tab_fixed.getBoundingClientRect().top
    self.titleFixed = offsetTop < 0
  },

防抖和节流

防抖和节流的作用都是防止函数被多次调用。区别在于:假设一个用户一直触发这个函数,且每次触发函数的间隔小于设定时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间就调用一次该函数。

  • 放抖:
    • 如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿
    • 触发高频事件后 n 秒内函数 只会执行一次 ,如果 n 秒内事件又再次被触发,则重新计算时间只有触发间隔超过制定间隔的任务才会执行
    • 每次触发事件时都取消之前所设定的延时调用方法,确保函数只被出发一次
    • 接受一个参数,可以取消等待,立即调用传入的函数
    • 页面滚动,监听输入框内容
function debounce(fn, wait = 50, immediate = true) {
  let timer, context, args, result, firstRun

  if (typeof fn !== 'function') {
    throw new Error('必须传入一个函数作为参数')
  }

  const debounced = () => {
    context = this
    args = arguments

    if (timer) clearTimeout(timer)
    if (immediate) {
      // 是否已经执行,如果已经执行过,不再执行
      firstRun = !timer // 首次立即调用
      timer = setTimeout(() => {
        fn.apply(context, args)
      }, wait)
      if (firstRun) result = fn.apply(context, args)
    } else {
      timer = setTimeout(() => {
        fn.apply(context, args)
      }, wait)
    }
    return result
  }
  // 取消 debounce 函数
  debounced.cancel = () => {
    clearTimeout(timer)
    timer = null
  }

  return debounced
}
  • 节流:
    • 高频事件每 n 秒内都会执行一次,所以节流会稀释函数的执行频率,将多次执行变成每隔一段时间执行
    • 每次触发事件时都判断当前是否有等待执行的延时函数,两种实现方式,一种是使用时间戳,一种是设置定时器
    • 首次是否执行以及结束后是否执行,效果有所不同

      使用时间戳可以实现首次就执行,设置定时器可以实现结束后仍然执行一次

时间戳形式

throttle(fn, time = 300) {
  let previous = 0
  if (typeof fn !== 'function') {
    throw new Error('必须传入一个函数作为参数')
  }
   return () => {
    let now = +new Date(); //返回当前的时间戳
    if (now - previous > wait) {
        func.apply(this, arguments);
        previous = now;
    }
  }
}

定时器模式

throttle(fn, time = 300) {
  let timer, context, args
  if (typeof fn !== 'function') {
    throw new Error('必须传入一个函数作为参数')
  }
  return () => {
    context = this;
    args = arguments;
    if (!timer) {
      timer = setTimeout(function(){
          timer = null;
          func.apply(context, args)
      }, wait)
    }
  }
}

requestAnimationFrame

告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame() window.requestAnimationFrame(callback);