Vant2 源码分析之 vant-sticky
前言
原打算借鑒 vant-sticky 源碼,實(shí)現(xiàn)業(yè)務(wù)需求的某個(gè)功能,第一眼看以為看懂了,拿來(lái)用的時(shí)候,才發(fā)現(xiàn)一知半解。看第二遍時(shí),對(duì)不起,是我膚淺了。這里側(cè)重分析實(shí)現(xiàn)原理,其他部分不拓展開(kāi)來(lái),否則像滾雪球越滾越多了。一邊讀源碼,一邊學(xué)習(xí)使用技巧吧,這里記錄下心得感悟,和大家共勉。
接下來(lái)會(huì)分析這三個(gè)的源碼實(shí)現(xiàn),因?yàn)轫?xiàng)目用的 Vue2,故參考 Vant2 的 v2.12.54 版本,
而該版本未實(shí)現(xiàn) Vant3 的吸底距離功能,故不做分析,同學(xué)們交給你們啦。
如果只關(guān)注實(shí)現(xiàn)原理,不關(guān)注每個(gè)部分實(shí)現(xiàn)細(xì)節(jié)的話(huà),可以跳到 onScroll 滾動(dòng)事件部分。
項(xiàng)目啟動(dòng)和調(diào)試
clone 項(xiàng)目:
git clone https://github.com/youzan/vant.git切換版本:
git checkout v2.12.54安裝和啟動(dòng)項(xiàng)目:
調(diào)試過(guò)程中,可以打印些計(jì)算值,幫助理解
源碼分析
找到 vant-sticky 目錄后,開(kāi)始我們的源碼分析吧
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 為包裹元素 用于占位,因?yàn)閮?nèi)部元素 class=‘vant-sticky–fixed’ 是用 fixed 實(shí)現(xiàn)的,會(huì)脫離文檔流。
2 class 和 style 都是根據(jù) fixed 去決定是否展示。如下可見(jiàn) class=‘vant-sticky–fixed’ 內(nèi)容是固定的,而 style 是計(jì)算屬性,動(dòng)態(tài)變化的。
因此,這里學(xué)習(xí)到的兩個(gè) 技巧 是,
- 元素使用 fixed 時(shí),為了不影響滾動(dòng)效果,布局錯(cuò)亂,可以包裹一個(gè)父元素去保持占位。
- 由同個(gè)變量去控制一個(gè)元素的樣式變化,而靜態(tài)的樣式放到 class 里,動(dòng)態(tài)的放到 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 改變的同時(shí), style 也改變了if (!this.fixed) {// 也就不設(shè)置 style 了,因?yàn)槭莿?dòng)態(tài)響應(yīng) dom 元素的return;}const style = {};if (isDef(this.zIndex)) {// 修改層級(jí),vant 默認(rèn)在 vant-sticky--fixed 里變量定義為 99,這里通過(guò)傳參修改style.zIndex = this.zIndex; }if (this.offsetTopPx && this.fixed) {style.top = `${this.offsetTopPx}px`; // 通過(guò)設(shè)置 top,來(lái)設(shè)置偏移量}if (this.transform) {style.transform = `translate3d(0, ${this.transform}px, 0)`;}return style;},},初始的生命周期部分
created 生命周期
created() {// compatibility: https://caniuse.com/#feat=intersectionobserver// vant2 使用 SSR 寫(xiě)的,故有 isServer 是否在服務(wù)器運(yùn)行的判斷// window.IntersectionObserver ie11 不支持if (!isServer && window.IntersectionObserver) {this.observer = new IntersectionObserver(// entries是一個(gè)數(shù)組,每個(gè)成員都是一個(gè) IntersectionObserverEntry 對(duì)象// 有幾個(gè)被觀察的成員就有幾個(gè)對(duì)象(entries) => {// 每次元素進(jìn)入可視區(qū) 或 離開(kāi)可視區(qū)時(shí) 觸發(fā)if (entries[0].intersectionRatio > 0) {this.onScroll();}},// root 屬性指定目標(biāo)元素所在的容器節(jié)點(diǎn)(即根元素){ root: document.body });}},window.IntersectionObserver 自動(dòng)觀察元素是否可見(jiàn)(本質(zhì)是目標(biāo)元素與視口產(chǎn)生一個(gè)交叉區(qū),只有線(xiàn)程空閑下來(lái),才會(huì)執(zhí)行觀察器), 詳見(jiàn) 阮一峰的 IntersectionObserver API 使用教程
后續(xù)會(huì)用到,雖然把 IntersectionObserver 相關(guān)部分全都注釋掉,也不影響使用。
// 用法 this.observer = new IntersectionObserver(callback, option)// 開(kāi)始觀察 this.observer.observe(this.$el);// 停止觀察 this.observer.unobserve(this.$el);// 關(guān)閉觀察器 this.observer.disconnect();通過(guò) mixins,混入生命周期函數(shù) mounted、activated、deactivated、beforeDestroy 以綁定和取消監(jiān)聽(tīng)事件
mixins: [BindEventMixin(function (bind, isBind) { // 1 BindEventMixin 建議先看下面的說(shuō)明部分,再往下看if (!this.scroller) {this.scroller = getScroller(this.$el); // getScroller 從當(dāng)前元素一直向上找到帶有滾動(dòng)屬性的元素}// IntersectionObserver 的對(duì)象if (this.observer) {// 當(dāng)綁定時(shí),isBind 為 true,開(kāi)始觀察// 當(dāng)取消監(jiān)聽(tīng)時(shí),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 簡(jiǎn)單分析下 BindEventMixin 實(shí)現(xiàn)如下
import { on, off } from '../utils/dom/event';let uid = 0; // 入?yún)?handler 是個(gè)函數(shù) export function BindEventMixin(handler) {const key = `binded_${uid++}`; // 記錄綁定function bind() {if (!this[key]) { // 沒(méi)有綁定handler.call(this, on, true); // 把 on(即 addEventListener)傳給 handler,第三個(gè)參數(shù)是告知 handler 當(dāng)前狀態(tài)是否綁定this[key] = true; // 標(biāo)記綁定}}function unbind() {if (this[key]) { // 綁定了,則取消監(jiān)聽(tīng)事件handler.call(this, off, false); // 把 off (即 removeEventListener )傳給 handlerthis[key] = false; // 標(biāo)記w未綁定}}// 通過(guò) mixins,混入生命周期函數(shù),以綁定和取消監(jiān)聽(tīng)事件return {mounted: bind, activated: bind,deactivated: unbind,beforeDestroy: unbind,}; }因此這里學(xué)習(xí)到的 技巧 是,我們也可以通過(guò) mixins 的方式去自動(dòng)的綁定和取消監(jiān)聽(tīng)事件。前提是,符合這些生命周期,需要一開(kāi)始載入便監(jiān)聽(tīng)的,但 watch 某個(gè)數(shù)據(jù)變化,去手動(dòng)的監(jiān)聽(tīng)和取消監(jiān)聽(tīng)就不太適用了。當(dāng)然,也可以依據(jù)情況改造下函數(shù)。
props 和 data 部分
簡(jiǎn)單看下傳值和變量定義部分
props: {zIndex: [Number, String], // 吸頂時(shí)的 z-indexcontainer: null, // 容器對(duì)應(yīng)的 HTML 節(jié)點(diǎn),類(lèi)型 ElementoffsetTop: { // 吸頂時(shí)與頂部的距離,支持 px vw vh rem 單位,默認(rèn) pxtype: [Number, String],default: 0,},},data() {return {fixed: false,height: 0, // 元素本身高度transform: 0, // 偏移量,只在有容器,且展示吸底效果時(shí),有用到};},onScroll 滾動(dòng)事件部分
先搞清楚幾個(gè)概念:
scrollTop 為 滾動(dòng)的距離
window.scrollTop:
getBoundingClientRect():其提供了元素的大小及其相對(duì)于視口的位置
el.getBoundingClientRect().top:
可以發(fā)現(xiàn),在向上滾動(dòng)的過(guò)程中,window.scrollTop 不斷增加,el.getBoundingClientRect().top 不斷減少。而增加的部分剛好等于減少的部分。
如果元素的頂部超出視口,那么 el.getBoundingClientRect().top 為負(fù)值,window.scrollTop 還是不斷增加。
可以得出,在滾動(dòng)的過(guò)程中, el.getBoundingClientRect().top + window.scrollTop 的值始終是不變的,也就是,元素初始的位置到視口頂部的距離,此時(shí) window.scrollTop 為 0。
接下來(lái)是重中之重的 onScroll 滾動(dòng)事件部分,先從 1、2 開(kāi)始講起
offsetHeight:一個(gè)元素本身的高度 + padding+border+滾動(dòng)條,不包括偽元素
因此在上面的基礎(chǔ)上,加上 el.offsetHeight,也就是元素的初始位置的底部到視口頂部的距離
el.getBoundingClientRect().top + window.scrollTop + el.offsetHeight
實(shí)現(xiàn)原理:
scrollTop + offsetTopPx > topToPageTop
當(dāng)頁(yè)面滾動(dòng)距離 + 偏移量 大于 目標(biāo)元素一開(kāi)始距離頂部的距離時(shí),目標(biāo)元素設(shè)置 fixed 屬性,吸頂。至于偏移量,通過(guò)設(shè)置 top 屬性去偏移。
當(dāng)頁(yè)面滾動(dòng)距離 + 偏移量 小于 目標(biāo)元素一開(kāi)始距離定都的距離時(shí),意味著滾回去了,那么移除 fixed 屬性
接下來(lái),分析 3 指定容器的情況。
有點(diǎn)特殊的是,目標(biāo)元素到達(dá)視口頂部時(shí),需要吸頂。而視口頂部到容器底部的距離,小于目標(biāo)元素時(shí),應(yīng)該吸底容器,如下圖。
而在該特殊情況出現(xiàn)之前,頁(yè)面滾動(dòng)+偏移距離超出元素一開(kāi)始到視口頂部距離時(shí),吸頂(這部分和容器沒(méi)有關(guān)系)。代碼實(shí)現(xiàn)和 1 2 部分相同
如果在容器和元素之間再放個(gè)元素,是否也有吸底效果呢
看樣子,這一版并不支持上述情況。因此,默認(rèn)目標(biāo)元素一開(kāi)始的位置是在容器邊緣。下面的源碼分析,也就排除這一情況了。
實(shí)現(xiàn)原理:
scrollTop + offsetTopPx + this.height > bottomToPageTop
當(dāng)頁(yè)面滾動(dòng)距離 + 偏移 + 目標(biāo)元素高度,超出了容器一開(kāi)始的底部到視口頂部的距離
如果超出部分小于元素高度,則展示吸底效果。設(shè)置 fixed 吸頂,在通過(guò) transfom 向上移動(dòng)超出的距離,以達(dá)到吸底容器的效果。
如果完全超出元素高度,則消除所有靜態(tài)、動(dòng)態(tài)樣式,回到原樣。
下面部分代碼,便是上述特殊吸底情況的分析。
if (container) {// 借鑒上面的分析,排除不支持的情況后// el.getBoundingClientRect().top + window.scrollTop 一開(kāi)始目標(biāo)元素到視口頂部的距離// 加上 container.offsetHeight 容器自身的高度,為容器一開(kāi)始從底部到視口頂部的距離const bottomToPageTop = topToPageTop + container.offsetHeight;// 頁(yè)面滾動(dòng)的距離+偏移+目標(biāo)元素的高度 > 容器一開(kāi)始從底部到頂部的距離// 意味著,如果保持 fixed 的狀態(tài),目標(biāo)元素會(huì)超出容器底部,這時(shí)候應(yīng)該讓它吸底if (scrollTop + offsetTopPx + this.height > bottomToPageTop) {// 目標(biāo)元素超出底部的距離 = 目標(biāo)元素高度 + 頁(yè)面滾動(dòng)距離 - 容器一開(kāi)始的底部到頂部的距離// 為什么不考慮偏移呢?因?yàn)榇藭r(shí)視覺(jué)上已經(jīng)超出容器底部了,不需要管偏移,而是要吸附容器底部了const distanceToBottom = this.height + scrollTop - bottomToPageTop;// 超出距離 < 元素高度// 沒(méi)有全部超出,元素吸底展示if (distanceToBottom < this.height) {// 給個(gè) fixed 吸頂,通過(guò)調(diào)整 transform 往上移動(dòng)使得 視覺(jué)上元素到了容器的底部this.fixed = true;// 需往上移動(dòng)的距離為,超出的距離 + top 值的大小(抵消掉 top 值,因?yàn)樵鹊?top 值還在)this.transform = -(distanceToBottom + offsetTopPx);} else {// 完全超出,解除 fixed// 意味著 class='van-sticky--fixed' 刪除,動(dòng)態(tài)的 style 返回 {} this.fixed = false;}emitScrollEvent();return;}在理解了上述原理后,為我們的業(yè)務(wù)增效吧。動(dòng)手之前多思考,生搬硬套不可取。
總結(jié)
以上是生活随笔為你收集整理的Vant2 源码分析之 vant-sticky的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 微信小程序3-模板与配置
- 下一篇: 【iOS开发】微信读书-组件化方案探索