阅读时间约: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,书的内容就不用咱多做介绍了吧~ 懂的都懂、前端攻城狮人手一份、
期待你学有所成哦 😘
One comment
博主真是太厉害了!!!