Intersection Observer が良さそうなので試してみた
スクロールでのイベント制御はしんどい。
jQuery にて scroll イベントで制御していた時代
$(function () {
$(window).on('scroll', function () {
// some event
});
});
個人的に辛いと思っていたこと
- レスポンシブ時で要素のスクロール位置がレイアウトによって変化する場合(SP だとレイアウトが縦長になるため)
- 感覚的に書けないつらさ
- 1回のみの動作させるときは flag 処理が必須(スクロールさせて false にして false 時は動作させないなど)
- スクロールイベントの発生は断続的なのでブラウザでの負荷がヤバい。
- setTimeout とか requestAnimationFrame とかで軽減させる方法は一応ある ⇛ (関連記事)
- lazyload などの遅延読み込みをネイティブで分かりやすく出来ないか。
Intersection Observer という技術について
こちらの記事で「Intersection Observer」という API を要素の遅延読み込みに利用していると知り、なんとなく調べてみることにした。
概要
Intersection(交点)Observer(監視)ということで要素自体が交差した瞬間にイベントを発火させる監視 API である。
これの重要な点は上述した スクロールによるイベント監視 というつらさ部分を解消してくれて、条件分岐に至ってもより分かりやすく実装できることが出来る。
サンプル
以下よりご確認ください
See the Pen Intersection Obeserver Test code by Oyama Michinoku (@yamanoku) on CodePen.
いわゆる「ある程度の表示にきたらヘッダの色を変える」アレです。スクロール量に制御されないので、レスポンシブなどで要素の高さが変わった場合でもこの調整は効きます。
サンプルコード(ES6)
(() => {
const clientHeight = document.documentElement.clientHeight;
const header = document.querySelector('.header');
let observer = new IntersectionObserver((changes) => {
for (const change of changes) {
let rect = change.target.getBoundingClientRect();
let h =
(0 < rect.top && rect.top < clientHeight) || // 対象の上端は表示領域に入っている
(0 < rect.bottom && rect.bottom < clientHeight) || // 対象の下端は表示領域に入っている
(0 > rect.top && rect.bottom > clientHeight); // 上端下端も表示されてないがその間が表示されている
if (h) {
header.classList.remove('scrolled');
} else {
header.classList.add('scrolled');
}
}
});
let target = document.querySelector('.main-middle'); // この要素のから外れたら
observer.observe(target);
})();
コード内でいうところの target 部分に何を指定するかでどの位置で変化するか制御することが出来ます。
let target = document.querySelector('.main-middle');
またunobserve()
を使用することでその対象の監視を終了させることができるので、1度限りの処理も対応できます。
画像遅延読み込み
Intersection Observer を使うと以下制御で実現出来る感じです
<img src="" data-src="/img/img_1.jpg" alt="img_1" class="lazy" />
<img src="" data-src="/img/img_2.jpg" alt="img_2" class="lazy" />
<img src="" data-src="/img/img_3.jpg" alt="img_3" class="lazy" />
<img src="" data-src="/img/img_4.jpg" alt="img_4" class="lazy" />
<img src="" data-src="/img/img_5.jpg" alt="img_5" class="lazy" />
...
let observer = new IntersectionObserver(onChange);
function onChange(changes) {
changes.forEach(change => {
// data-src属性の画像URLをsrcに挿入
change.target.src = change.target.dataset.src;
// 挿入し終わったら監視を終了
observer.unobserve(change.target);
}
}
// 配列化した画像一覧
const imgs = [ ...document.querySelectorAll('.lazy') ];
// それぞれに順次監視処理
imgs.forEach(img => observer.observe(img));
ちなみに上記の場合だと、画像が小さかったりして読み込みがない表示があるなどラグが起こりうるので、オプションのrootMargin
で要素の上下左右に空きを指定しておけば、その要素がくる前の段階でイベントを発火させることも出来ます。
margin の指定の順序は CSS のそれと同じです。
{
rootMargin: '10px 0px';
} // 上下10px、左右0pxの空き
{
rootMargin: '-10px 10px -5px';
} // 左記のようにマイナス値も設定できる。
ちなみに、0 の時は必ず単位を省略せずに書かないとエラー判定になります。
Callback 例
以下参考までに。
let observer = new IntersectionObserver(changes => {
for (const change of changes) {
console.log(change.time); // 変化が生じた時点でのタイムスタンプ
console.log(change.rootBounds); // root の getBoundingClientRect()
console.log(change.boundingClientRect); // target の getBoundingClientRect()
console.log(change.intersectionRect); // 交差領域の getBoundingClientRect()
console.log(change.intersectionRatio); // 交差している領域の割合
console.log(change.target); // target にされている要素
}
}, {});
const target = ... // ターゲットとする要素
observer.observe(target); // 特定のtargetに対して,監視開始
observer.unobserve(target); // 特定のtargetに対して,監視をやめる
observer.disconnect(); // すべての target に対し,監視をやめる
Polyfill(2017/07 現在)
現状 Intersection Observer は IE Edge 最新、Firefox 最新、Chrome 最新、Android ブラウザ最新のみの対応につき Polyfill が必要となります。(Can I Use)
<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>
追記(2019/02/05)
Safari12.1 でも Intersection Observer API がサポートされるようになりました。
所感
監視する要素の設定があるので感覚的かと言われると怪しいですが、上記にも書いた辛くて煩わしいスクロールイベントからは離れられそうでそこは安心しました。また、スクロールイベントではなくなるので処理に負荷がかかっていた場合に原因を特定する際にも役立ちそうです。
現状は縦イベントのみで確認していますが、横スクロールのコンテンツの場合でもこれは有効な API っぽいなと感じたので後で試してみます。