上上篇博客介绍了一个sticky组件的简洁实现,经过这两天的思考,发现上次提供的实现还有较多不足的地方,另外跟别的网站上实现的效果在取消固定的时候也有一些不同,上次提供的取消固定的处理方式不好,本文在上文的基础上,提供一个改进版的sticky组件,功能更加完善,希望您有兴趣阅读。代码下载

1. 旧版本的问题

上一个sticky组件的实现中,有多个问题存在:

第一,从sticky的效果上来说,sticky元素在固定前后,不会变化的是相对浏览器左边的位置以及sticky元素的整体宽度,可能会变化的是相对浏览器顶部或底部的位置和sticky元素的高度,而上文提供的实现中把后面两个会变化的值都当成了不变的值。为什么在固定的时候top值或bottom值就一定是0?当然可以不是0阿,比如top: 20px,bottom: 15px,在某些场景里,加上一些这样的偏移,sticky的效果会更好看,比如bootstrap官方文档中用到的affix组件实例(这个组件的功能跟本文实现的sticky组件是差不多的):

它就把固定的时候,相对浏览器顶部的位置设置成了top: 20px。sticky元素的高度也是,为了在固定的时候显示更好看的效果,调整原来的Line-height或者padding-top等更高度有关的属性,也是非常常见的需求,比如天猫花呗的这个页面,这块内容就用到了sticky组件:

固定前,sticky元素的高度是:

固定后,sticky元素的高度是:

第二,在取消固定的时候,以sticky元素固定在顶部为例,上文提供的实现是在target元素跟浏览器顶部的距离小于stickyHeight的时候,就直接取消sticky元素的position: fixed属性,sticky元素立马被还原到普通文档流中,效果是:


它是在临界点的时候立马就消失的,而天猫花呗的那个效果就不是这样:

它在临界点的时候并不是立即消失,而是重新去调整sticky元素的top值,让它配合着滚动条一起跟随网页主体内容一起向上滚动:

从体验上来说,显然天猫花呗的这个效果更好一点,从功能上来说,上文提供的实现有一个致命的缺点:就是当sticky元素的高度非常大,超出了浏览器可视区域的高度的时候,会出现不管你怎么滚动,都无法浏览全sticky元素所有内容的BUG,有兴趣的可以拿上次实现的代码在自己博客的侧边栏上试一试。我试过发现了这个问题,所以才想要改进sticky组件:(

第三,上次的实现还有几处不足的地方:
1)documentElement.clientHeight没有做缓存,导致每次判断临界点时都要去重新获取:

2)滚动回调间隔的默认值太大,应该再设置小一点,这次用的是5,bootstrap用的是1,只有这样才能保证效果流畅;
3)有的场景可能不需要resize的时候重新设置sticky元素的宽度,应该加个选项来控制;
4)在sticky元素固定和取消固定的时候,应该提供回调函数,以便其它组件依赖这个组件的时候可以在关键点做些事情。

2. 如何改进

组件的选项重新定义了一下:

var DEFAULTS = {
target: '', //target元素的jq选择器
type: 'top', //固定的位置,top | bottom,默认为top,表示固定在顶部
wait: 5, //scroll事件回调的间隔
stickyOffset: 0, //固定时距离浏览器可视区顶部或底部的偏移,用来设置top跟bottom属性的值,默认为0
isFixedWidth: true, //sticky元素宽度是否固定,默认为true,如果是自适应的宽度,需设置为false
getStickyWidth: undefined, //用来获取sticky元素宽度的回调,在不传该参数的情况下,stickyWidth将设置为sticky元素的offsetWidth
unStickyDistance: undefined, //该参数决定sticky元素何时进入dynamicSticky状态
onSticky: undefined, ///sticky元素固定时的回调
onUnSticky: undefined ///sticky元素取消固定时的回调
};

加粗的几个是新增或有修改的,去掉了原来的height,用unStickyDistance来替代。固定时候相对浏览器顶部或底部的位置,用stickyOffset来指定,这样在.sticky--in-top或.sticky--in-bottom的css里就不用再写top或bottom属性值了。isFixedWidth如果为false,才会去添加resize时刷新sticky元素宽度的回调:

!opts.isFixedWidth && $win.resize(throttle(function () {
setStickyWidth();
$elem.hasClass(className) && $elem.css('width', stickyWidth);
sticky();
}, opts.wait));

本次实现相比上次,麻烦的是取消固定时的逻辑处理,上次sticky元素只有2种状态,sticky或者unsticky,这次不一样,sticky状态里面又分成了staticSticky和dynamicSticky,前者表示top或bottom值不变的sticky状态,后者表示top或bottom值会变化的sticky状态,其实后者对应的就是快要取消固定的时候那段范围,为了更清晰地解决这个问题,将原来判断临界点以及在不同临界点做不同处理的代码重构成下面这个样子:

setSticky = function () {
!$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth)
&& (typeof opts.onSticky == 'function' && opts.onSticky($elem, $target));
return true;
},
states = {
staticSticky: function () {
setSticky() && $elem.css(opts.type, opts.stickyOffset);
},
dynamicSticky: function (rect) {
setSticky() && $elem.css(opts.type, rules[opts.type].getDynamicOffset(rect));
},
unSticky: function () {
$elem.hasClass(className) && $elem.removeClass(className).css('width', '').css(opts.type, '')
&& (typeof opts.onUnSticky == 'function' && opts.onUnSticky($elem, $target));
}
},
rules = {
top: {
getState: function (rect) {
if (rect.top < 0 && (rect.bottom - unStickyDistance) > 0) return 'staticSticky';
else if ((rect.bottom - unStickyDistance) <= 0 && rect.bottom > 0) return 'dynamicSticky';
else return 'unSticky';
},
getDynamicOffset: function (rect) {
return -(unStickyDistance - rect.bottom);
}
},
bottom: {
getState: function (rect) {
if (rect.bottom > docClientHeight && (rect.top + unStickyDistance) < docClientHeight) return 'staticSticky';
else if ((rect.top + unStickyDistance) >= docClientHeight && rect.top < docClientHeight) return 'dynamicSticky';
else return 'unSticky';
},
getDynamicOffset: function (rect) {
return -(unStickyDistance + rect.top - docClientHeight);
}
}
}
$win.scroll(throttle(sticky, opts.wait));
function sticky() {
var rect = $target[0].getBoundingClientRect(),
curState = rules[opts.type].getState(rect);
states[curState](rect);
}

有点状态模式的思想在里面,不过更简洁。当我写出这个代码的时候,其实是很想用之前了解的状态机来写的,我想过用状态机来写肯定是可以实现的,不过为了少引用一个类库就算了,等哪天想实践状态机的时候再来尝试一把。

整体实现如下:

var Sticky = (function ($) {
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);
}
} var DEFAULTS = {
target: '', //target元素的jq选择器
type: 'top', //固定的位置,top | bottom,默认为top,表示固定在顶部
wait: 5, //scroll事件回调的间隔
stickyOffset: 0, //固定时距离浏览器可视区顶部或底部的偏移,用来设置top跟bottom属性的值,默认为0
isFixedWidth: true, //sticky元素宽度是否固定,默认为true,如果是自适应的宽度,需设置为false
getStickyWidth: undefined, //用来获取sticky元素宽度的回调,在不传该参数的情况下,stickyWidth将设置为sticky元素的offsetWidth
unStickyDistance: undefined, //该参数决定sticky元素何时进入dynamicSticky状态
onSticky: undefined, ///sticky元素固定时的回调
onUnSticky: undefined ///sticky元素取消固定时的回调
}; return function (elem, opts) {
var $elem = $(elem);
opts = $.extend({}, DEFAULTS, opts || {}, $elem.data() || {}); var $target = $(opts.target);
if (!$elem.length || !$target.length) return; var stickyWidth,
setStickyWidth = function () {
stickyWidth = typeof opts.getStickyWidth === 'function' && opts.getStickyWidth($elem) || $elem[0].offsetWidth;
},
docClientHeight = document.documentElement.clientHeight,
unStickyDistance = opts.unStickyDistance || $elem[0].offsetHeight,
setSticky = function () {
!$elem.hasClass(className) && $elem.addClass(className).css('width', stickyWidth)
&& (typeof opts.onSticky == 'function' && opts.onSticky($elem, $target));
return true;
},
states = {
staticSticky: function () {
setSticky() && $elem.css(opts.type, opts.stickyOffset);
},
dynamicSticky: function (rect) {
setSticky() && $elem.css(opts.type, rules[opts.type].getDynamicOffset(rect));
},
unSticky: function () {
$elem.hasClass(className) && $elem.removeClass(className).css('width', '').css(opts.type, '')
&& (typeof opts.onUnSticky == 'function' && opts.onUnSticky($elem, $target));
}
},
rules = {
top: {
getState: function (rect) {
if (rect.top < 0 && (rect.bottom - unStickyDistance) > 0) return 'staticSticky';
else if ((rect.bottom - unStickyDistance) <= 0 && rect.bottom > 0) return 'dynamicSticky';
else return 'unSticky';
},
getDynamicOffset: function (rect) {
return -(unStickyDistance - rect.bottom);
}
},
bottom: {
getState: function (rect) {
if (rect.bottom > docClientHeight && (rect.top + unStickyDistance) < docClientHeight) return 'staticSticky';
else if ((rect.top + unStickyDistance) >= docClientHeight && rect.top < docClientHeight) return 'dynamicSticky';
else return 'unSticky';
},
getDynamicOffset: function (rect) {
return -(unStickyDistance + rect.top - docClientHeight);
}
}
},
className = 'sticky--in-' + opts.type,
$win = $(window); setStickyWidth(); $win.scroll(throttle(sticky, opts.wait));
!opts.isFixedWidth && $win.resize(throttle(function () {
setStickyWidth();
$elem.hasClass(className) && $elem.css('width', stickyWidth);
sticky();
}, opts.wait));
$win.resize(throttle(function () {
docClientHeight = document.documentElement.clientHeight;
}, opts.wait)); function sticky() {
var rect = $target[0].getBoundingClientRect(),
curState = rules[opts.type].getState(rect);
states[curState](rect);
}
}
})(jQuery);

难理解的可能是getState的那个方法的逻辑,这部分的一些思路在上上篇博客有比较详细的说明。

3. 博客侧边栏应用说明

首先得把本次的实现粘贴到博客设置页脚html文本域里面去,然后加入下面的代码来初始化:

var timer = setInterval(function(){
if($('#blogCalendar').length && $('#profile_block').length && $('#sidebar_search').length) {
new Sticky('#sideBar', {
target: '#main',
onSticky: function($elem, $target){
$target.css('min-height',$elem.outerHeight());
$elem.css('left', '65px');
},
onUnSticky: function($elem, $target){
$target.css('min-height','');
$elem.css('left', '');
}
});
}
},100);

使用timer是因为侧边栏的内容都是ajax加载,又不可能在这些ajax请求时候添加回调,只能通过它们返回的内容来判断侧边栏是否加载完毕。

4. 总结

这周末琢磨了下如何改进sticky组件,加上写这篇文章,花了大半天的时间,好歹现在这个sticky组件的功能跟实现能让自己有点满意的感觉了,上次写完总觉得怪怪的,好像缺点什么,原来是因为还差这么多东西。现在这个组件还只是能实现固定和取消固定的效果,对于实际工作而言,这个层级的效果可能还不够,网上常见的那种在固定的同时支持导航滚动或者tab导航的功能也很常见,下篇文章会介绍基于本文的sticky组件,如何实现navScrollSticky以及tabSticky组件,敬请关注。

感谢您的阅读:)

代码下载


补充于2016-03-22:

IE跟火狐里面,在刷新页面的时候,如果刷新前页面有滚动,刷新的操作虽然还会把页面的滚动位置设置成刷新的位置,但是不会触发scroll事件,所以必须在组件初始化之后立即调用一次sticky函数:

代码下载里提供的代码已经修改,不过前面贴出的实现代码没有改,以补充记录和代码下载为准。

sticky组件的改进实现的更多相关文章

  1. 利用getBoundingClientRect方法实现简洁的sticky组件

    补充于2016-03-20: 本文实现有不足,不完美的地方,请在了解本文相关内容后,移步阅读<sticky组件的改进实现>了解更佳的实现. sticky组件,通常应用于导航条或者工具栏,当 ...

  2. 基于sticky组件,实现带sticky效果的tab导航和滚动导航

    上文提供了一个改进版的sticky组件,并将演示效果应用到了自己的博客.有了类似sticky的这种简单组件,我们就可以在利用它开发更丰富的效果,比如本文要介绍的tab导航和滚动导航.实现简单,演示效果 ...

  3. 原生js实现简洁的返回顶部组件

    本文内容相当简单,所以没有发布到博客园首页,如果你不幸看到,那只能是我这篇文章的荣幸,谢谢你的大驾光临~(本博客返回顶部的功能就使用的是这个组件) 返回顶部组件是一种极其常见的网页功能,需求简单:页面 ...

  4. vue-sticky组件详解

    sticky简介 sticky的本意是粘的,粘性的,使用其进行的布局被称为粘性布局. sticky是position属性新推出的值,属于CSS3的新特性,常用与实现吸附效果. 设置了sticky布局的 ...

  5. ASP.NET Core Blazor Webassembly 之 组件

    关于组件 现在前端几大轮子全面组件化.组件让我们可以对常用的功能进行封装,以便复用.组件这东西对于搞.NET的同学其实并不陌生,以前ASP.NET WebForm的用户控件其实也是一种组件.它封装ht ...

  6. Python-Tkinter几何布局管理(转)

    所有的Tkinter组件都包含专用的几何管理方法,这些方法是用来组织和管理整个父配件区中子配件的布局的.Tkinter提供了截然不同的三种几何管理类:pack.grid和place. pack() p ...

  7. Mathematica

    Mathematica是一款科学计算软件,很好地结合了数值和符号计算引擎.图形系统.编程语言.文本系统.和与其他应用程序的高级连接.很多功能在相应领域内处于世界领先地位,它也是使用最广泛的数学软件之一 ...

  8. Strata 2014 上的 AzureCAT 粉笔会谈

     本周,AzureCAT 团队非常高兴在 Strata 会议上首次集体亮相.对于那些对 AzureCAT 团队不太熟悉的人来说,我们是 Microsoft 云与企业部门一个核心的国际性团队,由大约 ...

  9. 使用Flex构建Web和移动参考应用程序

    范例文件 Shopping Cart Sales Dashboard Expense Tracker 需要的其他产品 Android 2.2及更高版本或Android 3.0及更高版本的设备 仅仅在F ...

随机推荐

  1. WPF,Silverlight与XAML读书笔记第四十七 - Silverlight与浏览器

    说明:本系列基本上是<WPF揭秘>的读书笔记.在结构安排与文章内容上参照<WPF揭秘>的编排,对内容进行了总结并加入一些个人理解. 这部分内容主要介绍Silverlight与浏 ...

  2. 程序员必须知道的几个Git代码托管平台

    上一篇博客中2015继续任性——不会Git命令,照样玩转Git我们简单的介绍了在VS2013中使用Git,和GitHub客户端的使用.那么使用Git到底有什么好处呢?最为明显的是支持Git代码托管的平 ...

  3. 论checkbox和radio的样式美化问题

    如果你下定决心要改变现有的默认的checkbox和radio的样式,那么我目前有两种办法: 1.自己动手写一个,也就是自己写代码实现将input的checkbox和radio默认的样式隐藏掉,使用绝对 ...

  4. (小常识)Dictionary的遍历

                Dictionary<int, string> objDictionary = new Dictionary<int, string>();       ...

  5. 一个线上运营着3000+人的游戏,因为我不小心一个DROP DATABASE,全没了。 怎么办??跟我HOLD住!!!

    前言 今天下午3点,我按照惯例,打开游戏服务器,开新服部署嘛,游戏在腾讯开放平台,简单.闭着眼睛都OK.于是一轮子的复制黏贴拷贝,把服务器加起来,然后启动查看日志. ....突然发现不断的有Excep ...

  6. XMPie Tracking 操作

    XMPie Tracking 操作 1.1    打开页面 我们随便打开一个页面,这里打开landing.aspx 我们想要跟踪页面,选中DW上面的TrackingàTracking Page Vis ...

  7. Java程序员的日常 —— Java类加载中的顺序

    之前说过Java中类的加载顺序,这次看完继承部分,就结合继承再来说说类的加载顺序. 继承的加载顺序 由于static块会在首次加载类的时候执行,因此下面的例子就是用static块来测试类的加载顺序. ...

  8. struts2学习笔记之六:struts2的Action访问ServletAPI的几种方式

    方法一:通过ActionContext访问SerlvetAPI,这种方式没有侵入性 Action类部分代码 import com.opensymphony.xwork2.ActionContext; ...

  9. EasyUI分页索引不能输入非数字

    //分页索引不能输入非数字 function PagerCheck() { $(".pagination-num").keydown(function (event) { even ...

  10. 将Windows系统编译的.NET Core程序发布到Ubuntu系统

    在可移植方面.NET Core应用程序分为两种,Portable application(便捷,需要目标机器安装.NET Core Runtime)和Self-contained applicatio ...