Intersection Observer API 実装方法

この記事は、社内教育向けの説明資料として書いています。
文中のIntersection Observer APIに関する引用は、以下のサイト・記事から抜粋したり、参考にしています。

- [mozilla：Intersection Observer API](https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API)
- [ics.media：JSでのスクロール連動エフェクトにはIntersection Observerが便利](https://ics.media/entry/190902/)

以下の記事も参考になります。

- [使ってみよう！Intersection Observer！](https://qiita.com/ryo_hisano/items/42f5980720bc832e6e09)

この記事では、よく使いそうな利用例を1つですが用意しました。

- スクロールに応じて要素をフワッと表示する

## 概要
mozillaをみていただくのがよいと思います。
>Intersection Observer API (交差監視 API) は、ターゲットとなる要素が、祖先要素もしくは文書の最上位のビューポートと交差する変更を非同期的に監視する方法を提供します。

また、ics.mediaも非常にわかりやすく解説してあります。

>Intersection Observer APIは直訳すると交差監視APIという名前です。これは要素と要素の交差を検知するAPIになります。一見するとスクロールとの関連性がないようにも思えますが、「スクロールして特定位置にきたら」 というのは 「要素と交差したら」 ということに置き換えられるので、このAPIを活用できます。

>従来のscrollを使った手法とは違い、ビューポートサイズの変更で交差する位置が変わっても、自動的に反応します。また、スクロールに関するイベントではないので、スクロールのたびに呼ばれることもなくパフォーマンス面でも有利です。

割と最近まで、スクロールのアップ・ダウンをしたときに、画面の指定位置に要素が入ったことを取得するは、[Waypoints](http://imakewebthings.com/waypoints/)などのプラグインを使うことが多かったですが、その代替として使っています。

また、Waypointsは画面内の何%の位置にきたら発火、みたいな指定の仕方をするのですが（他の指定方法もあったらすんません）Intersection Observer APIはそれもできるし、要素自体がどの程度root（option設定のroot。後述）に入ったら、という設定もできるため使い勝手がよいので気に入っています。

### メリット

- プラグインを読み込まなくてもよい
- パフォーマンスはこちらのほうがよい（計測したわけではないです）

後述しますが、IE対応しようとするとpolyfillは読み込まなくてはいけません。

パフォーマンスに関しては、スクロールイベントなどではなく、閾値（後述のthreshold）の前後でしかコールバックが呼ばれないため、動作が重くなることは少なくなります。

以下はmozillaからの抜粋です。

>これまで、要素間の交差を検出する実装をするには、 Element.getBoundingClientRect() のようなメソッドを呼び出すイベントハンドラーやループがあり、影響を受ける要素に対する情報を都度計算し集めることで構成されていました。このようなコードがメインスレッドで実行されると、いずれかはパフォーマンスの問題を引き起こす可能性があります。試しにサイトにテストとして読み込めば分かりますが、事態は完全に酷くなりえます。

>Intersection Observer API を使用することで、監視したい要素が別の要素 (もしくはviewport) に入ってきたり出ていったりする時、まだ両要素が交差する量がある一定の量を満たす時、実行されるコールバック関数を登録するが出来ます。

## 使い方
[mozillaの記事](https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API#Creating_an_intersection_observer)と同じになってしまいますが、一応記載しておきます。

```javascript:optionsオブジェクトとIntersectionObserverコンストラクタの作成
let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);
```
### IntersectionObserverオブジェクト
第一引数：実行されるコールバック関数です
第二引数：コールバックが呼び出される状況を制御します

### optionsオブジェクト
`root`
ターゲットが見えるかどうかを確認するためのビューポートとして使用される要素です。指定されなかった場合、もしくは null の場合はデフォルトでブラウザーのビューポートが使用されます。

`rootMargin`
root の周りのマージンです。CSS margin プロパティに似た値を持つことができます。例えば、"10px 20px 30px 40px" (top, right, bottom, left) のようなものです。この値はパーセント値にすることができます。 たとえ値が0であっても "0px 15px 10px"のように**単位をつける必要がある**、ということです。

交差を検知するルート要素からの距離とも言えます。上記のように10pxを指定した場合、交差判定をルート要素の周囲10px分拡大します。見える少し前にイベントを発火させたい場合などに有効です。また負の値も指定できるので、ある程度見えてからイベントを発火させる、といった使い方もできます。

`threshold`
閾値になります。0〜1の間を数値もしくは配列形式で入力します（デフォルトは0）。rootMarginがルート要素にもとづくオプションに対して、thresholdは交差する要素にもとづくオプションです。

### 
```javascript:監視される要素をターゲットにする
const target = document.querySelector('#listItem');
observer.observe(target);
```
監視させたいターゲット要素をobserveメソッドの引数に指定することで監視ができます。

```javascript:コールバック関数の作成
let callback = (entries, observer) => {
  entries.forEach(entry => {
　　// entry.target等
  });
};
```
上記では監視対象が1要素ですが、複数の要素を監視対象にできるため、mozzillaでも配列で記載されています。

## IE対応
2020.3.17時点では、IEのみ非対応のためポリフィルを読み込みます。
https://github.com/w3c/IntersectionObserver/tree/master/polyfill
<img width="1139" alt="スクリーンショット 2020-03-17 10.30.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298406/badd7ec7-34d0-048c-43d8-dd7864ae3e01.png">

また、forEachでエラーを出さないようにJavaScriptを書く必要があります。
それは本題とそれるので詳しく書きませんが、以下の利用例はその一例です。

## 利用例：スクロールに応じて要素をフワッと表示する

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="shuCream" data-slug-hash="gOpzENL" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="IntersectionObserver-sample01">
  <span>See the Pen <a href="https://codepen.io/shuCream/pen/gOpzENL">
  IntersectionObserver-sample01</a> by shuCream (<a href="https://codepen.io/shuCream">@shuCream</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

### ポイント
- `rootMargin: '-50% 0px'`なので、viewportの中心にきたら発火します
- アニメーションはGSAPを使用しています。
- 一度発火させたものは発火させないようにしたいので、data属性の設定・取得で`return;`をさせています。
- IEでforEachを使うためには、Array.from()メソッドを使って、配列風オブジェクトや反復可能オブジェクトから、Arrayインスタンスを生成します。なお、IE11で動作保証するにはポリフィルが必要になります。

参考： [Array.from()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from)

```javascript
const options = {
   root: null,
   rootMargin: '-50% 0px', // viewportの中心にきたら発火
   threshold: 0
 };

 const active = entries => {
  Array.from(entries).forEach(entry => {
     if (entry.isIntersecting) {
       // 一度アニメしたら発火させない
       if (entry.target.getAttribute('data-animationend')) return;
       entry.target.setAttribute('data-animationend', true);

       gsap.to(entry.target, 1, { opacity: 1, y: 0, ease: 'power1.out' });
     }
   })
 };
```

冒頭で紹介したics.mediaさんの記事でも、**Intersection Observer APIを使って表示中コンテンツに合わせて目次を色を変える実装**という見出し部分で実装方法をご紹介してくれてますので、参考になると思います。

今後の対応案件でこのような実装をするときは、スクロールイベントやWaypointsでなく、Intersection Observer APIのほうがメインになってくると思うので、新人のコーダーさんにも理解しておいてもらいたくて記事にしました。

冒頭紹介したmozillaやics.mediaの記事は、しっかり読んでおいてもらいたいです。