Intensive reading of react intersection observer source code

1 Introduction

IntersectionObserver It's easy to judge whether the element is visible or not. Before Intensive reading "rendering on demand with React" The method of native API is introduced in. This time I just saw its React package version react-intersection-observer , let's take a look at React packaging ideas.

2 Introduction

react-intersection-observer It provides the Hook useInView to judge whether the element is in the visible area. The API is as follows:

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>
  );
};

Because judging whether the element is visible is based on DOM, the ref callback function must be passed to the DOM element representing the element outline. In the above example, we passed ref to the outermost DIV.

useInView also supports the following parameters:

  • root: check whether the window element based on is visible. The default is the entire browser viewport.
  • rootMargin: root margin. Fixed pixel judgment can be advanced or delayed during detection.
  • Threshold: the threshold value of whether or not to be visible. The range is 0-1. 0 means that any visible is visible, and 1 means that all visible is visible.
  • Trigger once: whether to trigger only once.

3 intensive reading

First, we start with the entry function useInView. This is a Hook. Use ref to store the last DOM instance, and state to store the boolean value of whether the inView element is visible

export function useInView(
  options: IntersectionOptions = {},
): InViewHookResponse {
  const ref = React.useRef<Element>()
  const [state, setState] = React.useState<State>(initialState)

  // The middle part

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

When the component ref is assigned, setRef will be called. The callback node is a new DOM node, so unobserve first( ref.current )Cancel the listening of the old node, then observe(node) the new node, and finally ref.current =Node update old node:

// Middle part 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]
);

The other is that when ref does not exist, the inView status will be cleared. After all, when there is no listening object, the inView value is only reasonable if it is reset to default false:

// Middle section 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);
  }
});

This is the logic of the entry file. We can see that there are two important functions, observe and unobserve, which are implemented in intersection.ts In this file, there are three core functions: observe, unobserve, onChange.

  • observe: monitor whether the element is in the visible area.
  • unobserve: cancel listening.
  • onChange: a callback that handles the observe change.

First, look at the observe. The monitor under the same root will be merged. Therefore, the observer ID needs to be generated as the unique ID, which is jointly determined by getRootId, rootMargin and threshold.

For the same root listening, get the observer instance instance created by new IntersectionObserver(), and call observerInstance.observe Monitor. Two maps - observer are stored here_ Map and INSTANCE_MAP. The former ensures the uniqueness of IntersectionObserver instance under the same root. The latter stores information such as component inView and callback. It is used in onChange function

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;
}

For onChange function, because multi-element monitoring is adopted, it is necessary to traverse the changes array, judge whether intersectionRatio exceeds the threshold value, and judge it as inView state. Use INSTANCE_MAP takes the corresponding instance, modifies its inView status and executes callback.

This callback corresponds to the second parameter callback of observe in useInView Hook:

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);
    }
  });
}

Finally, it is the implementation of unsubserve to cancel listening. When usenview setref fills in a new Node, it will call unsubserve to cancel listening to the old Node.

First use INSTANCE_MAP find instance, call observer.unobserve(element) destroy the monitor. Finally destroy unnecessary instances_ Map and ROOT_IDS storage.

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);
  }
}

From its implementation point of view, in order to ensure the correct recognition of the existence of child elements, it is necessary to ensure that ref can be continuously passed to the outermost DOM of the component. If there is a transfer fracture, it will be determined that the current component is not in the view, for example:

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

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

const Child = ({ loading, ref }) => {
  if (loading) {
    // This step will be judged as inView: false
    return <Spin />;
  }

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

If your code makes a decision to prevent rendering based on inView, the component cannot change its state after entering loading. In order to avoid this situation, either do not let the ref pass off, or determine that inView is true when no ref object is obtained.

4 Summary

This paper analyzes so many React class libraries, and its core ideas are as follows:

  1. Transform native API s into framework specific ones, such as Hooks and ref of React series.
  2. Deal with the boundary conditions caused by the life cycle, such as unobserve and reobserve when the dom is updated.

Yes react-intersection-observer After the source code, do you think there is any place to optimize? Welcome to the discussion.

Discussion address: React intersection observer source code, issue, DT Fe / weekly

If you want to participate in the discussion, please click here , new theme every week, released on weekend or Monday. Front end intensive reading - help you to select reliable content.

Focus on front-end intensive reading WeChat official account

Copyright notice: Free Reprint - non commercial - non derivative - keep signature( Creative sharing 3.0 License)

This article uses mdnice Typesetting

Tags: Javascript React Firefox

Posted on Mon, 22 Jun 2020 01:00:23 -0400 by Horizon88