上上篇博客介绍了一个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. (转)对SQLSERVER数据库事务日志的疑问

    本文转载自桦仔的博客http://www.cnblogs.com/lyhabc/archive/2013/06/10/3130856.html 对SQLSERVER数据库事务日志的疑问 摸不透SQLS ...

  2. 关于Web开发里并发、同步、异步以及事件驱动编程的相关技术

    一.开篇语 我的上篇文章<关于如何提供Web服务端并发效率的异步编程技术>又成为了博客园里“编辑推荐”的文章,这是对我写博客很大的鼓励,也许是被推荐的原因很多童鞋在这篇文章里发表了评论,有 ...

  3. java提高篇(十二)-----代码块

    在编程过程中我们可能会遇到如下这种形式的程序: public class Test { { //// } } 这种形式的程序段我们将其称之为代码块,所谓代码块就是用大括号({})将多行代码封装在一起, ...

  4. ASP.NET-自定义HttpModule与HttpHandler

    在之前的ASP.NET是如何在IIS下工作的这篇文章中介绍了ASP.NET与IIS配合工作的机制,在http请求经过一系列处理后,最后到达ASP.NET管道中,这时,就是Http Modules和Ht ...

  5. 虚拟化平台cloudstack(2)——安装(上)

    vmware workstation安装ubuntu server12.04 这个其实没什么说的了,下软件,安装,一顿下一步,OK. 安装完成后,为ubuntu server 12.04安装桌面. 使 ...

  6. Git学习笔记(10)——搭建Git服务器

    本文主要记录了Git服务器的搭建,以及一些其他的配置,和最后的小总结. Git远程仓库服务器 其实远程仓库和本地仓库没啥不同,远程仓库只是每天24小时开机为大家服务,所以叫做服务器.我们完全可以把自己 ...

  7. Atom

    http://blog.csdn.net/crper/article/details/45647459

  8. Linux创建用户(转)

    这篇文章总结的太好了,让我没动力新写一篇了,啊哈哈哈哈. 源自:http://www.cnblogs.com/ylan2009/articles/2321177.html Linux 系统是一个多用户 ...

  9. Mysql跨表更新 多表update sql语句总结

    Mysql跨表更新一直是大家所关心的话题,本文介绍mysql多表 update在实践中几种不同的写法 假定我们有两张表,一张表为Product表存放产品信息,其中有产品价格列Price:另外一张表是P ...

  10. 缓存篇(Cache)~第一回 使用static静态成员实现服务器端缓存(导航面包屑)

    返回目录 今天写缓存篇的第一篇文章,在写完目录后,得到了一些朋友的关注,这给我之后的写作带来了无穷的力量,在这里,感谢那几位伙伴,哈哈! 书归正传,今天我带来一个Static静态成员的缓存,其实它也不 ...