其实真正处理回调的是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)有助于提高自己的技术水平,增进知识的广度和深度;
所以有能力造轮子的时候,造造也是很有必要的。