浅谈Sticky组件的改进实现


Posted in Javascript onMarch 22, 2016

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

1. 旧版本的问题

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

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

浅谈Sticky组件的改进实现

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

浅谈Sticky组件的改进实现

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

浅谈Sticky组件的改进实现

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

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

浅谈Sticky组件的改进实现

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

浅谈Sticky组件的改进实现

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

浅谈Sticky组件的改进实现

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

1)documentElement.clientHeight没有做缓存,导致每次判断临界点时都要去重新获取:

浅谈Sticky组件的改进实现

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组件,敬请关注。
感谢您的阅读:)

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

浅谈Sticky组件的改进实现

Javascript 相关文章推荐
javascript客户端解决方案 缓存提供程序
Jul 14 Javascript
jQuery操作input type=radio的实现代码
Jun 14 Javascript
Bootstrap每天必学之轮播(Carousel)插件
Apr 25 Javascript
使用jQuery Ajax 请求webservice来实现更简练的Ajax
Aug 04 Javascript
Vue使用mixins实现压缩图片代码
Mar 14 Javascript
vue+vuex+json-seiver实现数据展示+分页功能
Apr 11 Javascript
详解vue-cli3多页应用改造
Jun 04 Javascript
JavaScript 双向链表操作实例分析【创建、增加、查找、删除等】
Apr 28 Javascript
简单了解vue 插值表达式Mustache
Jul 22 Javascript
JavaScript读取本地文件常用方法流程解析
Oct 12 Javascript
Vue3配置axios跨域实现过程解析
Nov 25 Vue.js
vue 基于abstract 路由模式 实现页面内嵌的示例代码
Dec 14 Vue.js
使用Sticky组件实现带sticky效果的tab导航和滚动导航的方法
Mar 22 #Javascript
关于JS中match() 和 exec() 返回值和属性的测试
Mar 21 #Javascript
快速掌握Node.js中setTimeout和setInterval的使用方法
Mar 21 #Javascript
快速掌握Node.js事件驱动模型
Mar 21 #Javascript
快速掌握Node.js模块封装及使用
Mar 21 #Javascript
JS DOM实现鼠标滑动图片效果
Sep 17 #Javascript
实践中学习AngularJS表单
Mar 21 #Javascript
You might like
PHPUnit PHP测试框架安装方法
2011/03/23 PHP
php获取网页标题和内容函数(不包含html标签)
2014/02/03 PHP
PHP HTTP 认证实例详解
2016/11/03 PHP
php实用代码片段整理
2016/11/12 PHP
PHP基于递归实现的约瑟夫环算法示例
2017/08/27 PHP
php删除一个路径下的所有文件夹和文件的方法
2018/02/07 PHP
yii 框架实现按天,月,年,自定义时间段统计数据的方法分析
2020/04/04 PHP
Firefox window.close()的使用注意事项
2009/04/11 Javascript
JS window.opener返回父页面的应用
2009/10/24 Javascript
Jquery Uploadify多文件上传带进度条且传递自己的参数
2013/08/28 Javascript
node.js使用nodemailer发送邮件实例
2014/03/10 Javascript
js判断请求的url是否可访问,支持跨域判断的实现方法
2016/09/17 Javascript
基于bootstrap页面渲染的问题解决方法
2018/08/09 Javascript
微信小程序的线程架构【推荐】
2019/05/14 Javascript
vue router 跳转时打开新页面的示例方法
2019/07/28 Javascript
解决vue.js提交数组时出现数组下标的问题
2019/11/05 Javascript
在vue项目实现一个ctrl+f的搜索功能
2020/02/28 Javascript
[02:32]DOTA2完美大师赛场馆静安体育中心观赛全攻略
2017/11/08 DOTA
python os用法总结
2018/06/08 Python
python3实现SMTP发送邮件详细教程
2018/06/19 Python
在python中实现将一张图片剪切成四份的方法
2018/12/05 Python
局域网内python socket实现windows与linux间的消息传送
2019/04/19 Python
Python使用到第三方库PyMuPDF图片与pdf相互转换
2019/05/03 Python
python collections模块的使用
2020/10/16 Python
深入解析HTML5中的Blob对象的使用
2015/09/08 HTML / CSS
全球度假村:Club Med
2017/11/27 全球购物
SportsDirect.com马来西亚:英国第一体育零售商
2018/11/21 全球购物
J2EE面试题大全
2016/08/06 面试题
过程装备与控制工程专业个人的求职信
2013/12/01 职场文书
函授药学自我鉴定
2014/02/07 职场文书
治庸问责心得体会
2014/09/12 职场文书
党员思想汇报材料
2014/12/19 职场文书
婚宴致辞
2015/07/28 职场文书
比赛口号霸气押韵
2015/12/24 职场文书
ORM模型框架操作mysql数据库的方法
2021/07/25 MySQL
《游戏王:大师决斗》新活动上线 若无符合卡组可免费租用
2022/04/13 其他游戏