【问题标题】:Update style of a component onScroll in React.js在 React.js 中更新组件 onScroll 的样式
【发布时间】: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 生命周期方法来创建和删除您的事件绑定。这使得它在组件内部自成一体,并通过确保在卸载使用它的组件时移除事件绑定来确保不会泄漏。

标签: javascript reactjs


【解决方案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:只需使用 classNameclassList。另外,我不需要window.addEventListener():我只是用了React的onScroll,只要你不更新props/state,它也一样快!
【解决方案3】:

您可以将函数传递给 React 元素上的 onScroll 事件:https://facebook.github.io/react/docs/events.html#ui-events

<ScrollableComponent
 onScroll={this.handleScroll}
/>

另一个类似的答案:https://stackoverflow.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() 在从事件侦听器调用时可以访问正确的范围。

    另外请注意,您不能在 addEventListenerremoveEventListener 方法中执行 .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: {scrollY}px</div>
            </div>
          );
        }
        

        【讨论】:

          【解决方案10】:

          使用classNames、React hooks useEffectuseStatestyled-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。
            【解决方案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来移除监听器。
            • 这甚至没有使用 addEventListenerremoveEventListener 并且有 30 个赞。我想知道我们有多少内存泄漏的应用程序......正如布拉德利格里菲斯所说,使用useEffectcleanup。
            • 我不确定,但我认为这不会导致内存泄漏,因为onscroll 是不是一个窗口上只有一个onscroll,而可能有很多eventListener s。这就是为什么在这种情况下也不需要清理功能。但是,如果我错了,请纠正我。相关:stackoverflow.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 ( &lt;div className="main" style={{ boxShadow: this.state.change ? 0px 6px 12px rgba(3,109,136,0.14):none}} &gt;&lt;/div&gt;

              我就是这样做的,而且效果很好。

              【讨论】:

                【解决方案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);
                    }
                }, []);
                

                【讨论】:

                  猜你喜欢
                  • 2018-10-15
                  • 1970-01-01
                  • 2021-08-14
                  • 1970-01-01
                  • 2021-01-29
                  • 1970-01-01
                  • 2019-04-30
                  • 2014-09-16
                  • 2020-06-19
                  相关资源
                  最近更新 更多