Backtop

4 阅读10分钟
<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
    },
    // 滚动容器的选择器,默认为 document.documentElement
    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();
    // 创建节流函数,限制滚动事件触发频率为300ms一次
    this.throttledScrollHandler = throttle(300, this.onScroll);
    // 绑定滚动事件监听器
    this.container.addEventListener('scroll', this.throttledScrollHandler);
  },

  methods: {
    // 初始化方法,设置滚动容器和目标元素
    init() {
      this.container = document;
      this.el = document.documentElement;
      // 如果指定了target属性,则查找对应的元素
      if (this.target) {
        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();
      // 触发click事件,传递事件对象
      this.$emit('click', e);
    },
    // 平滑滚动到顶部的方法
    scrollToTop() {
      const el = this.el;
      const beginTime = Date.now();
      const beginValue = el.scrollTop;
      // 使用requestAnimationFrame实现平滑动画,兼容性回退到setTimeout
      const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16));
      const frameFunc = () => {
        // 计算动画进度,总时长500ms
        const progress = (Date.now() - beginTime) / 500;
        if (progress < 1) {
          // 根据缓动函数计算当前滚动位置
          el.scrollTop = beginValue * (1 - easeInOutCubic(progress));
          rAF(frameFunc);
        } else {
          // 动画结束,滚动到顶部
          el.scrollTop = 0;
        }
      };
      rAF(frameFunc);
    }
  },

  beforeDestroy() {
    // 组件销毁前移除滚动事件监听器,防止内存泄漏
    this.container.removeEventListener('scroll', this.throttledScrollHandler);
  }
};
</script>

Backtop

组件概述

Backtop 是一个返回顶部按钮组件,当页面滚动超过指定高度时,会显示一个按钮,点击后可以平滑滚动回页面顶部。该组件支持自定义滚动容器、按钮位置和显示阈值。


一、模板部分(Template)

1.1 过渡动画

<transition name="el-fade-in">

作用:使用 Vue 的内置过渡组件 transition,配合 el-fade-in 类名实现按钮的淡入淡出效果。

原理:当 visible 属性变化时,Vue 会自动添加/移除相应的 CSS 类,实现平滑的显示/隐藏动画。

1.2 按钮容器

<div
  v-if="visible"
  @click.stop="handleClick"
  :style="{
    'right': styleRight,
    'bottom': styleBottom
  }"
  class="el-backtop">

属性解析

  • v-if="visible":条件渲染,只有当 visibletrue 时才渲染按钮
  • @click.stop="handleClick":点击事件处理,.stop 修饰符阻止事件冒泡
  • :style:动态绑定样式,通过计算属性设置按钮的位置
    • right:距离右侧的距离
    • bottom:距离底部的距离
  • class="el-backtop":基础样式类名

1.3 插槽内容

<slot>
  <el-icon name="caret-top"></el-icon>
</slot>

作用

  • 使用 <slot> 提供默认内容,允许父组件自定义按钮图标
  • 默认显示一个向上箭头图标(caret-top
  • 如果父组件提供了内容,则使用父组件的内容替换默认图标

二、脚本部分(Script)

2.1 导入依赖

import throttle from 'throttle-debounce/throttle';

作用:导入节流函数库,用于优化滚动事件处理。

为什么需要节流

  • 滚动事件触发频率非常高(每秒可能触发几十次)
  • 频繁触发会导致性能问题
  • 节流可以限制函数执行频率,提高性能

2.2 缓动函数

const cubic = value => Math.pow(value, 3);
const easeInOutCubic = value => value < 0.5
  ? cubic(value * 2) / 2
  : 1 - cubic((1 - value) * 2) / 2;

作用:实现平滑的滚动动画效果。

详细解析

  1. cubic 函数:三次贝塞尔曲线,计算 value³

    • 输入范围:0 到 1
    • 输出范围:0 到 1
    • 曲线形状:加速效果
  2. easeInOutCubic 函数:缓入缓出效果

    • 前 50%:加速(从慢到快)
    • 后 50%:减速(从快到慢)
    • 这种效果让滚动看起来更自然、更舒适

动画原理

  • 动画总时长:500ms
  • 根据当前进度(0 到 1)计算当前位置
  • 使用缓动函数使速度变化平滑

2.3 组件定义

export default {
  name: 'ElBacktop',

作用:定义组件名称,用于在 Vue DevTools 中识别组件。

2.4 Props 属性

2.4.1 visibilityHeight
visibilityHeight: {
  type: Number,
  default: 200
}

作用:设置按钮显示的滚动高度阈值。

说明

  • 类型:数字
  • 默认值:200 像素
  • 当滚动距离 ≥ 200px 时,按钮才会显示

使用场景

  • 页面较短时,不需要显示返回顶部按钮
  • 只有用户滚动一定距离后,才显示按钮
2.4.2 target
target: [String]

作用:指定滚动容器的选择器。

说明

  • 类型:字符串
  • 可选属性
  • 默认值:document.documentElement(整个页面)

使用示例

<!-- 监听整个页面的滚动 -->
<el-backtop></el-backtop>

<!-- 监听特定容器的滚动 -->
<el-backtop target=".scroll-container"></el-backtop>
2.4.3 right
right: {
  type: Number,
  default: 40
}

作用:设置按钮距离右侧的位置。

说明

  • 类型:数字
  • 默认值:40 像素
  • 单位:px
2.4.4 bottom
bottom: {
  type: Number,
  default: 40
}

作用:设置按钮距离底部的位置。

说明

  • 类型:数字
  • 默认值:40 像素
  • 单位:px

2.5 Data 数据

data() {
  return {
    el: null,           // 滚动元素
    container: null,    // 容器元素,用于绑定事件
    visible: false      // 控制按钮的显示/隐藏状态
  };
}

作用:定义组件的响应式数据。

详细解析

  1. el:存储实际的滚动元素

    • 默认:document.documentElement
    • 如果指定了 target,则为对应的 DOM 元素
  2. container:存储事件绑定的容器

    • 默认:document
    • 如果指定了 target,则为 el 本身
  3. visible:控制按钮显示状态

    • false:隐藏按钮
    • true:显示按钮

2.6 Computed 计算属性

2.6.1 styleBottom
styleBottom() {
  return `${this.bottom}px`;
}

作用:将 bottom 属性值转换为 CSS 样式字符串。

说明

  • 返回格式:"40px"
  • 用于动态绑定到按钮的 bottom 样式
2.6.2 styleRight
styleRight() {
  return `${this.right}px`;
}

作用:将 right 属性值转换为 CSS 样式字符串。

说明

  • 返回格式:"40px"
  • 用于动态绑定到按钮的 right 样式

2.7 生命周期钩子

2.7.1 mounted
mounted() {
  this.init();
  this.throttledScrollHandler = throttle(300, this.onScroll);
  this.container.addEventListener('scroll', this.throttledScrollHandler);
}

作用:组件挂载完成后执行初始化操作。

执行步骤

  1. 初始化:调用 init() 方法设置滚动容器
  2. 创建节流函数
    • 使用 throttle(300, this.onScroll) 创建节流函数
    • 限制滚动事件每 300ms 最多触发一次
  3. 绑定事件
    • 在容器上添加滚动事件监听器
    • 使用节流函数作为事件处理器

为什么在 mounted 中初始化

  • 此时 DOM 已经渲染完成
  • 可以安全地访问和操作 DOM 元素
  • 可以正确绑定事件监听器

2.8 Methods 方法

2.8.1 init 方法
init() {
  this.container = document;
  this.el = document.documentElement;
  if (this.target) {
    this.el = document.querySelector(this.target);
    if (!this.el) {
      throw new Error(`target is not existed: ${this.target}`);
    }
    this.container = this.el;
  }
}

作用:初始化滚动容器和目标元素。

执行逻辑

  1. 默认设置

    • 容器:document
    • 滚动元素:document.documentElement(整个页面)
  2. 自定义 target

    • 如果提供了 target 属性
    • 使用 querySelector 查找对应的 DOM 元素
    • 如果找不到元素,抛出错误
    • 将滚动元素和容器都设置为找到的元素

错误处理

  • 如果指定的 target 不存在,抛出明确的错误信息
  • 帮助开发者快速定位问题
2.8.2 onScroll 方法
onScroll() {
  const scrollTop = this.el.scrollTop;
  this.visible = scrollTop >= this.visibilityHeight;
}

作用:滚动事件处理函数,控制按钮的显示/隐藏。

执行逻辑

  1. 获取滚动位置this.el.scrollTop

    • 返回元素当前滚动的垂直距离
  2. 判断是否显示按钮

    • 如果 scrollTop >= visibilityHeight,设置 visible = true
    • 否则,设置 visible = false

性能优化

  • 该方法被节流函数包装,不会频繁执行
  • 每次执行都很轻量,性能开销小
2.8.3 handleClick 方法
handleClick(e) {
  this.scrollToTop();
  this.$emit('click', e);
}

作用:按钮点击事件处理函数。

执行逻辑

  1. 滚动到顶部:调用 scrollToTop() 方法
  2. 触发事件:向父组件发送 click 事件,传递事件对象

使用场景

  • 父组件可以监听 @click 事件
  • 可以在点击时执行额外的逻辑

示例

<el-backtop @click="handleBacktopClick"></el-backtop>
2.8.4 scrollToTop 方法
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) {
      el.scrollTop = beginValue * (1 - easeInOutCubic(progress));
      rAF(frameFunc);
    } else {
      el.scrollTop = 0;
    }
  };
  rAF(frameFunc);
}

作用:平滑滚动到顶部。

详细解析

  1. 初始化变量

    • el:滚动元素
    • beginTime:动画开始时间
    • beginValue:初始滚动位置
  2. 创建动画帧函数

    • rAF:使用 requestAnimationFrame 或回退到 setTimeout(16ms)
    • requestAnimationFrame:浏览器优化的动画 API,约 60fps
  3. 动画帧函数

    • 计算进度:(Date.now() - beginTime) / 500
      • 总时长:500ms
      • 进度范围:0 到 1
    • 如果进度 < 1:
      • 使用缓动函数计算当前位置
      • el.scrollTop = beginValue * (1 - easeInOutCubic(progress))
      • 递归调用下一帧
    • 如果进度 ≥ 1:
      • 直接设置到顶部:el.scrollTop = 0
  4. 启动动画:调用 rAF(frameFunc)

动画效果

  • 前 250ms:加速滚动
  • 后 250ms:减速滚动
  • 总时长:500ms
  • 效果:平滑、自然的滚动体验

2.9 生命周期钩子

2.9.1 beforeDestroy
beforeDestroy() {
  this.container.removeEventListener('scroll', this.throttledScrollHandler);
}

作用:组件销毁前清理事件监听器。

重要性

  • 防止内存泄漏
  • 避免组件销毁后事件仍然触发
  • 良好的编程实践

三、组件使用示例

3.1 基础用法

<template>
  <div>
    <el-backtop></el-backtop>
  </div>
</template>

说明:使用默认配置,滚动超过 200px 时显示按钮。

3.2 自定义阈值

<template>
  <div>
    <el-backtop :visibility-height="300"></el-backtop>
  </div>
</template>

说明:滚动超过 300px 时才显示按钮。

3.3 自定义位置

<template>
  <div>
    <el-backtop :right="100" :bottom="100"></el-backtop>
  </div>
</template>

说明:按钮距离右侧和底部都是 100px。

3.4 自定义容器

<template>
  <div class="scroll-container" style="height: 400px; overflow: auto;">
    <div style="height: 2000px;">
      长内容...
    </div>
    <el-backtop target=".scroll-container"></el-backtop>
  </div>
</template>

说明:只在 .scroll-container 容器内滚动时才显示按钮。

3.5 自定义图标

<template>
  <div>
    <el-backtop>
      <i class="el-icon-arrow-up"></i>
    </el-backtop>
  </div>
</template>

说明:使用自定义图标替换默认的向上箭头。

3.6 监听点击事件

<template>
  <div>
    <el-backtop @click="handleClick"></el-backtop>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick(e) {
      console.log('返回顶部按钮被点击', e);
    }
  }
}
</script>

说明:在点击时执行额外的逻辑。


四、核心知识点总结

4.1 Vue 组件开发

  1. 组件结构:Template + Script
  2. Props 定义:类型检查、默认值
  3. 生命周期:mounted、beforeDestroy
  4. 事件处理:自定义事件、事件修饰符
  5. 插槽:默认插槽、内容分发

4.2 性能优化

  1. 节流(Throttle)

    • 限制函数执行频率
    • 适用于高频事件(scroll、resize)
    • 提高性能,减少不必要的计算
  2. requestAnimationFrame

    • 浏览器优化的动画 API
    • 约 60fps 的刷新率
    • 比 setTimeout 更流畅、更省电

4.3 动画原理

  1. 缓动函数

    • easeInOutCubic:缓入缓出
    • 使动画更自然、更舒适
  2. 动画实现

    • 基于时间的动画
    • 递归调用 requestAnimationFrame
    • 根据进度计算当前位置

4.4 DOM 操作

  1. scroll 事件

    • 监听滚动位置
    • scrollTop 属性获取滚动距离
  2. 元素查找

    • querySelector 查找 DOM 元素
    • 错误处理:元素不存在时抛出错误
  3. 事件管理

    • addEventListener 绑定事件
    • removeEventListener 移除事件
    • 防止内存泄漏

4.5 用户体验

  1. 平滑滚动

    • 不是瞬间跳转
    • 有动画过渡
    • 提升用户体验
  2. 智能显示

    • 只在需要时显示
    • 避免干扰用户
    • 节省屏幕空间
  3. 自定义能力

    • 可配置位置
    • 可自定义图标
    • 可监听事件

五、常见问题

5.1 为什么按钮不显示?

可能原因

  1. 页面滚动距离未达到 visibilityHeight 阈值
  2. target 选择器不正确
  3. 容器高度不够,无法滚动

解决方法

  • 检查滚动距离
  • 验证 target 选择器
  • 确保容器有足够的内容可以滚动

5.2 如何调整滚动速度?

方法:修改 scrollToTop 方法中的动画时长。

// 将 500 改为其他值(单位:毫秒)
const progress = (Date.now() - beginTime) / 500;

示例

  • 300ms:快速滚动
  • 1000ms:慢速滚动

5.3 如何自定义缓动效果?

方法:修改 easeInOutCubic 函数。

示例:线性滚动(匀速)

const easeInOutCubic = value => value;

示例:弹性效果

const easeOutElastic = x => {
  const c4 = (2 * Math.PI) / 3;
  return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1;
};

5.4 为什么需要节流?

原因

  • scroll 事件触发频率极高(每秒几十次)
  • 频繁触发会导致性能问题
  • 节流可以限制执行频率

效果

  • 从每秒几十次降低到每秒 3-4 次
  • 大幅提升性能
  • 用户体验不受影响

六、扩展建议

6.1 功能扩展

  1. 支持水平滚动:添加 horizontal 属性
  2. 自定义动画时长:添加 duration 属性
  3. 多种缓动效果:添加 easing 属性
  4. 显示滚动百分比:在按钮上显示滚动进度

6.2 样式扩展

  1. 圆形/方形按钮:添加 shape 属性
  2. 自定义颜色:添加 color 属性
  3. 阴影效果:添加 shadow 属性
  4. 悬停效果:添加 hover 样式

6.3 交互扩展

  1. 长按快速滚动:添加长按事件
  2. 拖拽定位:允许用户拖拽按钮
  3. 键盘快捷键:支持快捷键触发
  4. 触摸手势:支持移动端手势

七、总结

Backtop 组件是一个简单但功能完善的返回顶部按钮组件。通过学习这个组件,我们掌握了:

  1. Vue 组件开发的基本流程
  2. 性能优化的技巧(节流、requestAnimationFrame)
  3. 动画实现的原理(缓动函数、帧动画)
  4. 事件管理的最佳实践
  5. 用户体验设计的重要性