Vant2 源码分析之 vant-sticky
前言
原打算借鉴 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 )传给 handlerthis[key] = false; // 标记w未绑定}}// 通过 mixins,混入生命周期函数,以绑定和取消监听事件return {mounted: bind, activated: bind,deactivated: unbind,beforeDestroy: unbind,};
}
因此这里学习到的 技巧 是,我们也可以通过 mixins 的方式去自动的绑定和取消监听事件。前提是,符合这些生命周期,需要一开始载入便监听的,但 watch 某个数据变化,去手动的监听和取消监听就不太适用了。当然,也可以依据情况改造下函数。
props 和 data 部分
简单看下传值和变量定义部分
props: {zIndex: [Number, String], // 吸顶时的 z-indexcontainer: null, // 容器对应的 HTML 节点,类型 ElementoffsetTop: { // 吸顶时与顶部的距离,支持 px vw vh rem 单位,默认 pxtype: [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 单位传值转换为 pxconst { container, offsetTopPx } = this;// window 滚动的距离 window.scrollTopconst 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 {// 当滚回顶部时,取消 fixedthis.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;}
在理解了上述原理后,为我们的业务增效吧。动手之前多思考,生搬硬套不可取。
相关文章:

Vant2 源码分析之 vant-sticky
前言 原打算借鉴 vant-sticky 源码,实现业务需求的某个功能,第一眼看以为看懂了,拿来用的时候,才发现一知半解。看第二遍时,对不起,是我肤浅了。这里侧重分析实现原理,其他部分不拓展开来&…...

【自然语言处理】【大模型】大语言模型BLOOM推理工具测试
相关博客 【自然语言处理】【大模型】大语言模型BLOOM推理工具测试 【自然语言处理】【大模型】GLM-130B:一个开源双语预训练语言模型 【自然语言处理】【大模型】用于大型Transformer的8-bit矩阵乘法介绍 【自然语言处理】【大模型】BLOOM:一个176B参数…...

云桌面技术初识:VDI,IDV,VOI,RDS
VDI(Virtual Desktop Infrastucture,虚拟桌面架构),俗称虚拟云桌面 VDI构架采用的“集中存储、集中运算”构架,所有的桌面以虚拟机的方式运行在服务器硬件虚拟化层上,桌面以图像传输的方式发送到客户端。 …...
基于本地centos构建gdal2.4.4镜像
1.前言 基于基础镜像构建gdal环境一般特别大,一般少则1.6G,多则2G甚至更大,这对于镜像的迁移造成了极大的不便。究其原因在于容器中有大量的源码文件以及编译中间过程文件,还要大量编译需要的yum库。本文主要通过在centos系统上先…...
生产环境线程问题排查
线程状态的解读RUNNABLE线程处于运行状态,不一定消耗CPU。例如,线程从网络读取数据,大多数时间是挂起的,只有数据到达时才会重新唤起进入执行状态。只有Java代码显式调用sleep或wait方法时,虚拟机才可以精准获取到线程…...

Day908.joinsnljdist和group问题和备库自增主键问题 -MySQL实战
join&snlj&dist和group问题和备库自增主键问题 Hi,我是阿昌,今天学习记录的是关于join&snlj&dist和group问题和备库自增主键问题的内容。 一、join 的写法 join 语句怎么优化?中,在介绍 join 执行顺序的时候&am…...
算法 - 剑指Offer 丑数
题目 我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。 解题思路 这题我使用最简单方法去做, 首先我们可以获取所有2n,3n,5*n的丑数,只是我们这里暂时无法排序,并且可能…...

【ONE·C || 文件操作】
总言 C语言:文件操作。 文章目录总言1、文件是什么?为什么需要文件?1.1、为什么需要文件?1.2、文件是什么?2、文件的打开与关闭2.1、文件指针2.2、文件打开和关闭:fopen、fclose2.3、文件使用方式3、文…...

cmd窗口中java命令报错。错误:找不到或无法加载主类 java的jdk安装过程中踩过的坑
错误: 找不到或无法加载主类 HelloWorld 遇到这个问题时,我尝试过网上其他人的做法。有试过添加classpath,也有试过删除classpath。但是依然报错,这里javac可以编译通过,说明代码应该是没有问题的。只是在运行是出现了错误。我安装…...
Breathwork(呼吸练习)
查了下呼吸练习相关内容,做个记录。我又在油管学习啦。 喜欢在you. tube看一些self-help相关的内容。比如学习方法、拉伸、跑步、力量举、自重锻炼等等。 总是听Obi Vicent说起Breathwork,比如: My 6am Morning Routine | New Healthy Habit…...

taobao.itemprops.get( 获取标准商品类目属性 )
¥开放平台基础API不需用户授权 通过设置必要的参数,来获取商品后台标准类目属性,以及这些属性里面详细的属性值prop_values。 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 请求参数 点…...

QT配置安卓环境(保姆级教程)
目录 下载环境资源 JDK1.8 NDK SDK 安装QT 配置环境 下载环境资源 JDK1.8 介绍JDK是Java开发的核心工具,为Java开发者提供了一套完整的开发环境,包括开发工具、类库和API等,使得开发者可以高效地编写、测试和运行Java应用程序。 下载…...

【uni-app教程】八、UniAPP Vuex 状态管理
八、UniAPP Vuex 状态管理 概念 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 应用场景 Vue多个组件之间需要共享数据或状态。 关键规则 State:…...
同花顺测试面经(30min)
大概三十分钟,面试官人还挺好的 1.自我介绍 2.详细问你了自我介绍中的一个实习经历 3.对我们公司有什么了解 !!(高频) 4.对测试有什么看法,为什么选测试 5.黑盒白盒分别是什么 6.对测试左移有什么看法…...
C++-简述#ifdef、#else、#endif和#ifndef的作用
回答如下: #ifdef,#else,#endif和#ifndef都是预处理指令,用于条件编译。#ifdef:这个指令用来判断一个宏是否已经被定义过,如果已经定义过,则执行后面的代码块。#else:这个指令一般与…...
VictoriaMetrics 集群部署
官网 ## 官网 https://github.com/VictoriaMetrics/VictoriaMetrics 集群角色详解 VictoriaMetrics 集群模式。主要由 vmstorage ,vminsert,vmselect 三部分组成,这三个组件每个组件都可以单独进行扩展。其中: vmstorage 负责提供数据存储服务vminsert 是数据存…...

【基于感知损失的无监督泛锐化】
PercepPan: Towards Unsupervised Pan-Sharpening Based on Perceptual Loss (PercepPan:基于感知损失的无监督泛锐化) 在基于神经网络的全色锐化文献中,作为地面实况标签的高分辨率多光谱图像通常是不可用的。为了解决这个问题…...
在vercel上用streamlit部署网站
Verce和Streamlit都是非常流行的Web应用程序部署平台。以下是从零开始在Vercel上部署Streamlit应用程序的一些基本步骤。 安装 Streamlit 在本地计算机上安装Streamlit。可以轻松地通过在命令行中运行以下命令来安装: pip install streamlit为 Streamlit 应用程序…...
华为OD机试题 - 斗地主(JavaScript)| 含思路
更多题库,搜索引擎搜 梦想橡皮擦华为OD 👑👑👑 更多华为OD题库,搜索引擎搜 梦想橡皮擦 华为OD 👑👑👑 更多华为机考题库,搜索引擎搜 梦想橡皮擦华为OD 👑👑👑 华为OD机试题 最近更新的博客使用说明本篇题解:斗地主题目输入输出描述示例一输入输出示例二输…...
i.MX8MP平台开发分享(clock篇)-计算clock速度相关的内核API
专栏目录:专栏目录传送门 平台内核i.MX8MP5.15.71文章目录 clk消费者clk生产者clk_set_rateclk_round_rateclk_pll1443x_recalc_rate这一篇我们具体来看看其他驱动如何使用clock,这里以lcdif驱动为例。 IMX8MP_CLK_MEDIA_BLK_CTRL_LCDIF_PIXEL是门控时钟,名为pix,这个门控时…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...

FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...

淘宝扭蛋机小程序系统开发:打造互动性强的购物平台
淘宝扭蛋机小程序系统的开发,旨在打造一个互动性强的购物平台,让用户在购物的同时,能够享受到更多的乐趣和惊喜。 淘宝扭蛋机小程序系统拥有丰富的互动功能。用户可以通过虚拟摇杆操作扭蛋机,实现旋转、抽拉等动作,增…...

保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!
目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...
全面解析数据库:从基础概念到前沿应用
在数字化时代,数据已成为企业和社会发展的核心资产,而数据库作为存储、管理和处理数据的关键工具,在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理,到社交网络的用户数据存储,再到金融行业的交易记录处理&a…...