# IntersectionObserver
網頁開發時,常常需要了解某個元素是否進入了“視口”(viewport),即用戶能不能看到它。

上圖的綠色方塊不斷滾動,頂部會提示它的可見性。
傳統的實現方法是,監聽到`scroll`事件后,調用目標元素(綠色方塊)的[`getBoundingClientRect()`](https://developer.mozilla.org/en/docs/Web/API/Element/getBoundingClientRect)方法,得到它對應于視口左上角的坐標,再判斷是否在視口之內。這種方法的缺點是,由于`scroll`事件密集發生,計算量很大,容易造成[性能問題](http://www.ruanyifeng.com/blog/2015/09/web-page-performance-in-depth.html)。
[IntersectionObserver API](https://wicg.github.io/IntersectionObserver/),可以自動“觀察”元素是否可見,Chrome 51+ 已經支持。由于可見(visible)的本質是,目標元素與視口產生一個交叉區,所以這個 API 叫做“交叉觀察器”(intersection oberserver)。
## 簡介
IntersectionObserver API 的用法,簡單來說就是兩行。
```javascript
var observer = new IntersectionObserver(callback, options);
observer.observe(target);
```
上面代碼中,`IntersectionObserver`是瀏覽器原生提供的構造函數,接受兩個參數:`callback`是可見性變化時的回調函數,`option`是配置對象(該參數可選)。
`IntersectionObserver()`的返回值是一個觀察器實例。實例的`observe()`方法可以指定觀察哪個 DOM 節點。
```javascript
// 開始觀察
observer.observe(document.getElementById('example'));
// 停止觀察
observer.unobserve(element);
// 關閉觀察器
observer.disconnect();
```
上面代碼中,`observe()`的參數是一個 DOM 節點對象。如果要觀察多個節點,就要多次調用這個方法。
```javascript
observer.observe(elementA);
observer.observe(elementB);
```
注意,IntersectionObserver API 是異步的,不隨著目標元素的滾動同步觸發。規格寫明,`IntersectionObserver`的實現,應該采用`requestIdleCallback()`,即只有線程空閑下來,才會執行觀察器。這意味著,這個觀察器的優先級非常低,只在其他任務執行完,瀏覽器有了空閑才會執行。
## IntersectionObserver.observe()
IntersectionObserver 實例的`observe()`方法用來啟動對一個 DOM 元素的觀察。該方法接受兩個參數:回調函數`callback`和配置對象`options`。
### callback 參數
目標元素的可見性變化時,就會調用觀察器的回調函數`callback`。
`callback`會觸發兩次。一次是目標元素剛剛進入視口(開始可見),另一次是完全離開視口(開始不可見)。
```javascript
var observer = new IntersectionObserver(
(entries, observer) => {
console.log(entries);
}
);
```
上面代碼中,回調函數采用的是[箭頭函數](http://es6.ruanyifeng.com/#docs/function#箭頭函數)的寫法。`callback`函數的參數(`entries`)是一個數組,每個成員都是一個[`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)對象(詳見下文)。舉例來說,如果同時有兩個被觀察的對象的可見性發生變化,`entries`數組就會有兩個成員。
### IntersectionObserverEntry 對象
`IntersectionObserverEntry`對象提供目標元素的信息,一共有六個屬性。
```javascript
{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}
```
每個屬性的含義如下。
> - `time`:可見性發生變化的時間,是一個高精度時間戳,單位為毫秒
> - `target`:被觀察的目標元素,是一個 DOM 節點對象
> - `rootBounds`:容器元素的矩形區域的信息,`getBoundingClientRect()`方法的返回值,如果沒有容器元素(即直接相對于視口滾動),則返回`null`
> - `boundingClientRect`:目標元素的矩形區域的信息
> - `intersectionRect`:目標元素與視口(或容器元素)的交叉區域的信息
> - `intersectionRatio`:目標元素的可見比例,即`intersectionRect`占`boundingClientRect`的比例,完全可見時為`1`,完全不可見時小于等于`0`

上圖中,灰色的水平方框代表視口,深紅色的區域代表四個被觀察的目標元素。它們各自的`intersectionRatio`圖中都已經注明。
我寫了一個 [Demo](http://jsbin.com/canuze/edit?js,console,output),演示`IntersectionObserverEntry`對象。注意,這個 Demo 只能在 Chrome 51+ 運行。
### Option 對象
`IntersectionObserver`構造函數的第二個參數是一個配置對象。它可以設置以下屬性。
**(1)threshold 屬性**
`threshold`屬性決定了什么時候觸發回調函數,即元素進入視口(或者容器元素)多少比例時,執行回調函數。它是一個數組,每個成員都是一個門檻值,默認為`[0]`,即交叉比例(`intersectionRatio`)達到`0`時觸發回調函數。
如果`threshold`屬性是0.5,當元素進入視口50%時,觸發回調函數。如果值為`[0.3, 0.6]`,則當元素進入30%和60%是觸發回調函數。
```javascript
new IntersectionObserver(
entries => {/* … */},
{
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);
```
用戶可以自定義這個數組。比如,上例的`[0, 0.25, 0.5, 0.75, 1]`就表示當目標元素 0%、25%、50%、75%、100% 可見時,會觸發回調函數。

**(2)root 屬性,rootMargin 屬性**
`IntersectionObserver`不僅可以觀察元素相對于視口的可見性,還可以觀察元素相對于其所在容器的可見性。容器內滾動也會影響目標元素的可見性,參見本文開始時的那張示意圖。
IntersectionObserver API 支持容器內滾動。`root`屬性指定目標元素所在的容器節點。注意,容器元素必須是目標元素的祖先節點。
```javascript
var opts = {
root: document.querySelector('.container'),
rootMargin: '0px 0px -200px 0px'
};
var observer = new IntersectionObserver(
callback,
opts
);
```
上面代碼中,除了`root`屬性,還有[`rootMargin`](https://wicg.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin)屬性。該屬性用來擴展或縮小`rootBounds`這個矩形的大小,從而影響`intersectionRect`交叉區域的大小。它的寫法類似于 CSS 的`margin`屬性,比如`0px 0px 0px 0px`,依次表示 top、right、bottom 和 left 四個方向的值。
上例的`0px 0px -200px 0px`,表示容器的下邊緣向上收縮200像素,導致頁面向下滾動時,目標元素的頂部進入可視區域200像素以后,才會觸發回調函數。
這樣設置以后,不管是窗口滾動或者容器內滾動,只要目標元素可見性變化,都會觸發觀察器。
## 實例
### 惰性加載(lazy load)
有時,我們希望某些靜態資源(比如圖片),只有用戶向下滾動,它們進入視口時才加載,這樣可以節省帶寬,提高網頁性能。這就叫做“惰性加載”。
有了 IntersectionObserver API,實現起來就很容易了。圖像的 HTML 代碼可以寫成下面這樣。
```html
<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">
```
上面代碼中,圖像默認顯示一個占位符,`data-src`屬性是惰性加載的真正圖像。
```javascript
function query(selector) {
return Array.from(document.querySelectorAll(selector));
}
var observer = new IntersectionObserver(
function(entries) {
entries.forEach(function(entry) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
});
}
);
query('.lazy-loaded').forEach(function (item) {
observer.observe(item);
});
```
上面代碼中,只有圖像開始可見時,才會加載真正的圖像文件。
### 無限滾動
無限滾動(infinite scroll)指的是,隨著網頁滾動到底部,不斷加載新的內容到頁面,它的實現也很簡單。
```javascript
var intersectionObserver = new IntersectionObserver(
function (entries) {
// 如果不可見,就返回
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log('Loaded new items');
}
);
// 開始觀察
intersectionObserver.observe(
document.querySelector('.scrollerFooter')
);
```
無限滾動時,最好像上例那樣,頁面底部有一個頁尾欄(又稱[sentinels](sentinels),上例是`.scrollerFooter`)。一旦頁尾欄可見,就表示用戶到達了頁面底部,從而加載新的條目放在頁尾欄前面。否則就需要每一次頁面加入新內容時,都調用`observe()`方法,對新增內容的底部建立觀察。
### 視頻自動播放
下面是一個視頻元素,希望它完全進入視口的時候自動播放,離開視口的時候自動暫停。
```html
<video src="foo.mp4" controls=""></video>
```
下面是 JS 代碼。
```javascript
let video = document.querySelector('video');
let isPaused = false;
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio != 1 && !video.paused) {
video.pause();
isPaused = true;
} else if (isPaused) {
video.play();
isPaused=false;
}
});
}, {threshold: 1});
observer.observe(video);
```
上面代碼中,`IntersectionObserver()`的第二個參數是配置對象,它的`threshold`屬性等于`1`,即目標元素完全可見時觸發回調函數。
## 參考鏈接
- [IntersectionObserver’s Coming into View](https://developers.google.com/web/updates/2016/04/intersectionobserver)
- [Intersection Observers Explained](https://github.com/WICG/IntersectionObserver/blob/gh-pages/explainer.md)
- [A Few Functional Uses for Intersection Observer to Know When an Element is in View](https://css-tricks.com/a-few-functional-uses-for-intersection-observer-to-know-when-an-element-is-in-view/), Preethi