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安裝和啟動項目:
調試過程中,可以打印些計算值,幫助理解
源碼分析
找到 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 屬性
接下來,分析 3 指定容器的情況。
有點特殊的是,目標元素到達視口頂部時,需要吸頂。而視口頂部到容器底部的距離,小于目標元素時,應該吸底容器,如下圖。
而在該特殊情況出現之前,頁面滾動+偏移距離超出元素一開始到視口頂部距離時,吸頂(這部分和容器沒有關系)。代碼實現和 1 2 部分相同
如果在容器和元素之間再放個元素,是否也有吸底效果呢
看樣子,這一版并不支持上述情況。因此,默認目標元素一開始的位置是在容器邊緣。下面的源碼分析,也就排除這一情況了。
實現原理:
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的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信小程序3-模板与配置
- 下一篇: 【iOS开发】微信读书-组件化方案探索