講講吸頂效果與react-sticky

前言

以前項目裏的頭部導航須要實現吸頂效果,一開始是本身實現,發現效果老是差那麼一點,當時急着實現功能找來了react-sticky這個庫,如今有空便想着完全琢磨透這個吸頂的問題。css

1. 粘性定位

吸頂效果天然會想到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!瀏覽器

2. react-sticky

2.1 使用

// 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>
複製代碼

2.2 疑惑

看上面的React代碼及對應生成的dom結構,發現Sticky生成了一個嵌套div結構,把咱們真正須要吸頂的元素給包裹了一層:bash

<div>                                            //  parent
    <div style='padding-bottom: 0px;'></div>     //  placeholder
    <div>123 </div>                              // 吸頂元素
</div>
複製代碼

一開始我是有些疑惑的,這個庫爲何要這樣實現,不能生成下面的結構嘛?減去div1,div2dom

<div style='height: 2000px;'>
    <div>123 </div>
   其它內容
</div>
複製代碼

因而我先無論別人的代碼,本地寫demo,思考着如何實現吸頂效果,才慢慢理解到react-sticky的設計。

2.3 解惑

吸頂,即當 頁面滾動的距離 超過 吸頂元素距離文檔(而非瀏覽器窗口)頂部的高度時,則吸頂元素進行吸頂,不然吸頂元素變爲正常的文檔流定位。

所以固然能夠在第一次滾動前,經過吸頂元素(以後會用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把兩個元素包裹起來,這樣不容易被別的元素影響也不容易影響別的元素(我猜的)。

2.4 源碼

它的實現也很簡單,就Sticky.jsContainer.js兩個文件,稍微講下。代碼不粘貼了點開這裏看 Container.js, Sticky.js

  • 首先綁定一批事件:resize,scroll,touchstart,touchmove,touchend ,pageshow,load
  • 經過觀察者模式,當以上這些事件觸發時,將Container的位置信息傳遞到Sticky組件上。
  • Sticky組件再經過計算位置信息判斷是否須要fixed定位。

其實也就是這樣,固然它還支持了relative,stacked兩種模式,所以代碼更復雜些。看看從中咱們能學到什麼:

  • 用到了raf庫控制動畫,它是對requestAnimationFrame作了兼容性處理
  • 使用到 Context,以及 觀察者模式
  • 竟然須要監聽那麼多種事件(反正是個人話就只會加個scroll)
  • React.cloneElement來建立元素,以至於最終使用Sticky組件用起來感受有些不尋常。
  • disableHardwareAcceleration屬性用於關閉動畫的硬件加速,實質上是決定是否設置transform:"translateZ(0)";

對最後一個知識點感興趣,總是說用transform能啓動硬件加速,動畫更流暢,真的假的?因而又去找了資料,An Introduction to Hardware Acceleration with CSS Animations。本地測試chrome的performance發現用left,topfps,gpu,frames等一片綠色柱狀線條,用 transform 則只有零星幾條綠色柱狀線條。感受有道理。

3. 來個簡易版

理解完源碼本身寫(抄)一個,只實現最簡單的吸頂功能:

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 ,減小了參數配置代碼和對兩種模式stackedrelative的支持。着實簡易,同時改變了組件調用形式,採用了render-props

4. 總結

本文源於以前工做急於完成任務而留下的一個小坑,所幸如今填上了。react-sticky在github上1926個star,自己卻並不複雜,經過閱讀這樣一個經受住開源考驗的小庫也能學到很多東西。

5. 討論

歡迎討論~

參考資料

react-sticky
CSS Position Sticky - How It Really Works!
An Introduction to Hardware Acceleration with CSS Animations
render-props

相關文章
相關標籤/搜索