📖
blog
  • README
  • JavaScript
    • 元素的宽高位置信息梳理
    • dom-align 源码浅析
    • Event Loop
    • 函数实参为对象时的陷阱
    • export 与 utils 方法书写规范
    • 手写 Promise 及相关代码理解
    • call,apply,bind 等函数的模拟实现
    • JavaScript继承
    • JavaScript 数据类型与类型判断
    • for..of 和 for..in 的区别
    • 写给自己看的 next 函数
    • JS 可选链与双问号
    • mouseenter 与 mouseover 事件的区别
    • Immutable相关知识
  • CSS
    • 不简单的 z-index
    • 两列布局,三列布局
    • CSS 居中方案整理
    • CSS 像素,设备像素,2倍图梳理
    • iconfont 的使用
  • Node JS
    • 实现简易的 express
  • React 核心知识点整理
    • 高阶组件
    • React 事件处理
    • React Hooks
    • React Context
  • React 状态管理
    • Redux 基础概念
    • Redux 中间件和异步操作
    • Redux Saga
    • Redux 只能有一个 store 对象嘛
  • React 开发实践
    • Ant Design Menu 组件的使用与深入
    • 讲讲吸顶效果与 react sticky
    • 基于 express,搭建 react 的开发环境
    • 通过 antd input 组件分析受控与非受控组件
    • DebounceClick 组件
    • react component Align 组件分析
    • React Portal 之事件冒泡
    • React Transition Group 源码浅析
    • React.cloneElement 父组件向子组件注入 props
    • 一次 Align 组件的问题记录
    • 如何知道子组件的类型
    • React Router 源码简单分析
    • React Redux 源码简单分析
  • Vue.js
    • Vue.js 概览
    • scoped 样式中的 deep
  • TypeScript 语法
    • 基础类型
    • 变量声明
    • 接口
    • 类
    • 函数
    • 泛型
    • 枚举
    • 类型推论
    • 类型兼容性
    • 高级类型
    • Symbol
    • 迭代器和生成器
    • 模块
    • 命名空间
    • JSX
  • 玩转 webpack
    • 第一章: webpack 与构建发展简史
    • 第二章:webpack基础用法
    • 第三章:webpack进阶用法
    • 第四章:编写可维护的 webpack 构建配置
    • 第五章:webpack构建速度和体积优化策略
    • 第六章:通过源代码掌握webpack打包原理
    • 第七章:编写Loader和插件
  • webpack 实践
    • 如何配置 output.library
  • 测试
    • 初识代码测试
    • Jest 中 如何测试 setTimeout
    • Jest Enzyme React 测试实践记录
  • WEB 开发,过往工作沉淀
    • Web安全(DVWA)
    • 内存泄露与事件移除的必要性
    • url to pdf api 与 服务部署踩坑记录
    • 前端调试指南
    • Markdown 转 email
    • github travis ci 自动部署
    • 浏览器缓存知识梳理
    • WEB 系统登录相关知识梳理
    • 将-Axios-请求参数和返回值进行格式化
    • source-map与源码调试
    • HTTPS
    • 使用 rollup 打造自己的 npm 包 (全流程)
    • father-build 是如何工作的
  • 书籍
    • 图解 HTTP 协议
    • 编写可维护的 JavaScript
    • 鸟哥的 Linux 私房菜
    • JavaScript Promise迷你书
  • Linux
    • vimtutor
    • CURL 使用指南
  • Nginx
    • 一次 nginx 分享
  • Git
    • Git Commit Message 须知
    • .gitignore 模板
    • git tag标签
  • 摄影
    • 摄影基础知识
    • 手机摄影从小白到大师
  • 翻译
    • log4js
    • log4js-node
    • 介绍GitLab上的CI/CD
    • 为GitLab Pages创建并调整GitLab CI/CD
    • 关于 rel=noopener
    • AngularJS 团队 Git 提交信息约定
    • JSON Schema
  • Lifehack
    • 20 个 Google 搜索 Tips 来高效使用 Google
    • 37 个高级 Google 搜索 Tips
Powered by GitBook
On this page
  • 前言
  • 1. 粘性定位
  • 2. react-sticky
  • 2.1 使用
  • 2.2 疑惑
  • 2.3 解疑
  • 2.4 源码
  • 3. 来个简易版
  • 4. 总结
  • 5. 讨论
  • 参考资料

Was this helpful?

  1. React 开发实践

讲讲吸顶效果与 react sticky

PreviousAnt Design Menu 组件的使用与深入Next基于 express,搭建 react 的开发环境

Last updated 4 years ago

Was this helpful?

前言

之前项目里的头部导航需要实现吸顶效果,一开始是自己实现,发现效果总是差那么一点,当时急着实现功能找来了这个库,现在有空便想着彻底琢磨透这个吸顶的问题。

1. 粘性定位

吸顶效果自然会想到position:sticky, 这属性网上相关资料也很多,大家可以自行查阅。就提一点与我最初预想不一样的地方:

示例1. 符合我的预期,正常吸顶

// html
<body>
    <div class="sticky">123</div>
</body>

// css  
body {
    height: 2000px;
}
div.sticky {
  position: sticky;
  top:0px;
}

示例2. 不符合我的预期 不能吸顶

// 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一样。

但是其实对于 position:sticky 而言,它的活动范围只能在父元素内,滚动超过父元素的话,它一样不能吸顶。示例2中,.sticky-container的高度和 .sticky 的高度一致,滚动就没有吸顶效果。 给 .sticky-container 设置个 1000px 的高度,那 .sticky 就能在那 1000px 的滚动中吸顶。

当然 sticky 这样设计是为了实现更为复杂的效果。

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结构,把我们真正需要吸顶的元素给包裹了一层:

<div>                                            //  parent
    <div style='padding-bottom: 0px;'></div>     //  placeholder
    <div>123 </div>                              // 吸顶元素
</div>

一开始我是有些疑惑的,这个库为什么要这样实现,不能生成下面的结构嘛?减去div1,div2?

<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 源码

  • 首先绑定一批事件:resize,scroll,touchstart,touchmove,touchend ,pageshow,load。

  • 通过观察者模式,当以上这些事件触发时,将Container的位置信息传递到Sticky组件上。

  • Sticky组件再通过计算位置信息判断是否需要fixed定位。

其实也就是这样,当然它还支持了relative,stacked两种模式,因此代码更复杂些。看看从中我们能学到什么:

  • 用到了raf库控制动画,它是对requestAnimationFrame做了兼容性处理

  • 使用到 Context,以及 观察者模式

  • 居然需要监听那么多种事件(反正是我的话就只会加个scroll)

  • 用React.cloneElement来创建元素,以致于最终使用Sticky组件用起来感觉有些不寻常。

  • disableHardwareAcceleration属性用于关闭动画的硬件加速,实质上是决定是否设置transform:"translateZ(0)";

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 ,减少了参数配置代码和对两种模式stacked和relative的支持。着实简易,同时改变了组件调用形式,采用了render-props。

4. 总结

本文源于之前工作急于完成任务而留下的一个小坑,所幸现在填上了。react-sticky在github上1926个star,本身却并不复杂,通过阅读这样一个经受住开源考验的小库也能学到不少东西。

5. 讨论

欢迎讨论~

参考资料

附上一份参考资料

它的实现也很简单,就Sticky.js和Container.js两个文件,稍微讲下。代码不粘贴了点开这里看 , 。

对最后一个知识点感兴趣,老是说用transform能启动硬件加速,动画更流畅,真的假的?于是又去找了资料,。本地测试chrome的performance发现用left,top,fps,gpu,frames等一片绿色柱状线条,用 transform 则只有零星几条绿色柱状线条。感觉有道理。

如果给一个用top,left改变来做动画的元素,例如中的第一个例子,添加transform:translateZ(0),那样会有硬件加速嘛?(我测试的结果像是没有,依旧很多painting)

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