Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >156. 精读《react-intersection-observer 源码》

156. 精读《react-intersection-observer 源码》

作者头像
黄子毅
发布于 2022-03-14 09:41:02
发布于 2022-03-14 09:41:02
1.1K00
代码可运行
举报
文章被收录于专栏:前端精读评论前端精读评论
运行总次数:0
代码可运行

1 引言

IntersectionObserver 可以轻松判断元素是否可见,在之前的 精读《用 React 做按需渲染》 中介绍了原生 API 的方法,这次刚好看到其 React 封装版本 react-intersection-observer,让我们看一看 React 封装思路。

2 简介

react-intersection-observer 提供了 Hook useInView 判断元素是否在可视区域内,API 如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import React from "react";
import { useInView } from "react-intersection-observer";

const Component = () => {
  const [ref, inView] = useInView();

  return (
    <div ref={ref}>
      <h2>{`Header inside viewport ${inView}.`}</h2>
    </div>
  );
};

由于判断元素是否可见是基于 dom 的,所以必须将 ref 回调函数传递给 代表元素轮廓的 DOM 元素,上面的例子中,我们将 ref 传递给了最外层 DIV。

useInView 还支持下列参数:

  • root:检测是否可见基于的视窗元素,默认是整个浏览器 viewport。
  • rootMargin:root 边距,可以在检测时提前或者推迟固定像素判断。
  • threshold:是否可见的阈值,范围 0 ~ 1,0 表示任意可见即为可见,1 表示完全可见即为可见。
  • triggerOnce:是否仅触发一次。

3 精读

首先从入口函数 useInView 开始解读,这是一个 Hook,利用 ref 存储上一次 DOM 实例,state 则存储 inView 元素是否可见的 boolean 值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function useInView(
  options: IntersectionOptions = {},
): InViewHookResponse {
  const ref = React.useRef<Element>()
  const [state, setState] = React.useState<State>(initialState)

  // 中间部分..

  return [setRef, state.inView, state.entry]
}

当组件 ref 被赋值时会调用 setRef,回调 node 是新的 DOM 节点,因此先 unobserve(ref.current) 取消旧节点的监听,再 observe(node) 对新节点进行监听,最后 ref.current = node 更新旧节点:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 中间部分 1
const setRef = React.useCallback(
  (node) => {
    if (ref.current) {
      unobserve(ref.current);
    }

    if (node) {
      observe(
        node,
        (inView, intersection) => {
          setState({ inView, entry: intersection });

          if (inView && options.triggerOnce) {
            // If it should only trigger once, unobserve the element after it's inView
            unobserve(node);
          }
        },
        options
      );
    }

    // Store a reference to the node, so we can unobserve it later
    ref.current = node;
  },
  [options.threshold, options.root, options.rootMargin, options.triggerOnce]
);

另一段是,当 ref 不存在时会清空 inView 状态,毕竟当不存在监听对象时,inView 值只有重设为默认 false 才合理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 中间部分 2
useEffect(() => {
  if (!ref.current && state !== initialState && !options.triggerOnce) {
    // If we don't have a ref, then reset the state (unless the hook is set to only `triggerOnce`)
    // This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView
    setState(initialState);
  }
});

这就是入口文件的逻辑,我们可以看到还有两个重要的函数 observeunobserve,这两个函数的实现在 intersection.ts 文件中,这个文件有三个核心函数:observeunobserveonChange

  • observe:监听 element 是否在可视区域。
  • unobserve:取消监听。
  • onChange:处理 observe 变化的回调。

先看 observe,对于同一个 root 下的监听会做合并操作,因此需要生成 observerId 作为唯一标识,这个标识由 getRootIdrootMarginthreshold 共同决定。

对于同一个 root 的监听下,拿到 new IntersectionObserver() 创建的 observerInstance 实例,调用 observerInstance.observe 进行监听。这里存储了两个 Map - OBSERVER_MAPINSTANCE_MAP,前者是保证同一 root 下 IntersectionObserver 实例唯一,后者存储了组件 inView 以及回调等信息,在 onChange 函数使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function observe(
  element: Element,
  callback: ObserverInstanceCallback,
  options: IntersectionObserverInit = {}
) {
  // IntersectionObserver needs a threshold to trigger, so set it to 0 if it's not defined.
  // Modify the options object, since it's used in the onChange handler.
  if (!options.threshold) options.threshold = 0;
  const { root, rootMargin, threshold } = options;
  // Validate that the element is not being used in another <Observer />
  invariant(
    !INSTANCE_MAP.has(element),
    "react-intersection-observer: Trying to observe %s, but it's already being observed by another instance.\nMake sure the `ref` is only used by a single <Observer /> instance.\n\n%s"
  );
  /* istanbul ignore if */
  if (!element) return;
  // Create a unique ID for this observer instance, based on the root, root margin and threshold.
  // An observer with the same options can be reused, so lets use this fact
  let observerId: string =
    getRootId(root) +
    (rootMargin
      ? `${threshold.toString()}_${rootMargin}`
      : threshold.toString());

  let observerInstance = OBSERVER_MAP.get(observerId);
  if (!observerInstance) {
    observerInstance = new IntersectionObserver(onChange, options);
    /* istanbul ignore else  */
    if (observerId) OBSERVER_MAP.set(observerId, observerInstance);
  }

  const instance: ObserverInstance = {
    callback,
    element,
    inView: false,
    observerId,
    observer: observerInstance,
    // Make sure we have the thresholds value. It's undefined on a browser like Chrome 51.
    thresholds:
      observerInstance.thresholds ||
      (Array.isArray(threshold) ? threshold : [threshold]),
  };

  INSTANCE_MAP.set(element, instance);
  observerInstance.observe(element);

  return instance;
}

对于 onChange 函数,因为采用了多元素监听,所以需要遍历 changes 数组,并判断 intersectionRatio 超过阈值判定为 inView 状态,通过 INSTANCE_MAP 拿到对应实例,修改其 inView 状态并执行 callback

这个 callback 就对应了 useInView Hook 中 observe 的第二个参数回调:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function onChange(changes: IntersectionObserverEntry[]) {
  changes.forEach((intersection) => {
    const { isIntersecting, intersectionRatio, target } = intersection;
    const instance = INSTANCE_MAP.get(target);

    // Firefox can report a negative intersectionRatio when scrolling.
    /* istanbul ignore else */
    if (instance && intersectionRatio >= 0) {
      // If threshold is an array, check if any of them intersects. This just triggers the onChange event multiple times.
      let inView = instance.thresholds.some((threshold) => {
        return instance.inView
          ? intersectionRatio > threshold
          : intersectionRatio >= threshold;
      });

      if (isIntersecting !== undefined) {
        // If isIntersecting is defined, ensure that the element is actually intersecting.
        // Otherwise it reports a threshold of 0
        inView = inView && isIntersecting;
      }

      instance.inView = inView;
      instance.callback(inView, intersection);
    }
  });
}

最后是 unobserve 取消监听的实现,在 useInView setRef 灌入新 Node 节点时,会调用 unobserve 对旧节点取消监听。

首先利用 INSTANCE_MAP 找到实例,调用 observer.unobserve(element) 销毁监听。最后销毁不必要的 INSTANCE_MAPROOT_IDS 存储。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function unobserve(element: Element | null) {
  if (!element) return;
  const instance = INSTANCE_MAP.get(element);

  if (instance) {
    const { observerId, observer } = instance;
    const { root } = observer;

    observer.unobserve(element);

    // Check if we are still observing any elements with the same threshold.
    let itemsLeft = false;
    // Check if we still have observers configured with the same root.
    let rootObserved = false;
    /* istanbul ignore else  */
    if (observerId) {
      INSTANCE_MAP.forEach((item, key) => {
        if (key !== element) {
          if (item.observerId === observerId) {
            itemsLeft = true;
            rootObserved = true;
          }
          if (item.observer.root === root) {
            rootObserved = true;
          }
        }
      });
    }
    if (!rootObserved && root) ROOT_IDS.delete(root);
    if (observer && !itemsLeft) {
      // No more elements to observe for threshold, disconnect observer
      observer.disconnect();
    }

    // Remove reference to element
    INSTANCE_MAP.delete(element);
  }
}

从其实现角度来看,为了保证正确识别到子元素存在,一定要保证 ref 能持续传递给组件最外层 DOM,如果出现传递断裂,就会判定当前组件不在视图内,比如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const Component = () => {
  const [ref, inView] = useInView();

  return <Child ref={ref} />;
};

const Child = ({ loading, ref }) => {
  if (loading) {
    // 这一步会判定为 inView:false
    return <Spin />;
  }

  return <div ref={ref}>Child</div>;
};

如果你的代码基于 inView 做了阻止渲染的判定,那么这个组件进入 loading 后就无法改变状态了。为了避免这种情况,要么不要让 ref 的传递断掉,要么当没有拿到 ref 对象时判定 inView 为 true。

4 总结

分析了这么多 React- 类的库,其核心思想有两个:

  1. 将原生 API 转换为框架特有 API,比如 React 系列的 Hooks 与 ref。
  2. 处理生命周期导致的边界情况,比如 dom 被更新时先 unobserve 再重新 observe

看过 react-intersection-observer 的源码后,你觉得还有可优化的地方吗?欢迎讨论。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端精读评论 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【今天你更博学了么】一个神奇的交叉观察 API Intersection Observer
上面这一段话来自 MDN ,中心思想就是现在判断一个元素是否能被用户看见的使用场景越来越多,监听 scroll 事件以及通过 Element.getBoundingClientRect() 获取节点位置的方式,又麻烦又不好用,那么怎么办呢。于是就有了今天的内容 Intersection Observer API。
一尾流莺
2022/12/10
1.3K0
【今天你更博学了么】一个神奇的交叉观察 API Intersection Observer
大白话详解Intersection Observer API
昨天我写了Vue2 中自定义图片懒加载指令这篇博客,文章数据很好,阅读量可以上千,对于我这个刚写博客一周的新博主来说,是何等的荣幸。
用户6256742
2024/06/22
5280
大白话详解Intersection Observer API
使用Intersection Observer API实现视频队列自动播放
笔者利用空余时间研究了一下javascript的Intersection Observer API,发现其有很大的应用场景,比如图片或者内容的懒加载,视差动画等。笔者也在之前的文章中详细介绍了3种Observer(观察者)的用法,包括位置监听,dom变化监听以及窗口变化监听,它们有非常多的应用场景,所以很有必要研究明白, 感兴趣的可以读完本片文章之后学习一下(几个非常有意思的javascript知识点总结).
徐小夕
2020/05/29
1.5K0
Interection Observer如何观察变化
有很多精彩的文章探讨了如何使用Intersection Observer API,包括Phil Hawksworth,Preethi和Mateusz Rybczonek等。我这篇文章将讲一些不一样的东西。我在今年早些时候有幸向达拉斯VueJS聚会介绍了VueJS过渡组件,我在CSS-Tricks的第一篇文章就是以此为基础的。在演讲的问答环节中,有人问我基于滚动事件触发过渡怎么样 - 我说当然可以,但是一些听众建议我了解一下Intersection Observer。
WecTeam
2019/12/16
2.8K0
Interection Observer如何观察变化
90行代码,15个元素实现无限滚动
无限下拉加载技术使用户在大量成块的内容面前一直滚动查看。这种方法是在你向下滚动的时候不断加载新内容。
前端劝退师
2019/09/19
3.2K0
90行代码,15个元素实现无限滚动
[译] 精通 Intersection Observer API
原文:https://www.hweaver.com/intersection-observer-single-page-navigation/
江米小枣
2020/06/15
1.4K0
「实用推荐」如何优雅的判断元素是否进入当前视区
首先:编写一个 util 函数 isVisible,它将仅接收一个参数,即 element。
皮小蛋
2020/12/22
1.5K0
网页元素相交监测:Intersection Observer API
对于不支持某些新特性的浏览器,一般都可以通过profill进行兼容;https://polyfill.io/v3/
房东的狗丶
2023/02/17
9880
现代浏览器观察者 Observer API 指南
想要计算Web页面的元素的位置,非常依赖于DOM状态的显式查询。但这些查询是同步的,会导致昂贵的样式计算开销(重绘和回流),且不停轮询会导致大量的性能浪费。
前端劝退师
2019/10/28
4.5K0
现代浏览器观察者 Observer API 指南
图片懒加载的几种实现方式
Lazyload 可以加快网页访问速度,减少请求,实现思路就是判断图片元素是否可见来决定是否加载图片。当图片位于浏览器视口 (viewport) 中时,动态设置 <img> 标签的 src 属性,浏览器会根据 src 属性发送请求加载图片。
李振
2021/11/26
2.8K0
图片懒加载的几种实现方式
通过自定义 Vue 指令实现前端曝光埋点
互联网发展至今,数据的重要性已经不言而喻,尤其是在电商公司,数据的统计分析尤为重要,通过数据分析可以提升用户的购买体验,方便运营和产品调整销售策略等等。埋点就是网站分析的一种常用的数据采集方法。
政采云前端团队
2021/04/08
1.7K0
通过自定义 Vue 指令实现前端曝光埋点
? 这是第 94 篇不掺水的原创,想要了解更多,请戳上方蓝色字体:政采云前端团队 关注我们吧~ 本文首发于政采云前端团队博客:通过自定义 Vue 指令实现前端曝光埋点 https://www.zoo
用户3806669
2021/07/06
1.7K0
通过自定义 Vue 指令实现前端曝光埋点
IntersectionObserver对象
IntersectionObserver对象 IntersectionObserver对象,从属于Intersection Observer API,提供了一种异步观察目标元素与其祖先元素或顶级文档视
WindRunnerMax
2021/11/24
7560
掌握Intersection Observer API,轻松实现实现图片懒加载、元素滚动动画、无限滚动加载等功能
Intersection Observer API 是浏览器提供的一个强大接口,用来异步观察一个元素是否进入(或者离开)另一个元素或视口(viewport)的可视范围。
watermelo37
2025/04/22
1900
掌握Intersection Observer API,轻松实现实现图片懒加载、元素滚动动画、无限滚动加载等功能
20行代码,封装一个 React 图片懒加载组件
图片懒加载是我们在做性能优化时非常重要的手段。我们常常需要图片在进入页面可视区域时,才让加载图片的行为发生。
用户6901603
2024/01/04
4170
20行代码,封装一个 React 图片懒加载组件
一次DOM曝光封装历程
随着最近曝光埋点的需求越来越频繁,就想把之前写好的曝光逻辑抽出来封装了一下作为公用。
政采云前端团队
2023/09/01
2300
一次DOM曝光封装历程
AI简历开发-自定义指令实现图片懒加载
今天,为我的AI简历项目(https://github.com/weidong-repo/AIResume)的图片加载做一个小的优化。
吃猫的鱼Code
2025/03/06
860
React技巧之检查元素是否可见
原文链接:https://bobbyhadz.com/blog/react-check-if-element-in-viewport[1]
chuckQu
2022/08/19
1.3K0
IntersectionObserver交叉观察器
前段时间内部系统业务需要,用 IntersectionObserver实现了table中的上拉数据加载,如果有类似需求,希望本文能带给你一点思考和帮助
Maic
2022/07/28
9620
IntersectionObserver交叉观察器
IntersectionObserver API 使用教程
网页开发时,常常需要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它。 上图的绿色方块不断滚动,顶部会提示它的可见性。 传统的实现方法是,监听到scroll事件后,调用目标元素(
ruanyf
2018/04/12
2.1K0
IntersectionObserver API 使用教程
相关推荐
【今天你更博学了么】一个神奇的交叉观察 API Intersection Observer
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验