正常情况下,我们想到的是通过监听父容器的滚动事件,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,进而改变目标元素的定位方式,这种方法的缺点是,由于scroll事件密集发生,计算量很大,不断的进行读取操作,就会不断的触发重排,进而会导致不断的重绘,容易造成性能问题。 好在,有一个css属性可以帮助我们实现这种效果。需求: 实现滚动吸顶的效果,兼容IOS和安卓。
position: sticky
sticky:粘性定位,可以被认为是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。例如:
1 | #one { position: sticky; top: 10px; } |
在 viewport 视口滚动到元素 top 距离小于 10px 之前,元素为相对定位。之后,元素将固定在与顶部距离 10px 的位置,直到 viewport 视口回滚到阈值以下。
所以滚动吸顶可以按照如下方式实现:
1 | position: sticky; |
但是这个属性还是处于实验性的取值,兼容性还存在问题,可以看到,对IOS6以上的兼容性还是可以的,而IOS6的发布时间在2014年,iPhone3GS发布系统才是6,iPhone4以后的系统版本都高于6,意味着现在市面上的苹果手机和ipad使用sticky这个属性基本都会生效,所以我们可以区分终端来应用,对IOS直接就可以使用css属性实现这个效果,安卓手机使用下面的方式。
IntersectionObserver
IntersectionObserver:字面解释,交叉观察者,目标元素与视口(可见区域)产生交叉区域,可以自动观察目标元素在滚动的过程中在视口可见。
用法:
1 | const observer = new IntersectionObserver(callback, option); |
上面代码中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。option是配置对象(该参数可选)。其中有个threshold属性,决定了什么时候触发回调函数,默认为0,即目标元素刚进入视口时触发,设置为1,即目标元素完全进入视口时触发。
构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。
这个api的兼容性也不是很好,对安卓的webview和chrome兼容到51及以上,而chrome的51发布时间是2016年5月,对于安卓7.0及以上版本,webview的版本也都在51以上,
所以可以这样使用:
1 | if (IntersectionObserver) { |
对于不支持这个新的api的机型,可以继续使用老方法,监听scroll,同时为了减少重排,可以使用lodash的节流
1 | this.wrapContainer.scrollWrap.addEventListener('scroll', _.throttle(this.handleSetFixed, 50)); |
注意:IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。规格写明,IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
优化
对于原来是relative定位,滚动过程变为fixed定位的元素,会出现瞬间抖动现象。
出现抖动的原因在于,在吸顶元素 position 变为 fixed 的时候,该元素就脱离了文档流,下一个元素就进行了补位。就是这个补位操作造成了抖动。
解决方案
为这个吸顶元素添加一个等高的父元素,我们监听这个父元素的 getBoundingClientRect().top 值,对吸顶元素设置fixed定位。
组件的封装
虽然只是一个吸顶元素的实现,但是却要监听当前页的滚动,事件都是在父容器里触发的,组件的设计要保证引入就能用,不需要添加过多的事件处理函数,很明显,如果我们把监听事件放到父容器里实现,耦合就太严重了,此外,吸顶元素在未吸顶前,定位是相对的,你不能确定他在视口首屏内的布局位置,所需父组件需要传递给子组件滚动的元素:
吸顶组件
1 | import React from 'react'; |
样式
1 | .scroll-fixedbar { |
使用方式示例
滚动元素默认为window,如果特殊,需要指定。
1 | <ScrollFixedBar |