IntersectionObserve初試

深紅發表於2019-02-28

IntersectionObserve這個API,可能知道的人並不多(我也是最近才知道...),這個API可以很方便的監聽元素是否進入了可視區域。

<style>
* {
  margin: 0;
  padding: 0;
}

.test {
  width: 200px;
  height: 1000px;
  background: orange;
}

.box {
  width: 150px;
  height: 150px;
  margin: 50px;
  background: red;
}
</style>
<div class="test">test</div>
<div class="box">box</div>
複製程式碼

上圖程式碼中,.box元素目前並不在可視區域(viewport)內,如何監聽它進入可視區域內?

傳統做法是:監聽scroll方法,實時計算.box距離viewport的top值:

const box = document.querySelector('.box');
const viewHeight = document.documentElement.clientHeight;

window.addEventListener('scroll', () => {
  const top = box.getBoundingClientRect().top;
  
  if (top < viewHeight) {
    console.log('進入可視區域');
  }
});
複製程式碼

然而scroll方法會頻繁觸發,因此我們還需要手動節流。

使用IntersectionObserve就非常方便了:

const box = document.querySelector('.box');
const intersectionObserver = new IntersectionObserver((entries) => {
  entries.forEach((item) => {
    if (item.isIntersecting) {
      console.log('進入可視區域');
    }
  })
});
intersectionObserver.observe(box);
複製程式碼

IntersectionObserver API是非同步的,不隨著目標元素的滾動同步觸發,所以不會有效能問題。

IntersectionObserver建構函式

const io = new IntersectionObserver((entries) => {
  console.log(entries);
});

io.observe(dom);
複製程式碼

呼叫IntersectionObserver時,需要給它傳一個回撥函式。當監聽的dom元素進入可視區域或者從可視區域離開時,回撥函式就會被呼叫。

注意: 第一次呼叫new IntersectionObserver時,callback函式會先呼叫一次,即使元素未進入可視區域。

建構函式的返回值是一個觀察器例項。例項的observe方法可以指定觀察哪個 DOM 節點。

// 開始觀察
io.observe(document.getElementById('example'));

// 停止觀察
io.unobserve(element);

// 關閉觀察器
io.disconnect();
複製程式碼

IntersectionObserverEntry物件

callback函式被呼叫時,會傳給它一個陣列,這個陣列裡的每個物件就是當前進入可視區域或者離開可視區域的物件(IntersectionObserverEntry物件)

這個物件有很多屬性,其中最常用的屬性是:

  • target: 被觀察的目標元素,是一個 DOM 節點物件
  • isIntersecting: 是否進入可視區域
  • intersectionRatio: 相交區域和目標元素的比例值,進入可視區域,值大於0,否則等於0

options

呼叫IntersectionObserver時,除了傳一個回撥函式,還可以傳入一個option物件,配置如下屬性:

  • threshold: 決定了什麼時候觸發回撥函式。它是一個陣列,每個成員都是一個門檻值,預設為[0],即交叉比例(intersectionRatio)達到0時觸發回撥函式。使用者可以自定義這個陣列。比如,[0, 0.25, 0.5, 0.75, 1]就表示當目標元素 0%、25%、50%、75%、100% 可見時,會觸發回撥函式。

  • root: 用於觀察的根元素,預設是瀏覽器的視口,也可以指定具體元素,指定元素的時候用於觀察的元素必須是指定元素的子元素

  • rootMargin: 用來擴大或者縮小視窗的的大小,使用css的定義方法,10px 10px 30px 20px表示top、right、bottom 和 left的值

const io = new IntersectionObserver((entries) => {
  console.log(entries);
}, {
  threshold: [0, 0.5],
  root: document.querySelector('.container'),
  rootMargin: "10px 10px 30px 20px",
});
複製程式碼

懶載入

圖片懶載入的原理就是:給img標籤一個自定義屬性,用來記錄真正的圖片地址。預設img標籤只載入一個佔位符。當圖片進入可視區域時,再把img的src屬性更換成真正的圖片地址。

<div>
  <img src="/empty.jpg" data-src="/img/1.jpg" />
  <img src="/empty.jpg" data-src="/img/2.jpg" />
</div>
複製程式碼
const intersectionObserver = new IntersectionObserver((entries) => {
    entries.forEach((item) => {
        if (item.isIntersecting) {
            item.target.src = item.target.dataset.src;
            // 替換成功後,停止觀察該dom
            intersectionObserver.unobserve(item.target);
        }
    })
  }, {
      rootMargin: "150px 0px" // 提前150px進入可視區域時就載入圖片,提高使用者體驗
    });

const imgs = document.querySelectorAll('[data-src]');
imgs.forEach((item) => {
    intersectionObserver.observe(item)
});
複製程式碼

打點上報

前端頁面經常有上報資料的需求,比如統計頁面上的某些元素是否被使用者檢視,點選。這時,我們就可以封裝一個Log元件,用於當元素進入可視區域時,就上報資料。 以React為例,我們希望:被Log元件包裹的元素,進入可視區域後,就向後臺傳送{ appid: 1234, type: 'news'}資料

<Log
  className="container"
  log={{ appid: 1234, type: 'news'}}
  style={{ marginTop: '1400px' }}
  onClick={() => console.log('log')}
>
  <div className="news" onClick={() => console.log('news')}>
    <p>其他內容</p>
  </div>
</Log>
複製程式碼
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Log extends Component {
  static propTypes = {
    log: PropTypes.object
  };

  constructor(props) {
    super(props);
    this.io = null;
    this.ref = React.createRef();
  }

  componentDidMount() {
    this.io = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          console.log('進入可視區域,將要傳送log資料', this.props.log);
          sendLog(this.props.log);
          this.io.disconnect();
        }
      }
    );

    this.io.observe(this.ref.current);
  }

  componentWillUnmount() {
    this.io && this.io.disconnect();
  }

  render() {
    // 把log屬性去掉,否則log屬性也會渲染在div上 
    // <div log="[object Object]"></div>
    
    const { log, ...props } = this.props;
    return (
      <div ref={this.ref} {...props}>
        {this.props.children}
      </div>
    )
  }
}

export default Log
複製程式碼

相容性

safari並不支援該API,因此為了相容主流瀏覽器,我們需要引入polyfill intersection-observer

相關文章