Loading... 阅读时间约:15 分钟。 ***依照惯例,文末有福利哦~*** --- 前面我们讨论了 `useRef`、`forwardRef` 和 `useImperativeHandle`,他们都能进行 `ref` 操作。除了这三个方法之外,React 中还有许多方法能让我们方便的操作 `ref`。我们将继续讨论与 ref 操作相关的场景。 不过,在正式开始之前,我们先回顾一下之前关于《[React必知必会:通过 MutationObserver 实现无限滚动的功能](https://mp.weixin.qq.com/s?__biz=MzIzMDM4MzE3Mw==&mid=2247485032&idx=1&sn=e3563a586fb4d1e7bf2902494f15c757&chksm=e8b50d42dfc284540f7442c11334b56a3f3357637057b674094000f0f79a3e26e75914367fc3#rd)》中的内容。`MutationObserver` 方法是 Web API 的一部分,它主要用于监听 DOM 中的变化,比如 DOM 的增删改操作,当相应的变化发生时就会触发 `MutationObserver` 的回调函数。 而在本文中,我们将讨论一个 React 中更加准确和直接的方法,以此来监听滚动位置从而实现一个更高性能的“无限滚动”组件。 如果你对 `MutationObserver` 还不太了解,建议你可以先看看上面的文章。在对 `MutationObserver` 有一个基本的认识后再阅读此文,可能会对你有一些额外的启发。当然,你也可以先收藏上面的文章稍后再看,直接阅读本文不会对你在理解上有任何影响。 让我们开始吧~ ## 初识 `useLayoutEffect` 想必大家对 useEffect 已经非常了解了吧,而 useLayoutEffect 与 useEffect 就非常类似。它是 React 中的一个钩子函数,用于在组件渲染之后执行副作用操作,例如更新 DOM,发送网络请求等等。不同的是,`useLayoutEffect` 会在 DOM 更新完成之后**同步**执行副作用操作,而 `useEffect` 则是在 DOM 更新完成之后**异步**执行副作用操作。 下面我们通过一个简单的示例来演示 useLayoutEffect 的基本用法: ```jsx 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钩子函数](https://mp.weixin.qq.com/s?__biz=MzIzMDM4MzE3Mw==&mid=2247485109&idx=1&sn=91eeea7f7b53e0d8a136d4877ad65fea&chksm=e8b50d9fdfc284894782520d9ae159e0debf47a30a5d3a810f9ec3435799c4c10dd4e9983090#rd) * [提升React组件灵活性:深入了解forwardRef API的妙用](https://mp.weixin.qq.com/s?__biz=MzIzMDM4MzE3Mw==&mid=2247485147&idx=1&sn=d0948ec534dd2fbdabea15fc051edef4&chksm=e8b50df1dfc284e729f0d52fc986f40e9b7d8f40290d412f37cf0ac647ea262fd0595b1133c9#rd) * [不看后悔系列!进阶React开发技巧:如何灵活运用useImperativeHandle](https://mp.weixin.qq.com/s?__biz=MzIzMDM4MzE3Mw==&mid=2247485191&idx=1&sn=8a77728ec1a97e9a558b1b0542dce186&chksm=e8b50c2ddfc2853bf0a17ba6b5c35a0c446a3cf5232aa8b69df7ee0547c8cb9fb178432cf641#rd) 首先我们定义一个父组件 ParentComponent: ```jsx 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` 组件,滚动加载的具体实现将在子组件中完成。 ```jsx 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 实现的无限滚动的功能来说,上面的示例有以下优点: 1. 通过使用 `useImperativeHandle` 钩子函数,我们可以将 `reset()` 方法暴露给父组件,使得父组件可以通过调用该方法来重置子组件的状态和数据。这样做的好处是,在父组件需要重新加载数据时,不需要卸载和重新挂载子组件,而只需要调用 `reset()` 方法即可,从而提高了代码的可维护性和性能。同时,使用 React 钩子函数更符合现代 React 的开发方式,实现的代码更简洁、易懂、 2. 在这个例子中 `useLayoutEffect` 比 `useEffect` 的性能更好,它能够在 DOM 更新之前同步执行,因此使用 `useLayoutEffect` 可以更快地响应滚动事件,从而提高了用户的体验,而且也可以更好地优化性能。而我们之前通过 `MutationObserver` 实现的代码中使用的是 `useEffect`,`useEffect` 是在 DOM 更新之后异步执行,可能会有一定的性能损失。(如果你对这里反复提及的《[通过 MutationObserver 实现无限滚动的功能](https://mp.weixin.qq.com/s?__biz=MzIzMDM4MzE3Mw==&mid=2247485032&idx=1&sn=e3563a586fb4d1e7bf2902494f15c757&chksm=e8b50d42dfc284540f7442c11334b56a3f3357637057b674094000f0f79a3e26e75914367fc3#rd)》一文感兴趣,可以移步去阅读一下具体的实现代码。) ## 总结 在实现滚动加载组件时,我们结合了 `useRef`、`forwardRef`、`useImperativeHandle` 和 `useLayoutEffect`,数量运用 React 内置的这些函数方法能让我们写出更加灵活、健壮、可扩展的组件,同时也可以让我们在编写 React 程序的过程中更加得心应手。 --- ***福利来咯~*** 后台回复 **JavaScript权威指南** 可获取这本书的电子版哦,电子书是最新的第 6 版。书名全称为《JavaScript权威指南(原书第6版)》,作者是大名鼎鼎的David Flanagan,书的内容就不用咱多做介绍了吧~ 懂的都懂、前端攻城狮人手一份、 期待你学有所成哦 😘 Last modification:April 14, 2023 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 0 如果觉得我的文章对你有用,请随意赞赏