• js 回到顶部逻辑实现和elementUI源码解析


    回到顶部

    大家或多或少都会遇到“回到顶部”这样的需求,在此分享这个技术点以及可能遇到的问题。再分析element源码。

    回到顶部代码实现

    DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Documenttitle>
            <style>
                * {
                    padding: 0;
                    margin: 0;
                }
                .scroll {
                    border: 1px solid;
                    height: 3600px;
                    background-color: red;
                }
                .child {
                    margin-top: 200px;
                    display: grid;
                    place-content: center;
                    height: 600px;
                    font-size: 20px;
                    background-color: green;
                }
                .scrollToTop {
                    position: fixed;
                    right: 10px;
                    bottom: 100px;
                    background-color: #fff;
                    padding: 8px 16px;
                    border-radius: 8px;
                    cursor: pointer;
                }
            style>
        head>
        <body>
            <div class="scroll">
                <div class="child">
                    <div>kinghiee - 回到顶部 - demodiv>
                div>
            div>
            <div class="scrollToTop" onclick="backTopClickHandle()">回到顶部div>
        body>
        <script>
            let timer = null;
    
            function  backTopClickHandle() {
                if (this.timer) return;  
                let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; // 获取滚动条高度 (注意点:1)
                this.timer = setInterval(() => {
                    const speed = Math.floor(-scrollTop / 8); // 获取-scrollTop / 8 小于等于计算值的最大整数,作为滑动速度
                    scrollTop = document.documentElement.scrollTop = document.body.scrollTop = scrollTop + speed; // 赋值
    
                    if(scrollTop <= 0) {
                      clearInterval(this.timer);
                      this.timer = undefined;
                    }
                }, 10); // (注意点:2)
            }
        script>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    请添加图片描述

    回到顶部关键技术点在backTopClickHandle函数中

    function  backTopClickHandle() {
        if (this.timer) return;  
        let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; // 获取滚动条高度 (注意点:1)
        this.timer = setInterval(() => {
            const speed = Math.floor(-scrollTop / 8); // 获取-scrollTop / 8 小于等于计算值的最大整数,作为滑动速度
            scrollTop = document.documentElement.scrollTop = document.body.scrollTop = scrollTop + speed; // 赋值
    
            if(scrollTop <= 0) {
              clearInterval(this.timer);
              this.timer = undefined;
            }
        }, 10); // (注意点:2)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在该函数中,首先判断当前是否存在定时器,如果有下面步骤不执行。否则获取滚动条高度, 启动定时器,在定时器中计算出当前滑动速度,并计算出当前滚动条高度。
    最后如果滚动条高度小于等于0,清除定时器。

    在backTopClickHandle函数中有两点需要特别注意

    注意点 1:获取scrollTop的位置

    
    let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; // 获取滚动条高度 (注意点:1)
    
    // 该语句一定要在this.timer = setInterval(() => {...}),前获取scrollTop值,不能在setInterval(() => {...})内获取
    
    • 1
    • 2
    • 3
    • 4

    scrollTop的位置在setInterval(() => {…})内,效果图如下
    请添加图片描述

    会发现,点击回到顶部,在回到顶部这段时间内,如果再次滑动滚轮,就会出现上图的状态。

    注意点 2:定时器时间间隔

    定时器时间间隔尽量小,会好在15ms~30ms之间。如果时间间隔过大,短时间内滚动条滑不到顶部,这时如果鼠标有交互,会带来不好的交互体验。

    Element 回到顶部源码分析

    <template>
      <transition name="el-fade-in">
        <div
          v-if="visible"
          @click.stop="handleClick"
          :style="{
            'right': styleRight,
            'bottom': styleBottom
          }"
          class="el-backtop">
          <slot>
            <el-icon name="caret-top"></el-icon>
          </slot>
        </div>
      </transition>
    </template>
    
    <script>
    import throttle from 'throttle-debounce/throttle';
    
    const cubic = value => Math.pow(value, 3);
    const easeInOutCubic = value => value < 0.5
      ? cubic(value * 2) / 2
      : 1 - cubic((1 - value) * 2) / 2;
    
    export default {
      name: 'ElBacktop',
    
      props: {
        visibilityHeight: {
          type: Number,
          default: 200
        },
        target: [String],
        right: {
          type: Number,
          default: 40
        },
        bottom: {
          type: Number,
          default: 40
        }
      },
    
      data() {
        return {
          el: null,
          container: null,
          visible: false
        };
      },
    
      computed: {
        styleBottom() {
          return `${this.bottom}px`;
        },
        styleRight() {
          return `${this.right}px`;
        }
      },
    
      mounted() {
        this.init(); // 初始化
        this.throttledScrollHandler = throttle(300, this.onScroll); // 节流
        this.container.addEventListener('scroll', this.throttledScrollHandler);
      },
    
      methods: {
        init() {
          this.container = document;
          this.el = document.documentElement;
          if (this.target) { // 如果存在target, 获取target dom作为el
            this.el = document.querySelector(this.target);
            if (!this.el) {
              throw new Error(`target is not existed: ${this.target}`);
            }
            this.container = this.el;
          }
        },
        onScroll() { 
          const scrollTop = this.el.scrollTop; // 获取滚动条高度
          this.visible = scrollTop >= this.visibilityHeight; // 判断是否达到可见的标准
        },
        handleClick(e) { // 点击事件处理函数
          this.scrollToTop();
          this.$emit('click', e); // 触发事件
        },
        scrollToTop() {
          const el = this.el; // 获取元素
          const beginTime = Date.now();// 获取当前时间戳
          const beginValue = el.scrollTop;
          const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16));
          const frameFunc = () => {
            const progress = (Date.now() - beginTime) / 500;
            if (progress < 1) { // 和0.5s做对比,小于0.5秒执行下面步骤
              el.scrollTop = beginValue * (1 - easeInOutCubic(progress));
              rAF(frameFunc);
            } else {
              el.scrollTop = 0;
            }
          };
          rAF(frameFunc);
        }
      },
    
      beforeDestroy() { // 销毁时,移除监听
        this.container.removeEventListener('scroll', this.throttledScrollHandler);
      }
    };
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110

    可以看到,除了对scroll监听加了节流外,其余的思想大致相同。主要分析scrollToTop函数

    scrollToTop函数分析

    scrollToTop() {
        const el = this.el; // 获取元素
        const beginTime = Date.now(); // 获取当前时间戳
        const beginValue = el.scrollTop; // 获取当前滚动条位置
        const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16)); // 注意点 1
        const frameFunc = () => {
          const progress = (Date.now() - beginTime) / 500;
          if (progress < 1) { // 和0.5s做对比,小于0.5秒执行下面步骤
            el.scrollTop = beginValue * (1 - easeInOutCubic(progress));
            rAF(frameFunc); // 循环
          } else {
            el.scrollTop = 0;
          }
        };
        rAF(frameFunc);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注意点1

    const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16));
    
    • 1

    调用循环时,首先判断了当前环境是否存在requestAnimationFrame,如果不存在,则使用性能稍微差点的setTimeout。

    优先使用requestAnimationFrame的主要优势时因为

    1.经过浏览器优化,动画更流畅
    2.窗口没激活时,动画将停止,省计算资源
    3.更省电,尤其是对移动终端

    requestAnimationFrame最大的优势是

    由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题.

  • 相关阅读:
    Nuxt3 中使用 ESLint
    Python手写人脸识别
    Python项目移到Linux环境下出现ModuleNotFoundError: No module named ‘xxx‘解决方案
    Chrome 浏览器经常卡死问题解决
    防静电门禁闸机管理系统的优点有哪些
    数据工程师&数据分析师这两个岗位有什么区别?有没有发展前景?
    【新版】系统架构设计师 - 软件架构设计<新版>
    Android学习笔记 84. 自适应布局
    全栈经验总结(不间断更新)
    全网最简约的Vscode配置Anaconda环境(百分百成功)
  • 原文地址:https://blog.csdn.net/qq_42683219/article/details/127888210