阅读时间约:15 分钟。

依照惯例,文末有福利哦~


前面我们讨论了 useRefforwardRefuseImperativeHandle,他们都能进行 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。

滚动加载组件的具体实现

下面的示例代码结合了之前我们讨论的 useRefforwardRefuseImperativeHandle 相关的知识点,建议在阅读代码之前对着三个方法有一个基本的认识。如果你对这几个方法感兴趣或者对他们还不够了解,可以看看这三篇文章:

首先我们定义一个父组件 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 调用子组件 ScrollLoadComponentreset 方法,以便重置子组件的状态。

下面我们编写 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 的实现较为复杂,结合了我们之前提到的 useRefforwardRefuseImperativeHandle 方法,而代码中的副作用函数 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. 在这个例子中 useLayoutEffectuseEffect 的性能更好,它能够在 DOM 更新之前同步执行,因此使用 useLayoutEffect 可以更快地响应滚动事件,从而提高了用户的体验,而且也可以更好地优化性能。而我们之前通过 MutationObserver 实现的代码中使用的是 useEffectuseEffect 是在 DOM 更新之后异步执行,可能会有一定的性能损失。(如果你对这里反复提及的《通过 MutationObserver 实现无限滚动的功能》一文感兴趣,可以移步去阅读一下具体的实现代码。)

总结

在实现滚动加载组件时,我们结合了 useRefforwardRefuseImperativeHandleuseLayoutEffect,数量运用 React 内置的这些函数方法能让我们写出更加灵活、健壮、可扩展的组件,同时也可以让我们在编写 React 程序的过程中更加得心应手。


福利来咯~

后台回复 JavaScript权威指南 可获取这本书的电子版哦,电子书是最新的第 6 版。书名全称为《JavaScript权威指南(原书第6版)》,作者是大名鼎鼎的David Flanagan,书的内容就不用咱多做介绍了吧~ 懂的都懂、前端攻城狮人手一份、

期待你学有所成哦 😘

Last modification:August 15, 2024
如果觉得我的文章对你有用,请随意赞赏