以前項目裏的頭部導航須要實現吸頂效果,一開始是本身實現,發現效果老是差那麼一點,當時急着實現功能找來了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