使用getBoundingClientRect方法实现简洁的sticky组件的方法

sticky组件,通常应用于导航条或者工具栏,当网页在某一区域滚动的时候,将导航条或工具栏这类元素固定在页面顶部或底部,方便用户快速进行这类元素提供的操作。本文介绍这种组件的实现思路,并提供一个同时支持将sticky元素固定在顶部或底部的具体实现,由于这种组件在网站中非常常见,所以有必要掌握它的实现方式,以便在有需要的时候基于它的思路写出功能更多的组件出来。

固定在顶部的demo效果(对应sticky-top.html):

固定在底部的demo效果(对应sticky-bottom.html):

1. 实现思路

实现这个组件的关键在于找到元素何时被固定以及何时被取消固定的临界点,要找到这个临界点,首先要详细看看前面demo的变化过程。在前面的demo中,有一个导航条元素,也就是我们要控制固定与否的元素,我把它称为sticky元素;还有一个元素,它用来显示网页的一块列表内容,这个列表元素跟sticky元素在功能上是相关的,因为sticky元素要导航的正是这个列表元素提供的内容,本文在开始介绍sticky组件的功能时,就说过sticky组件固定是发生在网页滚动至某一区域的时候,离开这一区域就会取消固定,这个滚动区域或者说滚动范围,就是由列表元素来决定的,所以这个列表元素是找到临界点的关键,它表示sticky组件可被固定的网页滚动范围,为了后面引用方便,我把这个元素称为target元素。下面就来详细了解下前面demo的变化过程,由于固定在底部的情况与固定在顶部的情况实现思路是相通的,如果弄明白了固定在顶部的实现原理,相信你也一定能弄明白固定在底部的实现原理,所以这里也是为了减少篇幅,提高效率,仅仅介绍固定在顶部的情况:

一开始sticky元素和target元素的状态是这样的:

当滚动条慢慢向下,使得网页向上滚动的时候,sticky元素和target元素在一段滚动距离内状态并没有发生变化,一直到这个状态(滚动条滚动距离为573px):

在这个状态只要滚动条再往下滚动1px,sticky元素就会被固定在顶部(滚动条滚动距离为574px):

也就是说当target元素的顶部离浏览器顶部的距离小于0的时候(target元素的顶部未超出浏览器顶部的时候,距离看作大于0),sticky元素就会被固定,所以这就是我们要找的第一个临界点。然后滚动条继续向下滚动,只要target元素还在浏览器可视区域内,sticky元素就会一直被固定:

直到这个状态(滚动条滚动距离为1861px):

在这个状态只要滚动条再往下滚动1px,sticky元素就会取消固定在顶部(滚动条滚动距离为1862px):

显然,这就是我们要找的第2个临界点,不过它的判断条件是:当target元素的底部离浏览器顶部的距离小于sticky元素的高度时,sticky元素就会被取消固定。这里为什么是小于sticky元素的高度,而不是小于0,原因是因为基于小于0这个临界点开发出来的组件,会出现target元素几乎快从浏览器可视区域消失了,但是sticky元素还固定在那的效果:

sticky还把footer的内容给盖住了,本来是为了方便用户操作,结果影响了用户操作,所以得把取消固定这个临界点提前,而用sticky元素的高度最合适。

通过前面对demo变化过程的拆解,我们已经得到了滚动条一直向下滚动时,sticky状态变化的两个临界点:

1)当target元素的顶部离浏览器顶部的距离小于0的时候,sticky元素就会被固定;

2)当target元素的底部离浏览器顶部的距离小于sticky元素的高度时,sticky元素就会被取消固定。

综合这两个临界点,可以得出滚动条向下滚动时,sticky元素被固定的滚动范围的判断条件是:target元素的顶部离浏览器顶部的距离小于0 并且 target元素的底部离浏览器顶部的距离大于sticky元素的高度。而且这个判断条件,同样适用于滚动条向上滚动的情况,因为滚动条一直向上滚动时,sticky状态变化的临界点是:

1)当target元素的底部离浏览器顶部的距离大于sticky元素的高度时,sticky元素就会被固定;

2)当target元素的顶部离浏览器顶部的距离大于0的时候,sticky元素就会被取消固定。

(这两个临界点,其实跟滚动条向下滚动时提到的两个临界点,是一个意思,只不过是正话反着说而已)

所以只要得到【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,【sticky元素的高度】这三个值基本上就能实现这个组件了。这三个值中sticky元素的高度由设计图决定,它从网页一开始制作就是已知的,在定义组件的时候我们可以从外部传进去,虽然也能从js去获取它的高度,不过显然没有必要增加额外的计算;另外两个值【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,我们正好可以利用DOM提供的一个方法来获取,这个方法是:getBoundingClientRect,这是一个兼容性很好的方法,它的调用方式是:

var target = document.getElementById('main-container');
var rect = target.getBoundingClientRect();
console.log(rect);

返回一个ClientRect对象,这个对象存储元素框模型的一些信息,比如它的宽高度(width and height),以及元素框上下边距离浏览器顶部边缘的距离(top and bottom),左右边距离浏览器左边缘的距离(left and right):

top跟bottom恰恰就是我们要获取的【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,而且当框的顶部或底部未超出浏览器顶部的时候,top跟bottom都是大于0的值,而当框的顶部或底部超出浏览器顶部的时候,top跟bottom是小于0的值:

当我们找到了【target元素的顶部离浏览器顶部的距离】,【target元素的底部离浏览器顶部的距离】,【sticky元素的高度】这三个值,就可以用代码来描述前面的判断条件:

rect.top < 0 && (rect.bottom - stickyHeight) > 0;

(rect表示target元素调用getBoundingClientRect返回的对象,stickyHeight表示sticky元素的高度)

最后为了让实现思路更加完整,虽然不详细介绍固定在底部的情况的变化过程,我还是把这种情况的临界点跟判断方式补充进来,它的临界点是(这里列的是滚动条向下滚动时的临界点):

1)当target元素的顶部离浏览器顶部的距离 + sticky元素的高度 小于浏览器可视区域的高度时,sticky元素被固定;

2)当target元素的底部离浏览器的顶部的距离小于浏览器可视区域的高度时,sticky元素被取消固定。

浏览器可视区域的高度,可用document.documentElement.clientHeight来获取,这个属性也是没有兼容性问题的,判断代码为:

var docClientWidth = document.documentElement.clientHeight;
rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth;

2. 实现细节

1)html结构

固定在顶部的html结构:

<div class="container-fluid sticky-wrapper">
<ul id="sticky" data-target="#main-container" class="sticky nav nav-pills">
<li role="presentation" class="active"><a href="#">Home</a></li>
<li role="presentation"><a href="#">Profile</a></li>
<li role="presentation"><a href="#">Messages</a></li>
</ul>
</div>
<div id="main-container" class="container-fluid">
<div class="row">
...
</div>
...
</div>

固定在底部的html结构:

<div id="main-container" class="container-fluid">
<div class="row">
...
</div>
...
</div>
<div class="container-fluid sticky-wrapper">
<ul id="sticky" data-target="#main-container" class="sticky nav nav-pills">
<li role="presentation" class="active"><a href="#">Home</a></li>
<li role="presentation"><a href="#">Profile</a></li>
<li role="presentation"><a href="#">Messages</a></li>
</ul>
</div>

以上#main-container就是我们的target元素,#sticky就是我们的sticky元素,还需要注意两点:

a. 顺序问题,两种结构中,target元素与sticky的父元素顺序位置是反的;

b. sticky元素外面必须包裹一层元素,而且还得给这一层元素设置height属性:

.sticky-wrapper {
margin-bottom: 10px;
height: 52px;
}

这是因为当sticky元素被固定的时候,它会脱离普通文档流,所以要利用它的父元素把sticky元素的高度在普通文档流中撑起来,以免在固定效果出现的时候,target元素的内容出现跳动的情况。

2)固定效果

让一个元素固定在浏览器的某个位置,当然是通过position: fixed来弄,所以可以用两个css类来实现固定在顶部和固定在底部的效果:

.sticky--in-top,.sticky--in-bottom {
position: fixed;
z-index: 1000;
}
.sticky--in-top {
top: 0;
}
.sticky--in-bottom {
bottom: 0;
}

当我们判断元素需要被固定在顶部的时候,就给它添加.sticky--in-top的css类;当我们判断元素需要被固定在底部的时候,就给它添加.sticky--in-bottom的css类。

3)滚动回调

控制sticy元素固定的逻辑显然要写在window的scroll事件回调中(有了前面对实现思路以及判断条件的说明,相信理解下面这段代码应该会很容易):

固定在顶部的回调逻辑:

$(window).scroll(function() {
var rect = $target[0].getBoundingClientRect();
if (rect.top < 0 && (rect.bottom - stickyHeight) > 0) {
!$elem.hasClass('sticky--in-top') && $elem.addClass('sticky--in-top').css('width', stickyWidth + 'px');
} else {
$elem.hasClass('sticky--in-top') && $elem.removeClass('sticky--in-top').css('width', 'auto');
}
});

其中:$target是target元素的jq对象,$elem是sticky元素的jq对象,stickyHeight是sticky元素的高度,stickyWidth是sticky元素的宽度。由于sticky元素固定时,脱离原来的文档流,需要设置宽度才能显示跟固定前一样的宽度。

固定在底部的回调逻辑:

$(window).scroll(function() {
var rect = $target[0].getBoundingClientRect(),
docClientWidth = document.documentElement.clientHeight;
if (rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth) {
!$elem.hasClass('sticky--in-bottom') && $elem.addClass('sticky--in-bottom').css('width', stickyWidth + 'px');
} else {
$elem.hasClass('sticky--in-bottom') && $elem.removeClass('sticky--in-bottom').css('width', 'auto');
}
});

这里是为了把回调逻辑说的更清楚才把代码分成两份,最后给的实现会把这两个代码合并成一份:)

4)函数节流

函数节流通常应用于window的scroll事件,resize事件以及普通元素的mousemove事件,因为这些事件由于鼠标或滚轮操作很频繁,会导致回调连续触发,如果回调里面含有DOM操作,这种连续调用就会影响页面的性能,所以很有必要控制这类回调的执行次数,函数节流就是做这个的,我这里提供了一个很简单的函数节流实现:

function throttle(func, wait) {
var timer = null;
return function() {
var self = this,
args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
return typeof func === 'function' && func.apply(self, args);
}, wait);
}
}

这个函数可以控制func所指定的函数,执行的间隔指定为wait指定的毫秒数,利用它,我们可以把前面的滚动回调改动一下,比如固定在顶部的情况改成:

$(window).scroll(throttle(function() {
var rect = $target[0].getBoundingClientRect(),
docClientWidth = document.documentElement.clientHeight;
if (rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth) {
!$elem.hasClass('sticky--in-bottom') && $elem.addClass('sticky--in-bottom').css('width', stickyWidth + 'px');
} else {
$elem.hasClass('sticky--in-bottom') && $elem.removeClass('sticky--in-bottom').css('width', 'auto');
}
}, 50);

其实真正处理回调的是throttle返回的函数,这个返回的函数逻辑少,而且没有DOM操作,它是会被连续调用的,但是不影响页面性能,而我们真正处理逻辑的那个函数,也就是传入throttle的那个函数因为throttle创建的闭包的作用,不会被连续调用,这样就实现了控制函数执行次数的目的。

5)resize的问题

window resize总是在定义组件的时候带来问题,因为页面可视区域的宽高度发生了变化,sticky元素的父容器宽度也可能发生了变化,而且resize的时候不会触发scroll事件,所以我们需要在resize回调内,刷新sticky元素的宽度以及重新调用固定效果的逻辑,这个相关的代码就不贴出来了,后面直接看整体实现吧,否则我怕放出来会影响理解。总之resize是我们在定义组件的时候肯定要考虑的,不过一般都放到最后来处理,有点算处理BUG之类的工作。

3. 整体实现

代码比较简洁:

/**
* @param elem: jquery选择器,用来获取要被固定的元素
* @param opts:
* - target: jquery选择器,用来获取表示固定范围的元素
* - type: top|bottom,表示要固定的位置
* - height: 要固定的元素的高度,由于高度在做页面时就是确定的并且几乎不会被DOM操作改变,直接从外部传入可以除去获取元素高度的操作
* - wait: 滚动事件回调的节流时间,控制回调至少隔多长时间才执行一次
* - getStickyWidth:获取要固定元素的宽度,window resize或者DOM操作会导致固定元素的宽度发生变化,需要这个回调来刷新stickyWidth
*/
var Sticky = function (elem, opts) {
var $elem = $(elem), $target = $(opts.target || $elem.data('target'));
if (!$elem.length || !$target.length) return;
var stickyWidth, $win = $(window),
stickyHeight = opts.height || $elem[0].offsetHeight,
rules = {
top: function (rect) {
return rect.top < 0 && (rect.bottom - stickyHeight) > 0;
},
bottom: function (rect) {
var docClientWidth = document.documentElement.clientHeight;
return rect.bottom > docClientWidth && (rect.top + stickyHeight) < docClientWidth;
}
},
type = (opts.type in rules) && opts.type || 'top',
className = 'sticky--in-' + type;
refreshStickyWidth();
$win.scroll(throttle(sticky, $.isNumeric(opts.wait) && parseInt(opts.wait) || 100));
$win.resize(throttle(function () {
refreshStickyWidth();
sticky();
}, 50));
function refreshStickyWidth() {
stickyWidth = typeof opts.getStickyWidth === 'function' && opts.getStickyWidth($elem) || $elem[0].offsetWidth;
$elem.hasClass(className) && $elem.css('width', stickyWidth + 'px');
}
//效果实现
function sticky() {
if (rules[type]($target[0].getBoundingClientRect())) {
!$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth + 'px');
} else {
$elem.hasClass(className) && $elem.removeClass(className).css('width', 'auto');
}
}
//函数节流
function throttle(func, wait) {
var timer = null;
return function () {
var self = this, args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
return typeof func === 'function' && func.apply(self, args);
}, wait);
}
}
};

调用方式,固定在顶部的情况(type选项默认为top):

<script>
new Sticky('#sticky',{
height: 52,
getStickyWidth: function($elem){
return ($elem.parent()[0].offsetWidth - 30);
}
});
</script>

固定在底部的情况:

<script>
new Sticky('#sticky',{
height: 52,
type: 'bottom',
getStickyWidth: function($elem){
return ($elem.parent()[0].offsetWidth - 30);
}
});
</script>

还有一个要说明的是,opts的getStickyWidth选项,这个回调用来获取sticky元素的宽度,为什么要把它放出来,通过外部去获取宽度,而不是在组件内部通过offsetWidth获取?是因为当sticky元素的外部容器是自适应的时候,sticky元素固定时的宽度不是由sticky元素自己决定的,而是依赖于外部容器的宽度,所以这个宽度只能在外部去获取,内部获取不准确。比如上面的代码中我减了一个30,如果在组件内部获取的话,我肯定不知道要添加减30这样一个逻辑。

4. 总结

本文提供了一个很常见的sticky组件实现,实现这个组件的关键在于找到控制sticky元素固定与否的关键点,同时在实现的时候函数节流跟window resize的问题需要特别注意。

我一直认为对于一些简单的组件,掌握它的思路,自己去定义比直接从github上去找开源的插件要来的更切实际:

1)代码可控,不用去阅读别人的代码,有问题也能快速修改

2)代码量小,开源的插件会尽可能多做事,而有些工作你的项目并不一定需要它去做;

3)更贴合项目的实际需求,跟第2点差不多的意思,在已有的思路基础上,我们能开发出与项目需求完全契合的功能模块;

4)有助于提高自己的技术水平,增进知识的广度和深度;

所以有能力造轮子的时候,造造也是很有必要的。

本文虽然在最后提供了整体的组件实现,但是并不是建议拿来就用,否则前面大篇幅地去介绍实现思路就没有必要了,我只要放个github地址即可,思路远比实现重要。我最近几篇博客都是在分享思路,而不是分享某个具体的实现,思路这种抽象的东西是通用的,理解前它不是你的,理解后它就存在于脑袋里,任何时候都可以拿来就用,我提供的思路也同样来自于我对其它博客其它插件源码学习之后的思考与总结。

补充于说明:

本文实现有不足,不完美的地方,请在了解本文相关内容后,移步阅读《sticky组件的改进实现》了解更佳的实现。

(0)

相关推荐

  • js getBoundingClientRect() 来获取页面元素的位置

    document.documentElement.getBoundingClientRect 下面这是MSDN的解释: Syntax oRect = object.getBoundingClientRect() Return Value Returns a TextRectangle object. Each rectangle has four integer properties (top, left, right, and bottom) that represent a coordina

  • 浅谈Sticky组件的改进实现

    在上一篇文章使用getBoundingClientRect方法实现简洁的sticky组件的方法介绍了一个sticky组件的简洁实现,经过这两天的思考,发现上次提供的实现还有较多不足的地方,另外跟别的网站上实现的效果在取消固定的时候也有一些不同,上次提供的取消固定的处理方式不好,本文在上文的基础上,提供一个改进版的sticky组件,功能更加完善,希望您有兴趣阅读. 1. 旧版本的问题 上一个sticky组件的实现中,有多个问题存在: 第一,从sticky的效果上来说,sticky元素在固定前后,不

  • javascript getBoundingClientRect() 来获取页面元素的位置的代码[修正版]第1/2页

    document.documentElement.getBoundingClientRect下面这是MSDN的解释: Syntax oRect = object.getBoundingClientRect()Return Value Returns a TextRectangle object. Each rectangle has four integer properties (top, left, right, and bottom) that represent a coordinate

  • javascript 获取元素位置的快速方法 getBoundingClientRect()

    它返回一个对象,其中包含了left.right.top.bottom四个属性,分别对应了该元素的左上角和右下角相对于浏览器窗口(viewport)左上角的距离. 所以,网页元素的相对位置就是 var X= this.getBoundingClientRect().left; var Y =this.getBoundingClientRect().top; 再加上滚动距离,就可以得到绝对位置 var X= this.getBoundingClientRect().left+document.doc

  • 各种常用浏览器getBoundingClientRect的解析

    先上测试代码 复制代码 代码如下: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

  • 使用Sticky组件实现带sticky效果的tab导航和滚动导航的方法

    sticky组件,通常应用于导航条或者工具栏,当网页在某一区域滚动的时候,将导航条或工具栏这类元素固定在页面顶部或底部,方便用户快速进行这类元素提供的操作. 在这篇文章Sticky组件的改进实现提供了一个改进版的sticky组件,并将演示效果应用到了自己的博客.有了类似sticky的这种简单组件,我们就可以在利用它开发更丰富的效果,比如本文要介绍的tab导航和滚动导航.实现简单,演示效果如下: tab导航(对应tab-sticky.html): 滚动导航(对应nav-scroll-sticky.

  • Firefox getBoxObjectFor getBoundingClientRect联系

    在一个含有Flash的网页中插入Flash会提示: 警告: 不建议使用 getBoxObjectFor() . 请使用 element.getBoundingClientRect(). 经本人测试,确实是Firefox在含flash的网页上提示,还不知道原因,也没找到解决办法. Firefox版本:3.0.3 Flash: 10.0 html页面代码: 复制代码 代码如下: <html> <body> <object type="application/x-shoc

  • 获取元素距离浏览器周边的位置的方法getBoundingClientRect

    复制代码 代码如下: var box = document.getElementById( "gaga1" ); /* alert( box.getBoundingClientRect().top ); alert( box.getBoundingClientRect().right ); alert( box.getBoundingClientRect().bottom ); alert( box.getBoundingClientRect().left ) */ function

  • 使用getBoundingClientRect方法实现简洁的sticky组件的方法

    sticky组件,通常应用于导航条或者工具栏,当网页在某一区域滚动的时候,将导航条或工具栏这类元素固定在页面顶部或底部,方便用户快速进行这类元素提供的操作.本文介绍这种组件的实现思路,并提供一个同时支持将sticky元素固定在顶部或底部的具体实现,由于这种组件在网站中非常常见,所以有必要掌握它的实现方式,以便在有需要的时候基于它的思路写出功能更多的组件出来. 固定在顶部的demo效果(对应sticky-top.html): 固定在底部的demo效果(对应sticky-bottom.html):

  • Vue3中10多种组件通讯方法小结

    目录 Props emits expose / ref Non-Props 单个根元素的情况 多个元素的情况 v-model 单值的情况 多个 v-model 绑定 v-model 修饰符 插槽 slot 默认插槽 具名插槽 作用域插槽 provide / inject 总线 bus getCurrentInstance Vuex State Getter Mutation Action Module Pinia 安装 注册 mitt.js 安装 使用 本文讲解 Vue 3.2 组件多种通讯方式

  • vue父组件通过props如何向子组件传递方法详解

    前言 本文主要给大家介绍了关于vue中父组件通过props向子组件传递方法的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: vue 组件中的 this vue 中 data/computed/methods 中 this的上下文是vue实例,需注意. 例如: 注意:不应该对 data 属性使用箭头函数 (例如data: () => { return { a: this.myProp }} ) .理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例

  • Vuejs实现带样式的单文件组件新方法

    本文实例为大家分享了Vuejs实现单文件组件的方法,供大家参考,具体内容如下 代码如下: example.html <script src="vue.js"></script> <div id="example"> <h3>Vue component<h3> <counter></counter> <counter></counter> </div>

  • vue 怎么创建组件及组件使用方法

    什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能.在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展. 我知道vue中核心就是组件,但是组件是什么呢?组件有什么用呢? 这里来说说怎么用组件?怎么样创建自己的组件?: 1)创建自己的组件 通过vue.extend("template");通过vue构造器去拓展一个模板,

  • 详解Vue 非父子组件通信方法(非Vuex)

    一提到两个非父子组件通信方法,有经验的 coder 肯定会说用 Vuex 啊,我个人建议不要为了用 Vuex 而用 Vuex,除非你的项目很大,耦合度很高,需要大量的储存一些 data,组件之间通信频繁.当然还是要根据自己的业务场景的来决定,总之还是那句话,不要为了用 Vuex 而用 Vuex! Vue 官网介绍了非父子组件通信方法: 不过官网说的太简单了,新手看完估计还是一脸懵逼.还有这个空的 Vue 实例放到哪里合适也值得商榷. 这篇文章的目的就是用一个简单的例子让你明白如何用 Bus

  • Angular 4根据组件名称动态创建出组件的方法教程

    一.理解angular组件 组件是一种特殊的指令,使用更简单的配置项来构建基于组件的应用程序架构,这样他能简单地写app,通过类似的web Component 或者angular2的样式.web Component 是一个规范.马上就要成为标准. 应用组件的优点: 比普通指令配置还简单 提供更好的默认设置和最好的实践 对基于组建的应用架构更优化. 对angular2的升级更平滑. 不用组建的情况: 对那些在 compile或者pre-link阶段要执行操作的指令,组件不能用,因为无法到达那个阶段

  • 在ABP框架中使用BootstrapTable组件的方法

    一.关于ABP ABP是"ASP.NET Boilerplate Project (ASP.NET样板项目)"的简称,它是一个成熟的开源框架,基于DDD+Repository模式,自带Zero权限和认证模块,避免了从零开始搭建框架的烦恼.关于ABP的框架优势就此打住,因为这样说下去要说三天三夜,脱离文本主题. 关于ABP的入门,博主不想说太多,园子里面tkb至简和阳光铭睿有很多入门级的文章,有兴趣的可以了解下,还是给出它的官网和开源地址. ABP官方网站:http://www.aspn

随机推荐