返回文章列表
frontend2026年1月8日2 分钟阅读

页面滚动→显示按钮

当用户滚动页面超过 300px 时,显示一个“回到顶部”的按钮。

import React, { useState, useEffect } from 'react'; function ScrollToTop() { const [show, setShow] = useState(false); useEffect(() => { // 1. 定义处理函数 const handleScroll = () => { console.log("正在滚动..."); // 验证是否多次注册 if (window.scrollY > 300) { setShow(true); } else { setShow(false); } }; // 2. 在 Mount 之后绑定 window.addEventListener('scroll', handleScroll); // 3. 必须:在 Unmount 之前清理 // 底层逻辑:当组件销毁,React 会调用这个返回函数 return () => { window.removeEventListener('scroll', handleScroll); }; }, []); // 空数组确保只在挂载和卸载时执行 const scrollToTop = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }; return ( show && ( <button onClick={scrollToTop} style={{ position: 'fixed', bottom: 50, right: 50 }} > Back to Top </button> ) ); }

如果监听的是“特定容器”而非 Window -> 使用useRef

function ScrollBox() { const containerRef = React.useRef(null); useEffect(() => { const box = containerRef.current; // 通过 ref 拿到 DOM if (!box) return; const handleScroll = () => { console.log("容器内滚动:", box.scrollTop); }; // 在特定元素上绑定 box.addEventListener('scroll', handleScroll); return () => box.removeEventListener('scroll', handleScroll); }, []); // 注意:如果 containerRef 指向的元素会变,这里逻辑会更复杂 return ( <div ref={containerRef} style={{ height: '200px', overflow: 'auto' }}> <div style={{ height: '1000px' }}>滚动我内容</div> </div> ); }

如果我的 handleScroll 函数里需要用到一个由 props 传进来的 threshold不是写死的数字?

function ScrollToTop({ threshold }) { // threshold 从 props 传进来 const [show, setShow] = React.useState(false); React.useEffect(() => { const handleScroll = () => { // 这里的 threshold 永远是当前渲染周期的新值 if (window.scrollY > threshold) { setShow(true); } else { setShow(false); } }; window.addEventListener('scroll', handleScroll); return () => { // 关键:每次 threshold 变化,都会先跑这里,注销旧的监听 window.removeEventListener('scroll', handleScroll); }; }, [threshold]); // 依赖项必须包含 threshold // ... render ... }

底层性能角度看,这里隐藏着一个工程痛点:

  • 问题:如果 threshold 频繁变化(虽然实际业务中少见,但假设它变了),useEffect 就会不断地“卸载-挂载-卸载-挂载”。对于全局 window 监听来说,这虽然不至于卡顿,但不是最优解。

引入useRef→跳出闭包陷阱

这个模式在开发高性能组件(如复杂图表、拖拽组件)时非常常用。其实可以总结记忆「当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref

function ScrollToTop({ threshold }) { const [show, setShow] = React.useState(false); // 1. 创建 Ref const thresholdRef = React.useRef(threshold); // 2. 每次渲染时,手动更新 current // 因为这是在 Render 阶段执行的赋值,它是同步的,极快 thresholdRef.current = threshold; React.useEffect(() => { const handleScroll = () => { // 3. 这里永远能拿到最新的 current if (window.scrollY > thresholdRef.current) { setShow(true); } else { setShow(false); } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); // 保持空数组,只绑定一次 // ... render ... }

Route和Link原理

import React, { useState, useEffect } from 'react'; // 1. 这是一个拦截器:点击时不让浏览器刷新,只改地址栏 const Link = ({ to, children }) => { const handleClick = (e) => { e.preventDefault(); // 阻止浏览器默认的“全页面跳转” window.history.pushState({}, '', to); // 改变地址栏,但不刷新 // 自定义一个事件通知路由:地址变了! const navEvent = new PopStateEvent('popstate'); window.dispatchEvent(navEvent); }; return <a href={to} onClick={handleClick}>{children}</a>; }; // 2. 这是一个匹配器:根据当前地址显示内容 const Route = ({ path, children }) => { // 核心:用状态来驱动 UI 渲染 const [currentPath, setCurrentPath] = useState(window.location.pathname); useEffect(() => { const onLocationChange = () => { setCurrentPath(window.location.pathname); // 当地址变了,更新状态触发重绘 }; // 监听浏览器的“前进/后退”或我们手动触发的事件 window.addEventListener('popstate', onLocationChange); return () => window.removeEventListener('popstate', onLocationChange); }, []); return currentPath === path ? children : null; }; // 3. 使用方式 export default function App() { return ( <div> <nav> <Link to="/">首页</Link> | <Link to="/about">关于</Link> </nav> <hr /> <Route path="/"> <h2>这是首页内容</h2> </Route> <Route path="/about"> <h2>这是关于页面</h2> </Route> </div> ); }

在实际的 react-router-dom 库中,它多做了两件事:

  1. Context 优化:它不会让每个 Route 都去监听一遍 window(太浪费性能了)。它会在最外层用一个 BrowserRouter 监听一次,然后通过 Context 把路径传下去。
  2. 正则匹配:我们的代码只能匹配静态路径。真实的路由支持 /user/:id 这种动态路径,底层是用正则表达式去匹配 URL 的。

未完待续…

© 2026 Blog Owner. All rights reserved.