阅读时间约:15 分钟。
依照惯例,文末有福利哦~
前面我们讨论了 useRef、forwardRef 和 useImperativeHandle,他们都能进行 ref 操作。除了这三个方法之外,React 中还有许多方法能让我们方便的操作 ref。我们将继续讨论与 ref 操作相关的场景。
不过,在正式开始之前,我们先回顾一下之前关于《React必知必会:通过 MutationObserver 实现无限滚动的功能》中的内容。MutationObserver 方法是 Web API 的一部分,它主要用于监听 DOM 中的变化,比如 DOM 的增删改操作,当相应的变化发生时就会触发 MutationObserver 的回调函数。
而在本文中,我们将讨论一个 React 中更加准确和直接的方法,以此来监听滚动位置从而实现一个更高性能的“无限滚动”组件。
如果你对 MutationObserver 还不太了解,建议你可以先看看上面的文章。在对 MutationObserver 有一个基本的认识后再阅读此文,可能会对你有一些额外的启发。当然,你也可以先收藏上面的文章稍后再看,直接阅读本文不会对你在理解上有任何影响。
让我们开始吧~
初识 useLayoutEffect
想必大家对 useEffect 已经非常了解了吧,而 useLayoutEffect 与 useEffect 就非常类似。它是 React 中的一个钩子函数,用于在组件渲染之后执行副作用操作,例如更新 DOM,发送网络请求等等。不同的是,useLayoutEffect 会在 DOM 更新完成之后同步执行副作用操作,而 useEffect 则是在 DOM 更新完成之后异步执行副作用操作。
下面我们通过一个简单的示例来演示 useLayoutEffect 的基本用法:
import React, { useState, useLayoutEffect, useRef } from 'react';
function Example() {
// 使用 useState 创建一个名为 scrollPosition 的状态,初始值为 0
const [scrollPosition, setScrollPosition] = useState(0);
// 使用 useRef 创建一个名为 containerRef 的引用,初始值为 null
const containerRef = useRef(null);
// 使用 useLayoutEffect 声明一个副作用函数
useLayoutEffect(() => {
// 定义一个名为 handleScroll 的函数,用于处理滚动事件
function handleScroll() {
// 调用 setScrollPosition 更新 scrollPosition 状态为滚动容器当前的 scrollTop 属性值
setScrollPosition(containerRef.current.scrollTop);
}
// 在滚动容器上添加一个名为 scroll 的事件监听器,当发生滚动时调用 handleScroll 函数
containerRef.current.addEventListener('scroll', handleScroll);
// 返回一个清理函数,用于移除事件监听器
return () => containerRef.current.removeEventListener('scroll', handleScroll);
}, []);
// 返回一个 JSX 元素,包含一个带有 ref 属性的 div 元素和一个展示当前滚动位置的 div 元素
return (
<div ref={containerRef} style={{ height: '100px', overflow: 'scroll' }}>
<div style={{ height: '500px' }}>Scrollable Content</div>
<div>Current Scroll Position: {scrollPosition}</div>
</div>
);
}上面的示例中,我们定义了一个名为 Example 的函数组件。组件中有三个较为重要的部分:状态、引用和副作用函数。
首先,使用 useState 创建一个名为 scrollPosition 的状态,其初始值为 0。这个状态用于存储滚动位置。其次,使用 useRef 创建一个名为 containerRef 的引用,初始值为 null。这个引用用于将容器元素与滚动事件处理程序关联起来。最后,使用 useLayoutEffect 声明一个副作用函数。这个副作用函数会在组件挂载时执行,用于将滚动事件处理程序附加到容器元素上,并在组件卸载时清除该处理程序。
副作用函数内部定义了一个名为 handleScroll 的函数,用于处理滚动事件。该函数使用 setScrollPosition 函数来更新 scrollPosition 状态为滚动容器当前的 scrollTop 属性值。然后,使用 addEventListener 函数将 handleScroll 函数附加到滚动容器上,以便在滚动时自动调用该函数。
了解了 useLayoutEffect 的基本用法,下面便让我们实现滚动加载组件。希望通过这个滚动加载的组件能让你更加深入地了解 useLayoutEffect。
滚动加载组件的具体实现
下面的示例代码结合了之前我们讨论的 useRef、forwardRef 和 useImperativeHandle 相关的知识点,建议在阅读代码之前对着三个方法有一个基本的认识。如果你对这几个方法感兴趣或者对他们还不够了解,可以看看这三篇文章:
- 仅此一文,让你全完掌握React中的useRef钩子函数
- 提升React组件灵活性:深入了解forwardRef API的妙用
- 不看后悔系列!进阶React开发技巧:如何灵活运用useImperativeHandle
首先我们定义一个父组件 ParentComponent:
import { useRef } from 'react';
// 引入子组件,稍后我们将实现这个组件
import ScrollLoadComponent from './ScrollLoadComponent';
function ParentComponent() {
// 创建一个 ref 对象,用于引用子组件 ScrollLoadComponent 的实例
const scrollLoadRef = useRef(null);
// 定义重置函数
function handleReset() {
// 通过 ref 对象调用子组件 ScrollLoadComponent 的 reset 方法
scrollLoadRef.current.reset();
}
return (
<div>
{/* 渲染一个按钮,当点击时调用 handleReset 函数 */}
<button onClick={handleReset}>Reset</button>
{/* 渲染子组件 ScrollLoadComponent,并将 ref 对象传递给子组件,使得可以在父组件中调用子组件的方法 */}
<ScrollLoadComponent ref={scrollLoadRef} />
</div>
);
}
export default ParentComponent;上面的代码中包含一个 button 元素和一个 ScrollLoadComponent 组件。我们使用 useRef 创建一个 scrollLoadRef 引用 ScrollLoadComponent 组件的实例,并将其传递给子组件的 ref 属性。当我们点击 Reset 按钮时,我们通过 scrollLoadRef 调用子组件 ScrollLoadComponent 的 reset 方法,以便重置子组件的状态。
下面我们编写 ScrollLoadComponent 组件,滚动加载的具体实现将在子组件中完成。
import { useState, useLayoutEffect, useRef, forwardRef, useImperativeHandle } from 'react';
// 使用 forwardRef 来创建一个可以被父组件引用的子组件
const ScrollLoadComponent = forwardRef((props, ref) => {
// 创建多个状态变量,用于控制组件的行为
const [data, setData] = useState([]); // 当前已加载的数据
const [isLoading, setIsLoading] = useState(false); // 当前是否正在加载数据
const [hasMore, setHasMore] = useState(true); // 当前是否还有更多数据可供加载
const containerRef = useRef(null); // 创建一个 ref 对象,用于引用组件的容器元素
// 使用 useImperativeHandle 钩子来暴露一个重置函数,供父组件调用
useImperativeHandle(ref, () => ({
reset() {
setData([]);
setIsLoading(false);
setHasMore(true);
containerRef.current.scrollTop = 0;
},
}));
// 使用 useLayoutEffect 钩子来监听组件容器的滚动事件,并在需要时加载更多数据
useLayoutEffect(() => {
function handleScroll() {
const container = containerRef.current;
if (!container) return;
const scrollBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
// 判断是否需要加载更多数据,这里的阈值为 50px
if (scrollBottom < 50 && !isLoading && hasMore) {
setIsLoading(true);
loadMoreData();
}
}
containerRef.current.addEventListener('scroll', handleScroll);
// 在组件卸载时移除监听器
return () => containerRef.current.removeEventListener('scroll', handleScroll);
}, [isLoading, hasMore]);
// 加载更多数据的函数
function loadMoreData() {
setTimeout(() => {
const newData = [...data, ...Array.from({ length: 10 }, (_, i) => `Item ${data.length + i}`)];
setData(newData);
setIsLoading(false);
setHasMore(newData.length < 50);
}, 1000);
}
// 渲染组件的 UI
return (
<div style={{ height: '300px', overflow: 'auto' }} ref={containerRef}>
{data.map((item, index) => (
<div key={index}>{item}</div>
))}
{isLoading && <div>Loading...</div>}
</div>
);
});
export default ScrollLoadComponent;子组件 ScrollLoadComponent 的实现较为复杂,结合了我们之前提到的 useRef、forwardRef 和 useImperativeHandle 方法,而代码中的副作用函数 useLayoutEffect 则实现了加载更多数据的功能。
首先使用 forwardRef 来创建一个可转发 ref 的组件。然后,我们使用 useRef 创建一个 containerRef 引用滚动容器的 DOM 元素。我们还使用 useImperativeHandle 将一个 reset 方法转发给外部组件,用于在父组件 ParentComponent 中重置该组件的状态。
之后,我们在 useLayoutEffect 中监听滚动事件,并在滚动到距离底部 50px 以内时,触发加载更多数据的操作。在 loadMoreData 函数中,我们使用 setTimeout 模拟异步加载更多数据的过程。当数据加载完成后更新组件的状态,并检查是否还有更多数据需要加载。如果数据已经加载完毕,便将 hasMore 状态设置为 false,从而停止触发加载更多数据的操作。
最后,我们将 data 数组中的每个元素渲染成一个 <div> 元素,用来展示组件的内容。当正在加载更多数据时,页面底部显示一个 Loading... 提示文案用来提醒用户数据正在加载中。
相比于之前我们通过 MutationObserver 实现的无限滚动的功能来说,上面的示例有以下优点:
- 通过使用
useImperativeHandle钩子函数,我们可以将reset()方法暴露给父组件,使得父组件可以通过调用该方法来重置子组件的状态和数据。这样做的好处是,在父组件需要重新加载数据时,不需要卸载和重新挂载子组件,而只需要调用reset()方法即可,从而提高了代码的可维护性和性能。同时,使用 React 钩子函数更符合现代 React 的开发方式,实现的代码更简洁、易懂、 - 在这个例子中
useLayoutEffect比useEffect的性能更好,它能够在 DOM 更新之前同步执行,因此使用useLayoutEffect可以更快地响应滚动事件,从而提高了用户的体验,而且也可以更好地优化性能。而我们之前通过MutationObserver实现的代码中使用的是useEffect,useEffect是在 DOM 更新之后异步执行,可能会有一定的性能损失。(如果你对这里反复提及的《通过 MutationObserver 实现无限滚动的功能》一文感兴趣,可以移步去阅读一下具体的实现代码。)
总结
在实现滚动加载组件时,我们结合了 useRef、forwardRef、useImperativeHandle 和 useLayoutEffect,数量运用 React 内置的这些函数方法能让我们写出更加灵活、健壮、可扩展的组件,同时也可以让我们在编写 React 程序的过程中更加得心应手。
福利来咯~
后台回复 JavaScript权威指南 可获取这本书的电子版哦,电子书是最新的第 6 版。书名全称为《JavaScript权威指南(原书第6版)》,作者是大名鼎鼎的David Flanagan,书的内容就不用咱多做介绍了吧~ 懂的都懂、前端攻城狮人手一份、
期待你学有所成哦 😘
5 comments
华纳圣淘沙开户步骤详解(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司开户流程全解析(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司账户注册指南(183-8890-9465—?薇-STS5099【6011643】
新手如何开通华纳圣淘沙公司账户(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙企业开户标准流程(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司开户:从零到一(183-8890-9465—?薇-STS5099【6011643】
官方指南:华纳圣淘沙公司开户流程(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司开户流程说明书(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙开户步骤详解(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司开户流程全解析(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司账户注册指南(183-8890-9465—?薇-STS5099【6011643】
新手如何开通华纳圣淘沙公司账户(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙企业开户标准流程(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司开户:从零到一(183-8890-9465—?薇-STS5099【6011643】
官方指南:华纳圣淘沙公司开户流程(183-8890-9465—?薇-STS5099【6011643】
华纳圣淘沙公司开户流程说明书(183-8890-9465—?薇-STS5099【6011643】
东方明珠客服开户联系方式【182-8836-2750—】?μ- cxs20250806
东方明珠客服电话联系方式【182-8836-2750—】?- cxs20250806】
东方明珠开户流程【182-8836-2750—】?薇- cxs20250806】
东方明珠客服怎么联系【182-8836-2750—】?薇- cxs20250806】
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
博主真是太厉害了!!!