Vant2 源码分析之 vant-sticky

news2024/12/23 18:20:24

前言

原打算借鉴 vant-sticky 源码,实现业务需求的某个功能,第一眼看以为看懂了,拿来用的时候,才发现一知半解。看第二遍时,对不起,是我肤浅了。这里侧重分析实现原理,其他部分不拓展开来,否则像滚雪球越滚越多了。一边读源码,一边学习使用技巧吧,这里记录下心得感悟,和大家共勉。

接下来会分析这三个的源码实现,因为项目用的 Vue2,故参考 Vant2 的 v2.12.54 版本,

在这里插入图片描述
而该版本未实现 Vant3 的吸底距离功能,故不做分析,同学们交给你们啦。
在这里插入图片描述
如果只关注实现原理,不关注每个部分实现细节的话,可以跳到 onScroll 滚动事件部分。

项目启动和调试

clone 项目:

git clone https://github.com/youzan/vant.git

切换版本:

git checkout v2.12.54

安装和启动项目:
在这里插入图片描述

npm run bootstrap
npm run dev

调试过程中,可以打印些计算值,帮助理解

源码分析

找到 vant-sticky 目录后,开始我们的源码分析吧
在这里插入图片描述

html 部分

render() {
    const { fixed } = this;
    const style = {
      height: fixed ? `${this.height}px` : null,
    };
    return (
      <div style={style}> // 1
        // bem({ fixed }) 生成 'vant-sticky--fixed'
        <div class={bem({ fixed })} style={this.style}> // 2
          {this.slots()}
        </div>
      </div>
    );
  }

1 为包裹元素 用于占位,因为内部元素 class=‘vant-sticky–fixed’ 是用 fixed 实现的,会脱离文档流。
2 class 和 style 都是根据 fixed 去决定是否展示。如下可见 class=‘vant-sticky–fixed’ 内容是固定的,而 style 是计算属性,动态变化的。

因此,这里学习到的两个 技巧 是,

  • 元素使用 fixed 时,为了不影响滚动效果,布局错乱,可以包裹一个父元素去保持占位。
  • 由同个变量去控制一个元素的样式变化,而静态的样式放到 class 里,动态的放到 style 里。

css 部分

@import '../style/var';

.van-sticky {
  &--fixed {
    position: fixed;
    top: 0;
    right: 0;
    left: 0;
    z-index: @sticky-z-index; // @sticky-z-index: 99;
  }
}

@import ‘…/style/var’ 定义了 less 变量,@sticky-z-index: 99;

  computed: {
    style() {
      // 意味着 fixed 改变的同时, style 也改变了
      if (!this.fixed) {
        // 也就不设置 style 了,因为是动态响应 dom 元素的
        return;
      }

      const style = {};

      if (isDef(this.zIndex)) {
        // 修改层级,vant 默认在 vant-sticky--fixed 里变量定义为 99,这里通过传参修改
        style.zIndex = this.zIndex; 
      }

      if (this.offsetTopPx && this.fixed) {
        style.top = `${this.offsetTopPx}px`; // 通过设置 top,来设置偏移量
      }

      if (this.transform) {
        style.transform = `translate3d(0, ${this.transform}px, 0)`;
      }

      return style;
    },
  },

初始的生命周期部分

created 生命周期

created() {
    // compatibility: https://caniuse.com/#feat=intersectionobserver
    // vant2 使用 SSR 写的,故有 isServer 是否在服务器运行的判断
    // window.IntersectionObserver ie11 不支持
    if (!isServer && window.IntersectionObserver) {
      this.observer = new IntersectionObserver(
        // entries是一个数组,每个成员都是一个 IntersectionObserverEntry 对象
        // 有几个被观察的成员就有几个对象
        (entries) => {
          // 每次元素进入可视区 或 离开可视区时 触发
          if (entries[0].intersectionRatio > 0) {
            this.onScroll();
          }
        },
        // root 属性指定目标元素所在的容器节点(即根元素)
        { root: document.body }
      );
    }
  },

window.IntersectionObserver 自动观察元素是否可见(本质是目标元素与视口产生一个交叉区,只有线程空闲下来,才会执行观察器), 详见 阮一峰的 IntersectionObserver API 使用教程

后续会用到,虽然把 IntersectionObserver 相关部分全都注释掉,也不影响使用。

// 用法
this.observer = new IntersectionObserver(callback, option)

// 开始观察
this.observer.observe(this.$el);

// 停止观察
this.observer.unobserve(this.$el);

// 关闭观察器
this.observer.disconnect();

通过 mixins,混入生命周期函数 mounted、activated、deactivated、beforeDestroy 以绑定和取消监听事件

mixins: [
    BindEventMixin(function (bind, isBind) { // 1 BindEventMixin 建议先看下面的说明部分,再往下看
      if (!this.scroller) {
        this.scroller = getScroller(this.$el); // getScroller 从当前元素一直向上找到带有滚动属性的元素
      }
      // IntersectionObserver 的对象
      if (this.observer) {
        // 当绑定时,isBind 为 true,开始观察
        // 当取消监听时,isBind 为 false,停止观察
        const method = isBind ? 'observe' : 'unobserve'; 
        this.observer[method](this.$el);
      }
      // bind 即为 on( addEventListener)
      bind(this.scroller, 'scroll', this.onScroll, true);
      this.onScroll();
    }),
  ],

1 简单分析下 BindEventMixin 实现如下

import { on, off } from '../utils/dom/event';

let uid = 0;
// 入参 handler 是个函数
export function BindEventMixin(handler) {
  const key = `binded_${uid++}`; // 记录绑定
  
  function bind() {
    if (!this[key]) { // 没有绑定
      handler.call(this, on, true); // 把 on(即 addEventListener)传给 handler,第三个参数是告知 handler 当前状态是否绑定
      this[key] = true; // 标记绑定
    }
  }

  function unbind() {
    if (this[key]) { // 绑定了,则取消监听事件
      handler.call(this, off, false); // 把 off (即 removeEventListener )传给 handler
      this[key] = false; // 标记w未绑定
    }
  }
  // 通过 mixins,混入生命周期函数,以绑定和取消监听事件
  return {
    mounted: bind, 
    activated: bind,
    deactivated: unbind,
    beforeDestroy: unbind,
  };
}

因此这里学习到的 技巧 是,我们也可以通过 mixins 的方式去自动的绑定和取消监听事件。前提是,符合这些生命周期,需要一开始载入便监听的,但 watch 某个数据变化,去手动的监听和取消监听就不太适用了。当然,也可以依据情况改造下函数。

props 和 data 部分

简单看下传值和变量定义部分

  props: {
    zIndex: [Number, String], // 吸顶时的 z-index
    container: null, // 容器对应的 HTML 节点,类型 Element
    offsetTop: { // 吸顶时与顶部的距离,支持 px vw vh rem 单位,默认 px
      type: [Number, String],
      default: 0,
    },
  },

  data() {
    return {
      fixed: false,
      height: 0, // 元素本身高度
      transform: 0, // 偏移量,只在有容器,且展示吸底效果时,有用到
    };
  },

onScroll 滚动事件部分

先搞清楚几个概念:
scrollTop 为 滚动的距离
window.scrollTop:
在这里插入图片描述
在这里插入图片描述

getBoundingClientRect():其提供了元素的大小及其相对于视口的位置
el.getBoundingClientRect().top:
在这里插入图片描述
可以发现,在向上滚动的过程中,window.scrollTop 不断增加,el.getBoundingClientRect().top 不断减少。而增加的部分刚好等于减少的部分。

如果元素的顶部超出视口,那么 el.getBoundingClientRect().top 为负值,window.scrollTop 还是不断增加。

可以得出,在滚动的过程中, el.getBoundingClientRect().top + window.scrollTop 的值始终是不变的,也就是,元素初始的位置到视口顶部的距离,此时 window.scrollTop 为 0。

接下来是重中之重的 onScroll 滚动事件部分,先从 1、2 开始讲起
在这里插入图片描述
offsetHeight:一个元素本身的高度 + padding+border+滚动条,不包括伪元素
在这里插入图片描述
因此在上面的基础上,加上 el.offsetHeight,也就是元素的初始位置的底部到视口顶部的距离
el.getBoundingClientRect().top + window.scrollTop + el.offsetHeight

实现原理
scrollTop + offsetTopPx > topToPageTop
当页面滚动距离 + 偏移量 大于 目标元素一开始距离顶部的距离时,目标元素设置 fixed 属性,吸顶。至于偏移量,通过设置 top 属性去偏移。
当页面滚动距离 + 偏移量 小于 目标元素一开始距离定都的距离时,意味着滚回去了,那么移除 fixed 属性

  methods: {
    onScroll() {
      // 判断当前元素,及祖先元素是否隐藏了,隐藏了就不需要滚动了
      if (isHidden(this.$el)) {
        return;
      }

      this.height = this.$el.offsetHeight; // 当前元素的高度,可用于占位,一直不变的

	  // offsetTopPx() 方法将 px vw vh rem 单位传值转换为 px
      const { container, offsetTopPx } = this;
      // window 滚动的距离 window.scrollTop
      const scrollTop = getScrollTop(window);

      // getElementTop() 返回 el.getBoundingClientRect().top + window.scrollTop
      // 上面分析过,保持不变,也就是 元素一开始与顶部的距离
      const topToPageTop = getElementTop(this.$el);

      const emitScrollEvent = () => {
        this.$emit('scroll', {
          scrollTop,
          isFixed: this.fixed,
        });
      };

      // 先注释掉该部分后面讲解,目前的部分足够实现 1 2 效果
      // if (container) {
      //   ... 
      // }
      
      // 当滚动距离达到指定上限:页面滚动的距离+偏移 > 元素一开始与顶部的距离 
      // offsetTopPx 偏移,会用设置 top 来解决
      if (scrollTop + offsetTopPx > topToPageTop) {
        this.fixed = true; // 设置 fixed 属性,目标元素视口吸顶
        this.transform = 0; // 重置因吸底容器效果而产生的偏移 transform,后面会提到。
      } else {
        // 当滚回顶部时,取消 fixed
        this.fixed = false;
      }

      emitScrollEvent();
    },
  }

接下来,分析 3 指定容器的情况。
在这里插入图片描述
有点特殊的是,目标元素到达视口顶部时,需要吸顶。而视口顶部到容器底部的距离,小于目标元素时,应该吸底容器,如下图。
而在该特殊情况出现之前,页面滚动+偏移距离超出元素一开始到视口顶部距离时,吸顶(这部分和容器没有关系)。代码实现和 1 2 部分相同
在这里插入图片描述
如果在容器和元素之间再放个元素,是否也有吸底效果呢
在这里插入图片描述

<div ref="container" style="height: 150px; background-color: #fff">
  <van-button type="warning">假容器</van-button>
  <van-sticky :container="container" :offset-top="20">
    <van-button type="warning" style="margin-left: 215px">指定容器</van-button>
  </van-sticky>

在这里插入图片描述
看样子,这一版并不支持上述情况。因此,默认目标元素一开始的位置是在容器边缘。下面的源码分析,也就排除这一情况了。
在这里插入图片描述
实现原理
scrollTop + offsetTopPx + this.height > bottomToPageTop
当页面滚动距离 + 偏移 + 目标元素高度,超出了容器一开始的底部到视口顶部的距离

如果超出部分小于元素高度,则展示吸底效果。设置 fixed 吸顶,在通过 transfom 向上移动超出的距离,以达到吸底容器的效果。

如果完全超出元素高度,则消除所有静态、动态样式,回到原样。

下面部分代码,便是上述特殊吸底情况的分析。

  if (container) {
    // 借鉴上面的分析,排除不支持的情况后
    // el.getBoundingClientRect().top + window.scrollTop 一开始目标元素到视口顶部的距离
    // 加上 container.offsetHeight 容器自身的高度,为容器一开始从底部到视口顶部的距离
    const bottomToPageTop = topToPageTop + container.offsetHeight;
    
    // 页面滚动的距离+偏移+目标元素的高度 > 容器一开始从底部到顶部的距离
    // 意味着,如果保持 fixed 的状态,目标元素会超出容器底部,这时候应该让它吸底
    if (scrollTop + offsetTopPx + this.height > bottomToPageTop) {
      // 目标元素超出底部的距离 = 目标元素高度 + 页面滚动距离 - 容器一开始的底部到顶部的距离
      // 为什么不考虑偏移呢?因为此时视觉上已经超出容器底部了,不需要管偏移,而是要吸附容器底部了
      const distanceToBottom = this.height + scrollTop - bottomToPageTop;
      // 超出距离 < 元素高度
      // 没有全部超出,元素吸底展示
      if (distanceToBottom < this.height) {
        // 给个 fixed 吸顶,通过调整 transform 往上移动使得 视觉上元素到了容器的底部
        this.fixed = true;
        // 需往上移动的距离为,超出的距离 + top 值的大小(抵消掉 top 值,因为原先的 top 值还在)
        this.transform = -(distanceToBottom + offsetTopPx);
      } else {
        // 完全超出,解除 fixed
        // 意味着 class='van-sticky--fixed' 删除,动态的 style 返回 {} 
        this.fixed = false;
      }

      emitScrollEvent();
      return;
    }

在理解了上述原理后,为我们的业务增效吧。动手之前多思考,生搬硬套不可取。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/390119.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

轮转数组(每日一题)

目录 一、题目描述 二、题目分析 2.1 方法一 2.1.1 思路 2.1.2 代码 2.2 方法二 2.2.1 思路 2.2.2 代码 2.3 方法三 2.3.1 思路 2.3.2 代码 一、题目描述 oj链接&#xff1a;https://leetcode.cn/problems/rotate-array 给定一个整数数组 nums&#xff0c;将数组中的…

GDScript 导出变量 (Godot4.0)

概述 导出变量的功能在3.x版本中也是有的&#xff0c;但是4.0版本对其进行了语法上的改进。 导出变量在日常的游戏制作中提供节点的自定义参数化调节功能时非常有用&#xff0c;除此之外还用于自定义资源。 本文是&#xff08;Bilibili巽星石&#xff09;在4.0官方文档《GDScr…

手把手创建flask项目

Flask 框架流程 什么是Flask&#xff1a; Flask诞生于2010年, 使用python语言基于Werkzeug工具箱编写的轻量级Web开发框架 Flask本身相当于一个内核, 其他几乎所有的功能都要用到扩展(邮件:Flask-Mail, 用户认证:Flask-Login, 数据库:Flask-SQLAlchemy). Flask的核心在于Werkz…

在线图书借阅网站( Python +Vue 实现)

功能介绍 平台采用B/S结构&#xff0c;后端采用主流的Python语言进行开发&#xff0c;前端采用主流的Vue.js进行开发。 整个平台包括前台和后台两个部分。 前台功能包括&#xff1a;首页、图书详情页、用户中心模块。后台功能包括&#xff1a;总览、借阅管理、图书管理、分类…

unity知识点小结02

虚拟轴 虚拟轴就是一个数值在-11内的轴&#xff0c;这个数轴上重要的数值就是-1,0和1。当使用按键模拟一个完整的虚拟轴时需要用到两个按键&#xff0c;即将按键1设置为负轴按键&#xff0c;按键2设置为正轴按键。在没有按下任何按键的时候&#xff0c;虚拟轴的数值为0&#xf…

【UEFI基础】UEFI事件介绍

简述 在【UEFI基础】System Table和Architecture Protocols介绍Boot Service时提到有一部分与事件相关的接口&#xff0c;它们创建、触发、等待和关闭事件&#xff0c;来完成某些功能&#xff0c;本文将进一步介绍事件。 需要注意&#xff0c;因为Boot Service需要在DXE阶段才…

用数组名作函数参数的详解,以及形参实参采用数组名,形参实参采用指针变量的几种情况解析

关于地址&#xff0c;指针&#xff0c;指针变量可以参考我的这篇文章&#xff1a; 地址&#xff0c;指针&#xff0c;指针变量是什么&#xff1f;他们的区别&#xff1f;符号&#xff08;*&#xff09;在不同位置的解释&#xff1f;_juechen333的博客-CSDN博客https://blog.csd…

Kali的安装与配置

虚拟机安装kali Kali下载 官网下载地址 注&#xff1a;下载VMware版本 百度网盘 提取码&#xff1a;Chen 创建虚拟机 将下载的压缩包放到合适的位置解压 双击运行虚拟机 登录 默认的账号密码都为kali 基本配置 修改root账户密码 打开命令行输入 sudo su root 输入kali 输…

【机器学习】验证集loss震荡(loss的其他问题)

训练过程中发现&#xff0c;train loss一直下降&#xff0c;train acc一直上升&#xff1b;但是val loss、val acc却一直震荡。loss一会上一会下&#xff0c;但是总体趋势是向下的。 “loss震荡但验证集准确率总体下降” 如何解决&#xff1f; 测试集准确率这样震荡是正常的吗…

python2.7/3.8版本安装教程

Wiondos-Python环境安装 Python2.7 下载地址 官网 速度比较慢 百度网盘 提取码:Chen 安装Python2.7 直接next 选择安装目录 注意这一步将最后一项勾选 安装完成 cmd中输入python 检查pip是否安装 cmd中输入pip --version Python3.8 下载地址 官网 速度比较慢 百度网…

蓝桥杯C/C++程序设计 往届真题汇总(进阶篇)

文章目录1. 最短路2. 数字三角形3. 递增序列4. 杨辉三角形5. 跳跃6. 路径7. 迷宫8. 装饰珠9. 明码10. 字串分值11. 作物杂交12. 承压计算13. 全球变暖14. 直线15. 平面切分1. 最短路 题目描述&#xff1a; 如下图所示&#xff0c;G是一个无向图&#xff0c;其中蓝色边的长度是…

线程池执行父子任务,导致线程死锁

前言&#xff0c; 一次线程池的不当使用&#xff0c;导致了现场出现了线程死锁&#xff0c;接口一直不返回。而且由于这是一个公共的线程池&#xff0c;其他使用了次线程池的业务也一直阻塞&#xff0c;系统出现了OOM&#xff0c;不过是幸好是线程同事测试出来的&#xff0c;没…

RPC通信原理解析

一、什么是RPC框架&#xff1f; RPC&#xff0c;全称为Remote Procedure Call&#xff0c;即远程过程调用&#xff0c;是一种计算机通信协议。 比如现在有两台机器&#xff1a;A机器和B机器&#xff0c;并且分别部署了应用A和应用B。假设此时位于A机器上的A应用想要调用位于B机…

第十一届蓝桥杯大赛青少组国赛Python真题2

第十一届蓝桥杯大赛青少组Python 真题 第二题 提示信息&#xff1a; 杨辉三角形&#xff0c;是二项式系数在三角形中的一种几何排列。中国南宋数学家杨辉在 1261 年所著的《详 解九章算法》一书有明确记载。欧洲数学家帕斯卡在 1654 年发现这一规律&#xff0c;所以又叫做帕斯卡…

Rabbit快速入门

入门案例 需求&#xff1a;使用简单模式完成消息传递 步骤&#xff1a; 创建工程&#xff08;生成者、消费者&#xff09; 分别添加依赖 编写生产者发送消息 编写消费者接收消息 3.1.2. 添加依赖 往heima-rabbitmq的pom.xml文件中添加如下依赖&#xff1a; <dependenc…

RabbitMQ的安装和配置

注意: 请使用资料里提供的CentOS-7-x86_64-DVD-1810.iso 安装虚拟机. 1. 安装依赖环境 在线安装依赖环境&#xff1a; yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c kernel-devel m4 ncurses-devel tk tc xz2. 安装Erlang 上…

【完整版】国内网络编译,Ambari 2.7.6 全部模块源码编译笔记

本次编译 ambari 2.7.6 没有使用科学上网的工具,使用的普通网络,可以编译成功,过程比 ambari 2.7.5 编译时要顺畅。 以下是笔记完整版。如果想单独查看本篇编译笔记,可参考:《Ambari 2.7.6 全部模块源码编译笔记》 该版本相对 2.7.5 版本以来,共有 26 个 contributors …

使用labelImg标注自己的VOC数据集

文章目录1.下载labelImg2.准备文件夹3.打开软件4.软件使用1.下载labelImg 步骤&#xff1a;WindowsR打开运行界面→输入cmd打开命令行窗口→输入pip install labelImg命令&#xff08;前提是python版本在3.0以上并安装anaconda&#xff0c;如果没有安装anaconda&#xff0c;输…

cmd窗口中java命令报错。错误:找不到或无法加载主类 java的jdk安装过程中踩过的坑

错误: 找不到或无法加载主类 HelloWorld 遇到这个问题时&#xff0c;我尝试过网上其他人的做法。有试过添加classpath&#xff0c;也有试过删除classpath。但是依然报错&#xff0c;这里javac可以编译通过&#xff0c;说明代码应该是没有问题的。只是在运行是出现了错误。我安装…

卷积神经网络的原理及实现

专栏&#xff1a;神经网络复现目录 卷积神经网络 本章介绍的卷积神经网络&#xff08;convolutional neural network&#xff0c;CNN&#xff09;是一类强大的、为处理图像数据而设计的神经网络。 基于卷积神经网络架构的模型在计算机视觉领域中已经占主导地位&#xff0c;当今…