以前项目里的头部导航须要实现吸顶效果,一开始是本身实现,发现效果老是差那么一点,当时急着实现功能找来了react-sticky这个库,如今有空便想着完全琢磨透这个吸顶的问题。css
吸顶效果天然会想到position:sticky
, 这属性网上相关资料也不少,你们能够自行查阅。就提一点与我最初预想不同的地方:html
示例1. 符合个人预期,正常吸顶react
// html
<body>
<div class="sticky">123</div>
</body>
// css
body {
height: 2000px;
}
div.sticky {
position: sticky;
top:0px;
}
复制代码
示例2. 不符合个人预期 不能吸顶git
// html
<body>
<div class='sticky-container'>
<div class="sticky">123</div>
</div>
</body>
// css
body {
height: 2000px;
}
div.sticky-contaienr {
height: 1000px; // 除非加上这段代码才会有必定的吸顶效果
}
div.sticky {
position: sticky;
top:0px;
}
复制代码
我觉得只要加上了 position:sticky
,设置了 top
的值就能吸顶,无论其余的元素如何,恰好也是我须要的效果,如示例1同样。github
可是其实对于 position:sticky
而言,它的活动范围只能在父元素内,滚动超过父元素的话,它同样不能吸顶。示例2中,.sticky-container
的高度和 .sticky
的高度一致,滚动就没有吸顶效果。 给 .sticky-container
设置个 1000px
的高度,那 .sticky
就能在那 1000px
的滚动中吸顶。chrome
固然 sticky
这样设计是为了实现更为复杂的效果。npm
附上一份参考资料 CSS Position Sticky - How It Really Works!浏览器
// React使用
<StickyContainer style={{height: 2000}}>
<Sticky>
{({style}) => {
return <div style={style}>123 </div> // 须要吸顶的元素
}}
</Sticky>
其它内容
</StickyContainer>
// 对应生成的Dom
<div style='height: 2000px;'> // sticky-container
<div> // parent
<div style='padding-bottom: 0px;'></div> // placeholder
<div>123 </div> // 吸顶元素
</div>
其它内容
</div>
复制代码
看上面的React代码及对应生成的dom结构,发现Sticky
生成了一个嵌套div结构,把咱们真正须要吸顶的元素给包裹了一层:bash
<div> // parent
<div style='padding-bottom: 0px;'></div> // placeholder
<div>123 </div> // 吸顶元素
</div>
复制代码
一开始我是有些疑惑的,这个库为何要这样实现,不能生成下面的结构嘛?减去div1
,div2
?dom
<div style='height: 2000px;'>
<div>123 </div>
其它内容
</div>
复制代码
因而我先无论别人的代码,本地写demo,思考着如何实现吸顶效果,才慢慢理解到react-sticky
的设计。
吸顶,即当 页面滚动的距离
超过 吸顶元素距离文档(而非浏览器窗口)顶部的高度
时,则吸顶元素进行吸顶,不然吸顶元素变为正常的文档流定位。
所以固然能够在第一次
滚动前,经过吸顶元素(以后会用sticky代替)sticky.getBoundingClientRect().top
获取元素距离html文档顶部的距离假设为htmlTop
,之因此强调在第一次滚动前是由于,只有第一次滚动前表明的是距离html文档顶部的距离,以后有滚动了就只能表明距离浏览器窗口顶部的距离。
经过document.documentElement.scrollTop
获取页面滚动距离假设为scrollTop
,每次滚动事件触发时计算scrollTop - htmlTop
,大于0则将sticky元素的position
设为 fixed
,不然恢复为原来的定位方式。
这样是能正常吸顶的,可是会有个问题,因为sticky
变为fixed
脱离文档流,致使文档内容缺乏一块。想象下:
div1
div2
div3
1,2,3三个div,假如忽然2变为fixed了,那么会变成:
div1
div3 div2
复制代码
即吸顶的以后,div3的内容会被div2遮挡住。
因此查看刚刚react-sticky
生成的dom中,吸顶元素会有个兄弟元素placeholder
。有了placeholder以后即便吸顶元素fixed
了脱离文档流,也有placeholder占据它的位置:
div1
placeholder div2
div3
复制代码
同时因为给吸顶元素添加了兄弟元素,那么最好的处理方式是再加个parent
把两个元素包裹起来,这样不容易被别的元素影响也不容易影响别的元素(我猜的)。
它的实现也很简单,就Sticky.js
和Container.js
两个文件,稍微讲下。代码不粘贴了点开这里看 Container.js, Sticky.js。
resize
,scroll
,touchstart
,touchmove
,touchend
,pageshow
,load
。Container
的位置信息传递到Sticky
组件上。Sticky
组件再经过计算位置信息判断是否须要fixed
定位。其实也就是这样,固然它还支持了relative
,stacked
两种模式,所以代码更复杂些。看看从中咱们能学到什么:
raf
库控制动画,它是对requestAnimationFrame
作了兼容性处理Context
,以及 观察者模式React.cloneElement
来建立元素,以至于最终使用Sticky
组件用起来感受有些不寻常。disableHardwareAcceleration
属性用于关闭动画的硬件加速,实质上是决定是否设置transform:"translateZ(0)";
对最后一个知识点感兴趣,总是说用transform
能启动硬件加速,动画更流畅,真的假的?因而又去找了资料,An Introduction to Hardware Acceleration with CSS Animations。本地测试chrome的performance
发现用left,top
,fps,gpu,frames
等一片绿色柱状线条,用 transform
则只有零星几条绿色柱状线条。感受有道理。
理解完源码本身写(抄)一个,只实现最简单的吸顶功能:
import React, { Component } from 'react'
const events = [
'resize',
'scroll',
'touchstart',
'touchmove',
'touchend',
'pageshow',
'load'
]
const hardwareAcceleration = {transform: 'translateZ(0)'}
class EasyReactSticky extends Component {
constructor (props) {
super(props)
this.placeholder = React.createRef()
this.container = React.createRef()
this.state = {
style: {},
placeholderHeight: 0
}
this.rafHandle = null
this.handleEvent = this.handleEvent.bind(this)
}
componentDidMount () {
events.forEach(event =>
window.addEventListener(event, this.handleEvent)
)
}
componentWillUnmount () {
if (this.rafHandle) {
raf.cancel(this.rafHandle)
this.rafHandle = null
}
events.forEach(event =>
window.removeEventListener(event, this.handleEvent)
)
}
handleEvent () {
this.rafHandle = raf(() => {
const {top, height} = this.container.current.getBoundingClientRect()
// 因为container只包裹着placeholder和吸顶元素,且container的定位属性不会改变
// 所以container.getBoundingClientRect().top大于0则吸顶元素处于正常文档流
// 小于0则吸顶元素进行fixed定位,同时placeholder撑开吸顶元素原有的空间
const {width} = this.placeholder.current.getBoundingClientRect()
if (top > 0) {
this.setState({
style: {
...hardwareAcceleration
},
placeholderHeight: 0
})
} else {
this.setState({
style: {
position: 'fixed',
top: '0',
width,
...hardwareAcceleration
},
placeholderHeight: height
})
}
})
}
render () {
const {style, placeholderHeight} = this.state
return (
<div ref={this.container}>
<div style={{height: placeholderHeight}} ref={this.placeholder} />
{this.props.content(style)}
</div>
)
}
}
//使用
<EasyReactSticky content={style => {
return <div style={style}>this is EasyReactSticky</div>
}} />
复制代码
显然,大部分代码借鉴 react-sticky
,减小了参数配置代码和对两种模式stacked
和relative
的支持。着实简易,同时改变了组件调用形式,采用了render-props
。
本文源于以前工做急于完成任务而留下的一个小坑,所幸如今填上了。react-sticky
在github上1926
个star,自己却并不复杂,经过阅读这样一个经受住开源考验的小库也能学到很多东西。
top,left
改变来作动画的元素,例如An Introduction to Hardware Acceleration with CSS Animations中的第一个例子,添加transform:translateZ(0)
,那样会有硬件加速嘛?(我测试的结果像是没有,依旧不少painting)欢迎讨论~
react-sticky
CSS Position Sticky - How It Really Works!
An Introduction to Hardware Acceleration with CSS Animations
render-props