页面滚动→显示按钮
当用户滚动页面超过 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 库中,它多做了两件事:
- Context 优化:它不会让每个
Route都去监听一遍window(太浪费性能了)。它会在最外层用一个BrowserRouter监听一次,然后通过 Context 把路径传下去。 - 正则匹配:我们的代码只能匹配静态路径。真实的路由支持
/user/:id这种动态路径,底层是用正则表达式去匹配 URL 的。
未完待续…