Vant2 源码分析之 vant-sticky

article/2025/9/22 18:23:45

前言

原打算借鉴 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;}

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


http://chatgpt.dhexx.cn/article/fat8W1ob.shtml

相关文章

position:sticky

1、position:sticky简介 设置了position:sticky的元素并不会脱离文档流。 当元素在区域内&#xff0c;元素不受定位的影响&#xff08;top、left等设置无效&#xff09;当发生滚动&#xff0c;元素就要移出区域时&#xff0c;定位又会变成fixed&#xff0c;根据设置的left、t…

css粘性定位position:sticky

前言&#xff1a; css3中有一个很有趣的属性position:sticky&#xff0c;粘性属性&#xff0c;效果是一个吸顶效果&#xff0c;可以说是相对定位relative和固定定位fixed的结合&#xff1b;它主要用在对scroll事件的监听上&#xff1b;简单来说&#xff0c;在滑动过程中&#x…

position:sticky 粘性定位

1、什么是粘性定位&#xff1f; 粘性定位它基于用户的滚动位置来定位。 粘性定位的元素是依赖于用户的滚动&#xff0c;在 position:relative 与 position:fixed 定位之间切换。 它的行为就像 position:relative; 而当页面滚动超出目标区域时&#xff0c;它的表现就像 position…

position: sticky 属性

关于 position 属性主要有五个值&#xff1a; static relative fixed absolute sticky 常见的就是 relative 和 absolute 这两个属性。 relative 表示&#xff0c;相对于默认位置&#xff08;即static时的位置&#xff09;进行偏移&#xff0c;即定位基点是元素的默认位置。 ab…

position的属性(sticky属性)

position的属性 对position所有的属性的介绍sticky属性定义demosticky生效以及失效 fixed属性定义demo absolute属性定义demo relative属性定义demo static属性定义 对position所有的属性的介绍 https://developer.mozilla.org/zh-CN/docs/Web/CSS/position sticky属性 定义…

css定位(二)---css中粘性定位(sticky)---C3新增属性

作用&#xff1a;相对定位&#xff08;position:relative&#xff09;和固定定位&#xff08;position:fixed&#xff09;的混合。sticky相当于加了一个滚动事件的处理&#xff0c;在滑动过程中&#xff0c;某个元素距离其父元素的距离达到sticky粘性定位的要求时(比如top&…

sticky

sticky position属性中最有意思的就是sticky了&#xff0c;设置了sticky的元素&#xff0c;在屏幕范围&#xff08;viewport&#xff09;时该元素的位置并不受到定位影响&#xff08;设置是top、left等属性无效&#xff09;&#xff0c;当该元素的位置将要移出偏移范围时&#…

sticky android,Sticky Warriors

《Sticky Warriors》是一款休闲格斗冒险类游戏&#xff0c;各种动画还有游戏当中的角色都会在这款游戏当中出现&#xff0c;简单但是非常硬核的操作&#xff0c;此外你还可以选择战斗的场景&#xff0c;更能烘托战斗的氛围&#xff0c;丰富多样的战斗模式玩法可以体验&#xff…

Mac破解百度云

https://github.com/CodeTips/BaiduNetdiskPlugin-macOS 转载于:https://www.cnblogs.com/wt645631686/p/10910576.html

百度云不限速下载最新破解真好用

大家肯定遇到过这种情况,家里的宽带明明就是 百兆光纤&#xff0c;为什么用百度云下载速度却是可怜几十kb/s? 其他工具下载速度却是正常&#xff0c;其实大家都知道这个是百度云限速了,为了让你冲会员,不择手段!!! 房贷要还&#xff0c;还要吃饭&#xff0c;该省的一分都不想…

百度云破解不限速版(绿色免安装)

百度云限速导致下载教程耗费大量的时间&#xff0c;给各位分享一个破解版的百度云。 百度云破解不限速版&#xff1a; 下载地址&#xff1a;链接: http://pan.baidu.com/s/1nuAF6LN 密码: c7jv 如果链接失效了直接向我反馈 下载地址&#xff1a;http://feixueteam.net/th…

破解版百度云【亲测好用】完美解决限速

现在百度云由于各种限制&#xff0c;下载速度非常的慢&#xff0c;今天用百度下载东西&#xff0c;感觉太慢&#xff0c;在网上无意间发现了一款不限速版的百度云&#xff0c;好的工具在这里一定要和大家分享了。&#xff0c; 咱们先来看看下载速度&#xff0c;我这里应该是10…

[不限速百度云]两款百度网盘破解版分享

Pandownload 简介 这个破解版百度云可以说是很经典了&#xff0c;由吾爱破解大神倾情制作。 缺点是使用自己的账号&#xff0c;容易被度受限速&#xff0c;好几天才能恢复 特别说明&#xff1a;使用前请登录百度账号&#xff01; 下崽链接 https://www.lanzous.com/i51s9wf…

极速下载!利用插件破解百度云限速

首发地址&#xff1a;【原创达人】极速下载&#xff01;利用插件破解百度云限速 http://club.lenovo.com.cn/thread-2781576-1-1.html (出处: 联想社区) 百度网盘真的是个好东西&#xff0c;但他让人又爱又恨&#xff0c;容量大&#xff0c;资源多给我们带来了很大的方便&…

X Chen笔记---百度云破解限速

http://blog.csdn.net/qq_29922333/article/details/55051619 http://chromecj.com/utilities/2015-04/423.html 原理 使用User-Agent-Switcher-for-Chrome这个插件&#xff0c;将window的浏览器模拟成safari浏览器&#xff0c;就可以不使用云盘来下载文件&#xff0c;使用…

从零开始速通百度云网盘

原文链接&#xff1a;CCR39のLazyNest 相信大家在网上冲浪时时常会下载一些资源、软件&#xff0c;如果你需要下载别人分享的资源时&#xff0c;就需要用到网盘。 百度云网盘由于早期在国内占据了一定规模的市场&#xff0c;拥有大量的资源与链接&#xff0c;直至目前&#x…

“油猴脚本”获取百度云文件URL,百度云速度破解

用通俗的语言来说&#xff0c;油猴脚本是一个浏览器插件&#xff0c;它本身也可以添加很多不同的脚本&#xff0c;是浏览器上的一个脚本管理集成工具。 我们来看一下&#xff0c;它在Chrome 插件市场已经1000 w 的下载了&#xff0c;也看出它的受欢迎程度。 借助所编写的脚本它…

破解网页版 百度云网盘无法下载大文件限制

问题描述&#xff1a; 百度云网盘网页版下载文件时&#xff0c;由于文件过大&#xff0c;点击下载&#xff0c;提示安装客户端下载。我们不安装客户端而继续使用网页版下载打文件呢&#xff1f; 方法如下&#xff1a; ---------------我用的chrome浏览器&#xff0c;其他的浏览…

免费的百度网盘批量转存软件工具

之前说的一个自动赚钱的项目&#xff0c;用到的工具就是这个一键转存工具。 项目文章《揭秘灰色项目&#xff1a;自动化引色流暴利变现方式》 此外的话百度账号也是很便宜的&#xff0c;也可以自己用软件注册&#xff0c;这个就不多说了。 这款软件免费的破解的&#xff0c;…

百度网盘+7.9+android,百度云7.9.0永不升级版

百度云7.9.0永不升级版是一款非常专业的云存储平台。在这里你可以随时随地在移动客户端和伙伴们快乐分享视频、照片&#xff0c;支持多条件筛选查找&#xff0c;群主智能管理&#xff0c;化繁为简随心阅览&#xff0c;让你的文件一目了然&#xff0c;备份数据、保存私密文件就选…