本文是关于如何在 React Native 应用程序中为组件实现可见性传感器。 借助垂直或水平 FlatList及其 onViewableItemsChangedprop ,只要列表项在视口中出现或消失,就可以观察事件。 使用这个概念,您可以对这些事件做出反应,例如,自动开始播放视频或跟踪“组件看到的事件”以用于营销目的。

在这篇文章中,我们将讨论如何使用示例项目在 React Native 中实现组件可见性传感器,并通过以下部分逐步构建解决方案:

每个“临时解决方案”都包含对解决方案不完整的原因的解释(即,由于 React Native 呈现错误,正如您将看到的那样)。 我想解释为什么事情不起作用的不同原因; 我们的局限性之间存在相互作用 FlatList回调,必须是稳定的,以及这些 React memoization Hooks ( useCallback, useEffect等)工作。

示例项目的范围

本文中提供的代码示例是 GitHub 配套项目 的一部分。 这是一个 搭建的Expo 项目 使用 TypeScript 并使用 create-expo-app 。

演示用例故意保持简单,只专注于使用 FlatListAPI。 它显示 星球大战 角色并跟踪出现在屏幕上至少两秒钟的人。 如果它们出现多次,它们将只被跟踪一次。 因此,示例应用程序在可见性事件上没有花哨的动画,因为这超出了范围。

相反,当 FlatList触发可见性更改事件只是一个跟踪功能,以使事情变得简单。 这与跟踪常见业务应用程序中的用户事件是一致的。

以下截屏视频展示了示例应用程序。

一看 FlatList的 API

在深入研究示例项目的实现细节之前,让我们看一下关键的 FlatList道具。 用于实现列表项可见性检测器的

原则上,你必须使用两个 FlatList道具:

  • viewabilityConfig

  • onViewableItemsChanged

和 viewabilityConfig,您确定“可见”对您的应用意味着什么。 我最常使用的配置是检测列表项至少 x在最短的时间内可见的百分比( y小姐)。

viewabilityConfig={{
  itemVisiblePercentThreshold: 75,
  minimumViewTime: 2000,
}}

使用此示例配置,列表项至少在它们可见时被视为可见 75至少在视口内的百分比 2秒。

看看 ViewabilityConfig部分 ViewabilityHelper了解其他有效设置及其类型定义。

你需要另一个 FlatList支柱, onViewableItemsChanged,根据我们的设置,只要列表项的可见性发生变化,就会调用它 viewabilityConfig类型定义。

// ...
  <FlatList
      data={listItems}
      renderItem={renderItem}
      onViewableItemsChanged={info => {
        // access info and do sth with viewable items
      })}
      viewabilityConfig={{
        itemVisiblePercentThreshold: 100,
        minimumViewTime: 2000,
      }}
      // ...
  />
  // ...

让我们仔细看看签名 onViewabilityItemsChanged, 定义在 VirtualizedList,由内部使用 FlatList. 这 info参数具有以下 流类型定义 :

// flow definition part of VirtualizedList.js
​
onViewableItemsChanged?: ?(info: {
    viewableItems: Array<ViewToken>,
    changed: Array<ViewToken>,
    ...
  }) => void

一个 ViewToken对象 保存有关列表项的可见性状态的信息。

// flow definition part of ViewabilityHelper.js
​
type ViewToken = {
  item: any,
  key: string,
  index: ?number,
  isViewable: boolean,
  section?: any,
  ...
};

我们如何使用它? 让我们看一下下一个 TypeScript 片段。

const onViewableItemsChanged = (info: { viewableItems: ViewToken[]; changed: ViewToken[] }): void => {      
      const visibleItems = info.changed.filter((entry) => entry.isViewable);
      visibleItems.forEach((visible) => console.log(visible.item));
  }  

在此示例中,我们只对更改后的内容感兴趣 ViewTokens 我们可以访问 info.changed. 在这里,我们要记录满足条件的列表项 viewabilityConfig. 正如你可以从 ViewToken定义,实际的列表项存储在 item.

和有什么区别 viewableItems和 changed?

后 onViewableItemsChanged被调用 viewabilityConfig, viewableItems存储符合我们标准的每个列表项 viewabilityConfig. 然而, changed只保留最后一个的 delta onViewableItemsChanged调用(即最后一次迭代)。


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →


如果您必须为列表项做不同的事情 100200 毫秒可见的百分比和那些 75500 毫秒可见的百分比,您可以利用 FlatList的 viewabilityConfigCallbackPairsprop ,它接受一个数组 ViewabilityConfigCallbackPair对象。

这是流类型定义 viewabilityConfigCallbackPairs,这是一部分 VirtualizedList.

// VirtualizedList.js
​
  viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>

流类型定义 ViewabilityConfigCallbackPair是其一部分 ViewabilityHelper.

// ViewabilityHelper.js
export type ViewabilityConfigCallbackPair = {
  viewabilityConfig: ViewabilityConfig,
  onViewableItemsChanged: (info: {
    viewableItems: Array<ViewToken>,
    changed: Array<ViewToken>,
    ...
  }) => void,
  ...
};

这是一个例子:

<FlatList
    data={listItems}
    renderItem={renderItem}
    viewabilityConfigCallbackPairs={[
      {
        viewabilityConfig: {
          minimumViewTime: 200,
          itemVisiblePercentThreshold: 100,
        },
        onViewableItemsChanged: onViewableItemsChanged100
      },
      {
        viewabilityConfig: {
          minimumViewTime: 500,
          itemVisiblePercentThreshold: 75
        },
        onViewableItemsChanged: onViewableItemsChanged75
      }
    ]}
    // ...
/>

如果你对列表项检测算法的实现细节感兴趣,可以 在这里阅读 。

FlatList的 API 蒸馏 ( onViewableItemsChanged, viewabilityConfig)

根据我们目前对相关 API 部分的了解,创建可见性传感器似乎很简单。 但是,传递给 onViewableItemsChangedprop 涉及到一些陷阱。

因此,我将制定不同的版本作为示例,直到我们得到最终的解决方案。 中间解决方案每个都有错误,因为 FlatListAPI 的实现以及 React 的工作方式。

我们将介绍两个不同的用例。 第一个将是一个简单的示例,每次列表元素出现在屏幕上时都会触发一个事件。 第二到第四个更复杂的示例相互构建,以演示如何在列表项处于视图中时触发事件,但仅触发一次。

我们正在讨论第二个用例,因为它需要管理状态,这就是丑陋的事情出现的地方 FlatList渲染错误。

以下是我们将尝试的解决方案,以及每个解决方案的问题:

  1. 每次 在屏幕上出现星球大战 角色(即列表元素)时进行跟踪

  2. 通过引入状态仅跟踪每个字符一次

  3. 尝试修复 useCallback导致过时关闭问题的解决方案(请参阅配套项目 分支过时关闭 )

  4. 通过使用状态更新器功能来访问以前的状态来修复问题(请参阅配套项目 分支 master )

临时解决方案 1:每次在屏幕上出现列表元素时进行跟踪

这第一次实现 onViewableItemsChanged跟踪每个可见项目,只要它出现在屏幕上。

const trackItem = (item: StarWarsCharacter) =>
    console.log("### track " + item.name);
const onViewableItemsChanged =
  (info: { changed: ViewToken[] }): void => {
    const visibleItems = info.changed.filter((entry) => entry.isViewable);
    visibleItems.forEach((visible) => {
      trackItem(visible.item);
    });
  };

我们使用 changed的对象 info参数传递给函数。 我们迭代这个 ViewToken数组以仅存储当前在屏幕上可见的列表项 visibleItems多变的。 然后,我们只需调用我们的简化版 trackItem函数通过将列表项的名称打印到控制台来模拟跟踪调用。

这应该有效,对吧? 抱歉不行。 我们得到一个渲染错误。

实施 FlatList 不允许 将函数传递给 onViewableItemsChanged在应用程序的生命周期中重新创建的道具。

为了解决这个问题,我们必须确保函数在最初创建后不会改变; 它需要在渲染周期内保持稳定。

我们应该怎么做? 我们可以使用 useCallback挂钩 。

const onViewableItemsChanged = useCallback(
    (info: { changed: ViewToken[] }): void => {
      const visibleItems = info.changed.filter((entry) => entry.isViewable);
      visibleItems.forEach((visible) => {
        trackItem(visible.item);
      });
    },
    []
  );

和 useCallback在适当的位置,我们的函数被记忆并且不会重新创建,因为没有可以更改的依赖项。 渲染问题消失,跟踪按预期工作。

临时解决方案 2:通过引入状态仅跟踪列表元素一次

接下来,我们只想跟踪每个 星球大战 角色一次。 因此,我们可以引入一个 React 状态, alreadySeen, 以跟踪用户已经看到的字符。

如您所见, useState挂钩存储一个 SeenItem大批。 传递给的函数 onViewableItemsChanged被包裹成一个 useCallback与一个依赖挂钩, alreadySeen. 这是因为我们使用这个状态变量来计算传递给的下一个状态 setAlreadySeen.

// TypeScript definitions
interface StarWarsCharacter {
  name: string;
  picture: string;
}
type SeenItem = {
  [key: string]: StarWarsCharacter;
};
interface ListViewProps {
  characters: StarWarsCharacter[];
}
export function ListView({
  characters,
}: ListViewProps) {
const [alreadySeen, setAlreadySeen] = useState<SeenItem[]>([]);
const onViewableItemsChanged = useCallback(
    (info: { changed: ViewToken[] }): void => {
      const visibleItems = info.changed.filter((entry) => entry.isViewable);
      // perform side effect
      visibleItems.forEach((visible) => {
        const exists = alreadySeen.find((prev) => visible.item.name in prev);
        if (!exists) trackItem(visible.item);
      });
      // calculate new state
      setAlreadySeen([
        ...alreadySeen,
        ...visibleItems.map((visible) => ({
          [visible.item.name]: visible.item,
        })),
      ]);
    },
    [alreadySeen]
  );
  // return JSX
}

再次,我们有一个问题。 因为依赖 alreadySeen,该函数被创建了不止一次,因此,我们再次对我们的渲染错误感到高兴。

我们 可以 通过使用 ESLint 省略依赖来摆脱渲染错误 ignore评论。

const onViewableItemsChanged = useCallback(
    (info: { changed: ViewToken[] }): void => {
        // ...
    },
    // bad fix
    // eslint-disable-next-line react-hooks/exhaustive-deps
    []
  );

但是,正如我在 关于 useEffectHook ,你永远不应该忽略你在 Hook 中使用的依赖项。 项是有原因的。 ESLint react-hooks 插件 告诉您缺少依赖

在我们的例子中,我们遇到了一个 陈旧的关闭 问题,并且我们的 alreadySeen状态变量不再更新。 该值保持初始值,即一个空数组。

但是如果我们按照 ESLint 插件告诉我们的去做,我们会再次遇到烦人的渲染错误。 我们正处于死胡同。

不知何故,由于限制,我们需要找到一个具有空依赖数组的解决方案 FlatList执行。

临时解决方案 3:尝试修复陈旧的闭包问题并返回一个空的依赖数组

我们如何回到一个空的依赖数组? 我们可以使用 state updater 函数,它可以接受带有先前状态的函数作为参数。 了解有关状态更新器功能的更多信息 您可以在我的LogRocket 文章 中 useState和 useRef.

  const onViewableItemsChanged = useCallback(
    (info: { changed: ViewToken[] }): void => {
      const visibleItems = info.changed.filter((entry) => entry.isViewable);
      setAlreadySeen((prevState: SeenItem[]) => {
        // perform side effect
        visibleItems.forEach((visible) => {
          const exists = prevState.find((prev) => visible.item.name in prev);
          if (!exists) trackItem(visible.item);
        });
        // calculate new state
        return [
          ...prevState,
          ...visibleItems.map((visible) => ({
            [visible.item.name]: visible.item,
          })),
        ];
      });
    },
    []
  );

它们之间的主要区别在于状态更新函数可以访问先前的状态,因此我们不必访问状态变量 alreadySeen直接地。 这样,我们就有了一个空的依赖数组,并且函数按预期工作(请参阅上一节中有关配套项目的截屏视频)。

下一节将进一步讨论渲染错误和陈旧的关闭问题。

莫扎兔影视网(mozhatu.com),免费观看Netflix影视资源,三端可用追剧神器!仔细看看问题 onViewableItemsChanged

React 的 memoization Hooks,比如 useEffect和 useCallback, 以这样一种方式构建,即每个组件上下文变量都被添加到依赖数组中。 这样做的原因是,只有当这些依赖项中的至少一个相对于上次运行发生更改时,才会调用 Hooks。

为了帮助您完成这个繁琐且容易出错的任务,React 团队构建 了一个 ESLint 插件 。 但是,即使您知道依赖项在运行时永远不会再次更改,作为一个优秀的开发人员,您也必须将其添加到依赖项数组中。 插件作者知道,例如,状态更新函数是稳定的,因此与其他(非纯)函数相比,插件不需要在数组中使用它。

如果您从自定义 Hook 返回此类状态更新器函数并在 React 组件中使用它,则插件错误地声称将其添加为依赖项。 在这种情况下,您必须添加 ESLint disable评论以使插件静音(或接受警告)。

但是,这是不好的做法。 尽管将它添加到依赖数组中应该不是问题,但是 Hook 的使用变得更加健壮,因为这个变量可能会随着时间而改变,例如,在重构之后。 如果它给您带来了问题,那么您的项目中很可能存在错误。

这个问题 onViewableItemsChanged道具来自 FlatList是您根本无法将任何依赖项添加到依赖项数组中。 这是一个限制 FlatList与 memoization Hooks 的概念相矛盾的实现。

因此,您必须找到摆脱依赖的解决方案。 您很可能希望使用我上面描述的方法,使用接受回调函数的状态更新器函数来访问先前的状态。

如果你想重构 onViewableItemsChanged功能——通过将其放入自定义 Hook 来降低复杂性或提高可测试性——不可能防止依赖。 目前没有办法告诉 React 自定义 Hook 返回一个稳定的依赖项,就像内置的结果一样 useRef挂钩或状态更新器功能。

在我目前的工作项目中,我选择添加一个 ESLint disable评论,因为我知道来自我的自定义 Hook 的依赖是稳定的。 此外,您可以忽略通过自定义 Hooks 获得的 纯函数 ,或者从另一个文件导入它们,因为它们本身不使用任何依赖项。

useCallback(
    () => {
      /* 
        Uses state updater functions from a custom hook or imported pure functions.
        The ESLint plugin does not know that the functions are stable / do not use any dependencies. It does not know that they can be omitted from the array list.
      /*      
    }, 
    // eslint-disable-next-line react-hooks/exhaustive-deps
    []
)

有 很多 关于 过去 将自定义 Hooks 的返回值标记为稳定的讨论,但目前还没有官方的解决方案。

概括

FlatList的 onViewableItemsChangedAPI 提供了检测屏幕上出现或消失的组件的能力。 一个 FlatList可以垂直和水平使用。 因此,这可以在大多数用例中使用,因为屏幕组件通常以列表形式组织。

这种方法的问题是,视口检测逻辑的实现首先是有限的、乏味的并且容易出错。 这是因为分配的函数在其初始创建后绝不能重新创建。 这意味着您根本不能依赖任何依赖项! 否则,您将收到渲染错误。

总而言之,以下是解决此问题的选项:

  • 包装您分配给的功能 onViewableItemsChanged成一个 useCallback用一个空的依赖数组挂钩

  • 如果您在函数内部使用一个或多个组件状态变量,则必须使用状态更新器函数,该函数接受访问前一个状态的回调,以便摆脱状态依赖

  • 如果你依赖于状态更新函数或来自另一个文件的纯函数(例如导入的或自定义的 Hook),你可以保留空依赖数组并忽略 ESLint 插件警告

  • 如果您依赖任何其他依赖项(例如,prop、上下文、自定义 Hook 中的状态变量等),您必须找到不使用它们的解决方案

如果要对可见性更改事件执行复杂任务,则必须以使用状态更新器函数更新状态变量的方式设计事件,以便为 useCallback钩。 然后,您可以响应状态变化,例如,使用 useEffectHook,执行依赖依赖的逻辑。 这种情况可能会变得复杂,但是通过我们在此处讨论的解决方法,您应该可以更轻松地找到和实施适合您的解决方案。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐