在 React.js 中更新组件 onScroll 的样式
Posted
技术标签:
【中文标题】在 React.js 中更新组件 onScroll 的样式【英文标题】:Update style of a component onScroll in React.js 【发布时间】:2015-06-25 21:02:11 【问题描述】:我在 React 中构建了一个组件,它应该在窗口滚动时更新自己的样式以创建视差效果。
组件render
方法如下所示:
function()
let style = transform: 'translateY(0px)' ;
window.addEventListener('scroll', (event) =>
let scrollTop = event.srcElement.body.scrollTop,
itemTranslate = Math.min(0, scrollTop/3 - 60);
style.transform = 'translateY(' + itemTranslate + 'px)');
);
return (
<div style=style></div>
);
这不起作用,因为 React 不知道组件已更改,因此不会重新渲染组件。
我尝试将itemTranslate
的值存储在组件的状态中,并在滚动回调中调用setState
。但是,这会导致滚动无法使用,因为这非常慢。
关于如何做到这一点的任何建议?
【问题讨论】:
永远不要在渲染方法中绑定外部事件处理程序。渲染方法(以及您在同一线程中从render
调用的任何其他自定义方法)应该只关注与渲染/更新 React 中的实际 DOM 相关的逻辑。相反,如下面的@AustinGreco 所示,您应该使用给定的 React 生命周期方法来创建和删除您的事件绑定。这使得它在组件内部自成一体,并通过确保在卸载使用它的组件时移除事件绑定来确保不会泄漏。
【参考方案1】:
您应该在componentDidMount
中绑定监听器,这样它只会创建一次。您应该能够将样式存储在状态中,侦听器可能是性能问题的原因。
类似这样的:
componentDidMount: function()
window.addEventListener('scroll', this.handleScroll);
,
componentWillUnmount: function()
window.removeEventListener('scroll', this.handleScroll);
,
handleScroll: function(event)
let scrollTop = event.srcElement.body.scrollTop,
itemTranslate = Math.min(0, scrollTop/3 - 60);
this.setState(
transform: itemTranslate
);
,
【讨论】:
我发现动画滚动事件中的 setState'ing 不稳定。我必须使用 refs 手动设置组件的样式。 handleScroll 中的“this”会指向什么?在我的情况下,它是“窗口”而不是组件。我最终将组件作为参数传递 @yuji 您可以通过在构造函数中绑定 this 来避免传递组件:this.handleScroll = this.handleScroll.bind(this)
将在 handleScroll
中将 this 绑定到组件,而不是窗口。
请注意 srcElement 在 Firefox 中不可用。
对我不起作用,但是将 scrollTop 设置为 event.target.scrollingElement.scrollTop
【参考方案2】:
为了帮助这里的任何人在使用 Austins 答案时注意到滞后的行为/性能问题,并希望使用 cmets 中提到的 refs 的示例,这是我用于切换类以向上/向下滚动的示例图标:
在渲染方法中:
<i ref=(ref) => this.scrollIcon = ref className="fa fa-2x fa-chevron-down"></i>
在处理方法中:
if (this.scrollIcon !== null)
if(($(document).scrollTop() + $(window).height() / 2) > ($('body').height() / 2))
$(this.scrollIcon).attr('class', 'fa fa-2x fa-chevron-up');
else
$(this.scrollIcon).attr('class', 'fa fa-2x fa-chevron-down');
并像 Austin 提到的那样添加/删除您的处理程序:
componentDidMount()
window.addEventListener('scroll', this.handleScroll);
,
componentWillUnmount()
window.removeEventListener('scroll', this.handleScroll);
,
refs 上的文档。
【讨论】:
你拯救了我的一天!对于更新,此时实际上不需要使用 jquery 来修改类名,因为它已经是原生 DOM 元素。所以你可以简单地做this.scrollIcon.className = whatever-you-want
。
这个解决方案打破了 React 封装,尽管我仍然不确定在没有滞后行为的情况下解决这个问题 - 也许一个去抖动的滚动事件(可能在 200-250 毫秒)将是一个解决方案
nope debounced scroll 事件仅有助于使滚动更顺畅(在非阻塞意义上),但在 DOM 中应用状态更新需要 500 毫秒到一秒:/
我也使用了这个解决方案,+1。我同意你不需要 jQuery:只需使用 className
或 classList
。另外,我不需要window.addEventListener()
:我只是用了React的onScroll
,只要你不更新props/state,它也一样快!【参考方案3】:
您可以将函数传递给 React 元素上的 onScroll
事件:https://facebook.github.io/react/docs/events.html#ui-events
<ScrollableComponent
onScroll=this.handleScroll
/>
另一个类似的答案:https://***.com/a/36207913/1255973
【讨论】:
与手动添加事件侦听器到@AustinGreco 提到的窗口元素相比,这种方法有什么好处/缺点吗? @Dennis 一个好处是您不必手动添加/删除事件侦听器。如果您在整个应用程序中手动管理多个事件侦听器,这可能是一个简单的示例,但很容易忘记在更新时正确删除它们,这可能导致内存错误。如果可能,我会一直使用内置版本。 值得注意的是,这会将滚动处理程序附加到组件本身,而不是附加到窗口,这是一个非常不同的事情。 @Dennis onScroll 的好处是它更跨浏览器和更高性能。如果您可以使用它,您可能应该使用它,但在像 OP 这样的情况下它可能没有用 我根本无法让上面的这个例子工作。有人可以为我提供如何使用 React 的onScroll
合成事件的链接吗?
@youjin ios 上的某些 IE 和 Safari 版本在使用 addEventListener
和滚动时可能会有点不稳定,而 jQuery 为您解决了很多问题(这就是 jQuery 的重点)。如果您好奇,请查看浏览器对两者的支持。我不确定 jQuery 是否比 vanilla js 更高效(实际上我确定不是),但是将滚动处理程序附加到元素本身而不是 window
是因为事件不必冒泡向上通过DOM来处理。不过总有取舍..【参考方案4】:
我制作响应式导航栏的解决方案(位置:不滚动时为“相对”,滚动时固定,不在页面顶部)
componentDidMount()
window.addEventListener('scroll', this.handleScroll);
componentWillUnmount()
window.removeEventListener('scroll', this.handleScroll);
handleScroll(event)
if (window.scrollY === 0 && this.state.scrolling === true)
this.setState(scrolling: false);
else if (window.scrollY !== 0 && this.state.scrolling !== true)
this.setState(scrolling: true);
<Navbar
style=color: '#06DCD6', borderWidth: 0, position: this.state.scrolling ? 'fixed' : 'relative', top: 0, width: '100vw', zIndex: 1
>
对我来说没有性能问题。
【讨论】:
你也可以使用一个假标题,它本质上只是一个占位符。所以你有你的固定标题,在它下面你有你的占位符 fakeheader 位置:相对。 没有性能问题,因为这不能解决问题中的视差挑战。 这怎么行? “this”不在其范围内..【参考方案5】:如果您对正在滚动的子组件感兴趣,那么这个示例可能会有所帮助:https://codepen.io/JohnReynolds57/pen/NLNOyO?editors=0011
class ScrollAwareDiv extends React.Component
constructor(props)
super(props)
this.myRef = React.createRef()
this.state = scrollTop: 0
onScroll = () =>
const scrollTop = this.myRef.current.scrollTop
console.log(`myRef.scrollTop: $scrollTop`)
this.setState(
scrollTop: scrollTop
)
render()
const
scrollTop
= this.state
return (
<div
ref=this.myRef
onScroll=this.onScroll
style=
border: '1px solid black',
width: '600px',
height: '100px',
overflow: 'scroll',
>
<p>This demonstrates how to get the scrollTop position within a scrollable
react component.</p>
<p>ScrollTop is scrollTop</p>
</div>
)
【讨论】:
【参考方案6】:要扩展@Austin 的答案,您应该将this.handleScroll = this.handleScroll.bind(this)
添加到您的构造函数中:
constructor(props)
this.handleScroll = this.handleScroll.bind(this)
componentDidMount: function()
window.addEventListener('scroll', this.handleScroll);
,
componentWillUnmount: function()
window.removeEventListener('scroll', this.handleScroll);
,
handleScroll: function(event)
let scrollTop = event.srcElement.body.scrollTop,
itemTranslate = Math.min(0, scrollTop/3 - 60);
this.setState(
transform: itemTranslate
);
,
...
这使handleScroll()
在从事件侦听器调用时可以访问正确的范围。
另外请注意,您不能在 addEventListener
或 removeEventListener
方法中执行 .bind(this)
,因为它们将各自返回对不同函数的引用,并且在组件卸载时不会删除该事件。
【讨论】:
【参考方案7】:我发现除非像这样传递 true,否则无法成功添加事件监听器:
componentDidMount = () =>
window.addEventListener('scroll', this.handleScroll, true);
,
【讨论】:
它工作正常。但是你能理解为什么我们必须将真正的布尔值传递给这个监听器吗? 来自 w3schools:[w3schools.com/jsref/met_document_addeventlistener.asp]userCapture
:可选。一个布尔值,指定事件是在捕获阶段还是在冒泡阶段执行。可能的值: true - 事件处理程序在捕获阶段执行 false - 默认值。事件处理程序在冒泡阶段执行【参考方案8】:
我通过使用和修改 CSS 变量解决了这个问题。这样我就不必修改导致性能问题的组件状态。
index.css
:root
--navbar-background-color: rgba(95,108,255,1);
Navbar.jsx
import React, Component from 'react';
import styles from './Navbar.module.css';
class Navbar extends Component
documentStyle = document.documentElement.style;
initalNavbarBackgroundColor = 'rgba(95, 108, 255, 1)';
scrolledNavbarBackgroundColor = 'rgba(95, 108, 255, .7)';
handleScroll = () =>
if (window.scrollY === 0)
this.documentStyle.setProperty('--navbar-background-color', this.initalNavbarBackgroundColor);
else
this.documentStyle.setProperty('--navbar-background-color', this.scrolledNavbarBackgroundColor);
componentDidMount()
window.addEventListener('scroll', this.handleScroll);
componentWillUnmount()
window.removeEventListener('scroll', this.handleScroll);
render ()
return (
<nav className=styles.Navbar>
<a href="/">Home</a>
<a href="#about">About</a>
</nav>
);
;
export default Navbar;
Navbar.module.css
.Navbar
background: var(--navbar-background-color);
【讨论】:
【参考方案9】:使用 useEffect 的函数组件示例:
注意:您需要通过在 useEffect 中返回“清理”函数来移除事件监听器。如果你不这样做,每次组件更新时,你都会有一个额外的窗口滚动监听器。
import React, useState, useEffect from "react"
const ScrollingElement = () =>
const [scrollY, setScrollY] = useState(0);
function logit()
setScrollY(window.pageYOffset);
useEffect(() =>
function watchScroll()
window.addEventListener("scroll", logit);
watchScroll();
// Remove listener (like componentWillUnmount)
return () =>
window.removeEventListener("scroll", logit);
;
, []);
return (
<div className="App">
<div className="fixed-center">Scroll position: scrollYpx</div>
</div>
);
【讨论】:
【参考方案10】:使用classNames、React hooks useEffect、useState 和styled-jsx 的示例:
import classNames from 'classnames'
import useEffect, useState from 'react'
const Header = _ =>
const [ scrolled, setScrolled ] = useState()
const classes = classNames('header',
scrolled: scrolled,
)
useEffect(_ =>
const handleScroll = _ =>
if (window.pageYOffset > 1)
setScrolled(true)
else
setScrolled(false)
window.addEventListener('scroll', handleScroll)
return _ =>
window.removeEventListener('scroll', handleScroll)
, [])
return (
<header className=classes>
<h1>Your website</h1>
<style jsx>`
.header
transition: background-color .2s;
.header.scrolled
background-color: rgba(0, 0, 0, .1);
`</style>
</header>
)
export default Header
【讨论】:
【参考方案11】:我的赌注是使用带有新钩子的函数组件来解决它,但不是像以前的答案那样使用useEffect
,我认为正确的选项是useLayoutEffect
,原因很重要:
签名与 useEffect 相同,但它是同步触发的 在所有 DOM 突变之后。
这可以在React documentation 中找到。如果我们改用 useEffect
并重新加载已经滚动的页面,则 scrolled 将为 false 并且我们的类将不会被应用,从而导致不需要的行为。
一个例子:
import React, useState, useLayoutEffect from "react"
const Mycomponent = (props) =>
const [scrolled, setScrolled] = useState(false)
useLayoutEffect(() =>
const handleScroll = e =>
setScrolled(window.scrollY > 0)
window.addEventListener("scroll", handleScroll)
return () =>
window.removeEventListener("scroll", handleScroll)
, [])
...
return (
<div className=scrolled ? "myComponent--scrolled" : "">
...
</div>
)
问题的可能解决方案是https://codepen.io/dcalderon/pen/mdJzOYq
const Item = (props) =>
const [scrollY, setScrollY] = React.useState(0)
React.useLayoutEffect(() =>
const handleScroll = e =>
setScrollY(window.scrollY)
window.addEventListener("scroll", handleScroll)
return () =>
window.removeEventListener("scroll", handleScroll)
, [])
return (
<div class="item" style='--scrollY': `$Math.min(0, scrollY/3 - 60)px`>
Item
</div>
)
【讨论】:
我很好奇useLayoutEffect
。我想看看你提到了什么。
如果您不介意,您能否提供一个 repo + 发生这种情况的视觉示例?与useLayoutEffect
相比,我无法重现您在此处提到的useEffect
问题。
当然! https://github.com/calderon/uselayout-vs-uselayouteffect。昨天发生在我身上的类似行为。顺便说一句,我是一个反应新手,可能我完全错了 :D :D
其实我已经尝试过很多次了,重新加载了很多次,有时它会出现红色而不是蓝色的标题,这意味着它有时会应用.Header--scrolled
类,但100%次@ 987654334@ 正确应用感谢 useLayoutEffect。
我将 repo 移至 github.com/calderon/useeffect-vs-uselayouteffect【参考方案12】:
带钩子:
import React, useEffect, useState from 'react';
function MyApp ()
const [offset, setOffset] = useState(0);
useEffect(() =>
const onScroll = () => setOffset(window.pageYOffset);
// clean up code
window.removeEventListener('scroll', onScroll);
window.addEventListener('scroll', onScroll, passive: true );
return () => window.removeEventListener('scroll', onScroll);
, []);
console.log(offset);
;
【讨论】:
这是迄今为止最有效和最优雅的答案。谢谢你。 如果你走这条路,一定要在组件卸载时使用cleanup function来移除监听器。 这甚至没有使用addEventListener
和 removeEventListener
并且有 30 个赞。我想知道我们有多少内存泄漏的应用程序......正如布拉德利格里菲斯所说,使用useEffect
cleanup。
我不确定,但我认为这不会导致内存泄漏,因为onscroll
是不是一个窗口上只有一个onscroll
,而可能有很多eventListener
s。这就是为什么在这种情况下也不需要清理功能。但是,如果我错了,请纠正我。相关:***.com/questions/60745475/…
如果您在应用程序中只有一个滚动事件,但如果您在同一页面中有两个滚动事件,这将非常有效。第二个将覆盖第一个。所以我建议使用事件监听器并在组件卸载时清理它。【参考方案13】:
这是另一个使用 HOOKS fontAwesomeIcon 和 Kendo UI React 的示例 [![截图][1]][1]
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
const ScrollBackToTop = () =>
const [show, handleShow] = useState(false);
useEffect(() =>
window.addEventListener('scroll', () =>
if (window.scrollY > 1200)
handleShow(true);
else handleShow(false);
);
return () =>
window.removeEventListener('scroll');
;
, []);
const backToTop = () =>
window.scroll( top: 0, behavior: 'smooth' );
;
return (
<div>
show && (
<div className="backToTop text-center">
<button className="backToTop-btn k-button " onClick=() => backToTop() >
<div className="d-none d-xl-block mr-1">Top</div>
<FontAwesomeIcon icon="chevron-up"/>
</button>
</div>
)
</div>
);
;
export default ScrollBackToTop;```
[1]: https://i.stack.imgur.com/ZquHI.png
【讨论】:
这太棒了。我在 useEffect() 使用 window.onscroll() 更改导航栏粘性状态时遇到问题...通过此答案发现 window.addEventListener() 和 window.removeEventListener() 是控制粘性的正确方法带有功能组件的导航栏...谢谢!【参考方案14】:使用 React Hooks 更新答案
这是两个钩子 - 一个用于方向(上/下/无),一个用于实际位置
这样使用:
useScrollPosition(position =>
console.log(position)
)
useScrollDirection(direction =>
console.log(direction)
)
这里是钩子:
import useState, useEffect from "react"
export const SCROLL_DIRECTION_DOWN = "SCROLL_DIRECTION_DOWN"
export const SCROLL_DIRECTION_UP = "SCROLL_DIRECTION_UP"
export const SCROLL_DIRECTION_NONE = "SCROLL_DIRECTION_NONE"
export const useScrollDirection = callback =>
const [lastYPosition, setLastYPosition] = useState(window.pageYOffset)
const [timer, setTimer] = useState(null)
const handleScroll = () =>
if (timer !== null)
clearTimeout(timer)
setTimer(
setTimeout(function ()
callback(SCROLL_DIRECTION_NONE)
, 150)
)
if (window.pageYOffset === lastYPosition) return SCROLL_DIRECTION_NONE
const direction = (() =>
return lastYPosition < window.pageYOffset
? SCROLL_DIRECTION_DOWN
: SCROLL_DIRECTION_UP
)()
callback(direction)
setLastYPosition(window.pageYOffset)
useEffect(() =>
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
)
export const useScrollPosition = callback =>
const handleScroll = () =>
callback(window.pageYOffset)
useEffect(() =>
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
)
【讨论】:
【参考方案15】:constructor()
super()
this.state =
change: false
componentDidMount()
window.addEventListener('scroll', this.handleScroll);
console.log('add event');
componentWillUnmount()
window.removeEventListener('scroll', this.handleScroll);
console.log('remove event');
handleScroll = e =>
if (window.scrollY === 0)
this.setState( change: false );
else if (window.scrollY > 0 )
this.setState( change: true );
render() return ( <div className="main" style= boxShadow: this.state.change ?
0px 6px 12px rgba(3,109,136,0.14):
none ></div>
我就是这样做的,而且效果很好。
【讨论】:
【参考方案16】:如果您发现上述答案不适合您,请尝试以下操作:
React.useEffect(() =>
document.addEventListener('wheel', yourCallbackHere)
return () =>
document.removeEventListener('wheel', yourCallbackHere)
, [yourCallbackHere])
基本上,您需要尝试document
而不是window
,以及wheel
而不是scroll
。
编码愉快!
【讨论】:
非常感谢,为什么要使用***,这是新功能?***工作正常,这个小任务浪费了 2-3 个小时【参考方案17】:我经常收到有关渲染的警告。此代码有效,但不确定它是否是最佳解决方案。
const listenScrollEvent = () =>
if (window.scrollY <= 70)
setHeader("header__main");
else if (window.scrollY >= 70)
setHeader("header__slide__down");
;
useEffect(() =>
window.addEventListener("scroll", listenScrollEvent);
return () =>
window.removeEventListener("scroll", listenScrollEvent);
, []);
【讨论】:
以上是关于在 React.js 中更新组件 onScroll 的样式的主要内容,如果未能解决你的问题,请参考以下文章
在 react js 功能组件中调度操作后如何立即在 redux store 中使用更新的值