JS怎麼監聽div元素的resize

  在實現一個自定義滾動條需求的時候,須要監聽到某個div元素的寬高變化,第一時間想到的是resize事件,可是很不幸運的是,resize事件只能加在window對象上,並不能監聽具體某個DOM元素。javascript

  多方查閱以後,瞭解到MutationObserverResize Observer,能夠用來監聽整個DOM中任何變化的東西,能夠把它理解爲一個類,實例化以後調用類實例的幾個簡單接口便可完成監聽,如下具體介紹:css

MutationObserver介紹

構造函數爲window.MutationObserver,參數爲一個回調函數。

  監控到DOM中的改變而且等待一系列改變結束後就會觸發回調函數。它與事件的不一樣之處在於,它在DOM變化時,會記錄每個DOM的變化(爲一個MutationRecord對象),可是到DOM變化結束時觸發回調。DOM變化多是一系列的(好比元素的寬和高同時改變),那麼這一系列的變化就會產生一個隊列,這個隊列會做爲參數傳遞給回調函數。html

  因爲瀏覽器差別的緣由,一些版本的瀏覽器各自支持了構造函數,可是用法都是同樣的,實例化一個觀察者的代碼以下:vue

let MutationObserver = window.MutationObserver ||
                      window.WebKitMutationObserver || 
                      window.MozMutationObserver
                      
let observer = new MutationObserver(callback)    
複製代碼

調用接口開始監控DOM。

經常使用的接口有三個:java

  • observe(element, options) 配置MutationObserver在DOM更改匹配給定選項時,經過其回調函數開始接收通知。node

    element即要監聽的DOM元素,options爲監聽選項對象,可選的選項以下:git

 因此監聽元素寬高變化,就是監聽其style屬性變化:es6

observer.observe(element, { 
            attributes: true, 
            attributeFilter: ['style'], 
            attributeOldValue: true
        })
複製代碼

這樣當元素的style發生改變的時候,就會觸發構造函數中傳入的callback函數。github

  • disconnect() 阻止 MutationObserver 實例繼續接收的通知,直到再次調用其observe方法,該觀察者對象包含的回調函數都不會再被調用。web

  • takeRecords() 從MutationObserver的通知隊列中刪除全部待處理的通知,並將它們返回到一個MutationRecord對象構成的新數組中。

示例

這裏以Vue中的一個組件做爲實例,瞭解了以上所述內容後其實很是簡單,代碼以下:

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="js/vue.js" type="text/javascript" charset="utf-8"></script>
		<style type="text/css">
			html,body{
				width: 100%;
				height: 100%;
			}
			.container {
				width: 100%;
				height: 100%;
				position: relative
			}
			
			.resize-element {
				position: absolute;
				top: 50%;
				left: 50%;
				height: 10rem;
				width: 10rem;
				transform: translate(-50%,-50%);
				overflow: hidden;
				resize: both;   /*用戶能夠調節元素的寬度和高度*/
				display: block;
				box-shadow: 0 0 1px 1px #3361D8;
				border-radius: 2px;
			}
		</style>
	</head>

	<body>
		<div class="container" id="main">
			<div class="resize-element">
				改變大小試試
			</div>
			<div class="resize-record">
				觸發了{{firedNum}}次resize事件。
			</div>
		</div>
		<script type="text/javascript">
			new Vue({
				el: "#main",
				data: {
					observer: null,
					firedNum: 0,
					recordOldValue: { // 記錄下舊的寬高數據,避免重複觸發回調函數
						width: '0',
						height: '0'
					}
				},
				mounted() {
					let MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver
					let element = document.querySelector('.resize-element')
					this.observer = new MutationObserver((mutationList) => {
						for(let mutation of mutationList) {
							console.log(mutation)
						}
						let width = getComputedStyle(element).getPropertyValue('width')
						let height = getComputedStyle(element).getPropertyValue('height')
						if(width === this.recordOldValue.width && height === this.recordOldValue.height) return
						this.recordOldValue = {
							width,
							height
						}
						this.firedNum += 1
					})
					this.observer.observe(element, {
						attributes: true,
						attributeFilter: ['style'],
						attributeOldValue: true
					})
				},
				beforeDestroyed() {
					if(this.observer) {
						this.observer.disconnect()
						this.observer.takeRecords()
						this.observer = null
					}
				}

			})
		</script>
	</body>

</html>
複製代碼

這裏記錄了舊的寬高數據來避免重複觸發回調函數,這樣作的緣由在於寬高數據改變時,不必定是整數,而MutationRecord.recordOldValue中記錄的是取整後的數據,這樣就會致使在拖動改變DOM元素的寬高時,數值一直在整數和小數之間跳動,會屢次觸發。

MutationObserver實現Vue nextTick

Vue 倡導開發者儘可能不直接操做DOM,但有的時候因爲各類需求讓開發者不得不這樣作,因而 nextTick 的實現就是讓開發者在修改數據後,可以在數據更新到DOM後才執行對應的函數,從而獲取最新的 DON 數據。

那麼如何實現 nextTick呢,咱們首先能夠想到的是利用 setTimeout 的異步回調來實現,不過因爲各個瀏覽器的不一樣,setTimeout 的延遲很高,所以在 nextTick 中只做爲最後的備胎,首選的方案則是 MutationObserver(在後面的內容中 MO 表明 MutationObserver)

nextTick 的源碼實現

export const nextTick = (function () {
  var callbacks = []
  var pending = false
  var timerFunc
  function nextTickHandler () {
    pending = false
    var copies = callbacks.slice(0)
    callbacks = []
    for (var i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  /* istanbul ignore if */
  if (typeof MutationObserver !== 'undefined') { // 首選 MutationObserver 
    var counter = 1
    var observer = new MutationObserver(nextTickHandler) // 聲明 MO 和回調函數
    var textNode = document.createTextNode(counter)
    observer.observe(textNode, { // 監聽 textNode 這個文本節點
      characterData: true // 一旦文本改變則觸發回調函數 nextTickHandler
    })
    timerFunc = function () {
      counter = (counter + 1) % 2 // 每次執行 timeFunc 都會讓文本在 1 和 0 間切換
      textNode.data = counter
    }
  } else {
    timerFunc = setTimeout // 若是不支持 MutationObserver, 退選 setTimeout
  }
  return function (cb, ctx) {
    var func = ctx
      ? function () { cb.call(ctx) }
      : cb
    callbacks.push(func)
    if (pending) return
    pending = true
    timerFunc(nextTickHandler, 0)
  }
})()
複製代碼

MutationObserver 的功能和做用

MO 給開發者提供了一種能在某個範圍內的DOM數發生變化時做出適當反應的能力

用人話說是開發者能經過它建立一個觀察者對象,這個對象會監聽某個DOM元素,並在它的DOM樹發生變化時執行咱們提供的回調函數。

具體參考這個 DEMO點擊預覽

比較特別的是實例化的時候須要先傳入回調函數:

var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        console.log(mutation.type);
      })
    })
複製代碼

而後才配置觀察選項,包括觀察節點和觀察的屬性:

// 選擇目標節點
var target = document.querySelector('#some-id');
 
// 配置觀察選項:
var config = { attributes: true, childList: true, characterData: true }
 
// 傳入目標節點和觀察選項
observer.observe(target, config);
 
// 隨後,你還能夠中止觀察
observer.disconnect();
複製代碼

對於老版本的谷歌和火狐,則須要使用帶前綴的 MO:

var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver
複製代碼

MutationObserver 和 microtask

那麼爲何優選使用 MutationObserver呢?

一開始覺得是 MO 就是用來監聽 DOM 變化的,那麼使用 textnode 模擬 DOM 變化再利用 MO 來監聽觸發從而實現 nextTick 不就很適合,直到了解看到了知乎上的問答才知道是由於 MO 會比 setTimeout 早執行的緣故,

這裏須要瞭解JS的運行運行機制(從新刷新了個人三觀), JS 的事件運行機制執行的時候會區分 taskmicrotask, 引擎在每一個 task 執行完畢,並在從隊列裏取下一個task來執行以前, 執行完全部的 microtask 隊列中的 microtask.

** setTimeout** 回調會被分配到一個新的task中等待執行,而 Promise 的 resolver、MO 的 回調都會被分配到 microtask 的隊列中,因此會比 setTimout 先執行.

除了比 setTimout 快以外,還有 渲染性能 的問題,根據HTML Standard, 每一個 task 運行完之後, UI 都會從新渲染,那麼在 microtask 中就完成數據更新, 當前 task 結束就能夠獲得最新的 UI, 反之若是新建一個 task 來作數據更新,那麼渲染就會進行兩次。

因此性價好比此高的 MO 天然成爲了首選

關於 microtask,具體能夠閱讀 Jake 寫的 Tasks, microtasks, queues and schedules

Vue nextTick的版本迭代

上面關於 nextTick 的源碼實現屬於 vue 最先的版本 v1.0.9,在深挖 mutationObserver 的時候發現 nextTick 在vue的版本迭代中也在不斷的進化,同事也發生過退化,很是有趣:

先說說退化的事件,尤大(vue的做者)曾經使用 window.postMessage 來替代 MO 實現 nextTick,結果開發者使用後發現了問題,能夠看看這兩個 JSFiddle:jsfiddle1點擊預覽 和 jsfiddle2點擊預覽, 兩個例子用了不一樣版原本實現元素的絕對定位,第一個使用的是 2.0.0-rc6,這個版本採用的是 MO,然後來由於 IOS 9.3 的 WebView 裏 MO 有 bug,尤大便換成 window.postMessage來實現,即第二個實例版本爲 2.0.0-rc7, 可是因爲 postMessage 會將回調放到 macrotask 其實也就是 task 裏面,致使可能執行了屢次 UI 的task都沒有執行 window.postMessage 的 task,也就延遲了更新DOM操做的時間。尤大在後續版本撤回了這一次修改,具體的討論能夠看issue

關於進化,在後續的版本里,因爲 es6 的新語法,nextTick 開始使用 Promise.then 和 MO 來作首選和次選,在前面的討論中已經提到,Promise.then 也屬於 microtask。

Resize Observer

Resize Observer是一個新的JavaScript API,與Intersection Observer API、Mutation Observer等其餘觀察者API很是類似。 它容許在尺寸發生變化時通知元素。

ResizeObserver的解釋:開發過程中常常遇到的一個問題就是如何監聽一個 div 的尺寸變化。但衆所周知,爲了監聽 div 的尺寸變化,都將偵聽器附加到 window 中的 resize 事件。但這很容易致使性能問題,由於大量的觸發事件。換句話說,使用 window.resize 一般是浪費的,由於它告訴咱們每一個視窗大小的變化,而不只僅是當一個元素的大小發生變化。

使用 ResizeObserver 的API的另外一個用例就是視窗 resize 事件不能幫助咱們:當元素被動態地添加或刪除時,會影響父元素的大小。這也是現代單頁應用程序愈來愈頻繁使用 ResizeObserver 緣由之一。 經過 window.resize 事件的監聽,能夠調用一個回調函數。在這個回調函數中作咱們須要作的事情。

// define a callback
function callback() {
    // something cool here
}
// add resize listener to window object
window.addEventListener('resize', callback)
複製代碼

好比說,你要調整一個元素的大小,那就須要在 resize 的回調函數 callback() 中調用 getBoundingClientRect 或 getComputerStyle 不過你要是不當心處理全部的讀和寫操做,就會致使佈局混亂。好比下面這個小示例:

當你改變瀏覽器視窗大小的時候,就能夠看到相應的變化:

這也就是爲何 ResizeObserver 是一個有用的API。它對所觀察到的任何元素的大小的變化作出反應,而不依賴於所引發的變化。它還提供了對所觀察元素的新大小的訪問。那接下來讓咱們直接切入正題。

簡單總結一下:

ResizeObserver 容許咱們觀察DOM元素的內容矩形大小(寬度、高度)的變化,並作出相應的響應。它就像給元素添加 document.onresize() 或 window.resize() 事件(但在JavaScript中,只有 window 纔有 resize 事件)。當元素改變大小而不調整視窗大小時,它是有用的。 下面描述一些調整觀察者的行爲:

  • 當觀察到的元素被插入或從DOM中刪除時,觀察將會觸發
  • 當觀察到的元素 display 值爲 none 時,觀察都會觸發
  • 觀察不會對未替換的內聯元素(non-replaced inline element)觸發
  • 觀察不會由CSS的 transform 觸發
  • 若是元素有顯示,並且元素大小不是 0,0 ,觀察將會觸發

基本用法 使用Resize Observer很是簡單,只需實例化一個新的ResizeObserver對象並傳入一個回調函數,該函數接收觀察到的條目

const myObserver = new ResizeObserver(entries => {
  // 遍歷條目,作一些事情
});
複製代碼

而後,咱們能夠在實例上調用observe並傳入一個元素來觀察

const someEl = document.querySelector('.some-element');
const someOtherEl = document.querySelector('.some-other-element');

myObserver.observe(someEl);
myObserver.observe(someOtherEl);
複製代碼

對於每一個entry,咱們都會獲得一個包含contentRect和一個target屬性的對象。target是DOM元素自己,contentRect是具備如下屬性的對象:width,height,x,y,top,right,bottom和left。

與元素的getBoundingClientRect不一樣,contentRect的width和height值不包含padding。contentRect.top是元素的頂部padding,contentRect.left是元素的左側padding。

好比要打印出被監聽元素寸尺變化時width和height的值,能夠像下面這樣作:

const myObserver = new ResizeObserver(entries => {
  entries.forEach(entry => {
    console.log('width', entry.contentRect.width);
    console.log('height', entry.contentRect.height);
  });
});

const someEl = document.querySelector('.some-element');
myObserver.observe(someEl);
複製代碼

上面的示例中,使用了forEach 循環來遍歷觀察者的回調中的 entries ,其實在 entries 上使用 for ... of 能夠獲得相同的效果

Resize Observer API 示例

下面是一個簡單的演示,以查看Resize Observer API的實際應用。 經過調整瀏覽器窗口的大小來嘗試一下,注意漸變角度和文本內容僅在元素的大小受到影響時才發生變化:

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="js/vue.js" type="text/javascript" charset="utf-8"></script>
		<style type="text/css">
			html,
			body {
				width: 100%;
				height: 100%;
			}
			
			.box {
				text-align: center;
				height: 20vh;
				border-radius: 8px;
				box-shadow: 0 0 4px var(--subtle);
				display: flex;
				justify-content: center;
				align-items: center;
			}
			
			.box h3 {
				color: #fff;
				margin: 0;
				font-size: 5vmin;
				text-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
			}
			
			.box.small {
				max-width: 550px;
				margin: 1rem auto;
			}
		</style>
	</head>

	<body>
		<div class="box">
			<h3 class="info"></h3>
		</div>
		<div class="box small">
			<h3 class="info"></h3>
		</div>
		<script type="text/javascript">
			const boxes = document.querySelectorAll('.box');

			const myObserver = new ResizeObserver(entries => {
				for(let entry of entries) {
					const infoEl = entry.target.querySelector('.info');
					const width = Math.floor(entry.contentRect.width);
					const height = Math.floor(entry.contentRect.height);

					const angle = Math.floor(width / 360 * 100);
					const gradient = `linear-gradient(${ angle }deg, rgba(0,143,104,1) 50%, rgba(250,224,66,1) 50%)`;

					entry.target.style.background = gradient;

					infoEl.innerText = `I'm ${ width }px and ${ height }px tall`; } }); boxes.forEach(box => { myObserver.observe(box); }); </script> </body> </html> 複製代碼

經常使用npm包

resize-detector
  size-sensor
複製代碼

使用

  • Install
cnpm i --save size-sensor
複製代碼
import { bind, clear } from 'size-sensor'
複製代碼
  • bind&unbind
import { bind, clear } from 'size-sensor';
 
// bind the event on element, will get the `unbind` function
const unbind1 = bind(document.querySelector('.container'), element => {
  // do what you want to to.
});
 
const unbind2 = bind(document.querySelector('.container'), element => {
  // do what you want to to.
});
 
// if you want to cancel bind event.
unbind1();
複製代碼
  • clear
import { bind, clear } from 'size-sensor';
 
/*
 * // bind the resize event.
 * const unbind1 = bind(...);
 * const unbind2 = bind(...);
 * ...
 */
 
// you can cancel all the event of element.
clear(element);
複製代碼
  • 實現方式:

模擬windows的resize

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="js/vue.js" type="text/javascript" charset="utf-8"></script>
		<style type="text/css">
			html,body{
				width: 100%;
				height: 100%;
			}
			.container {
				width: 100%;
				height: 100%;
				position: relative
			}
			
			.resize-element {
				position: absolute;
				top: 50%;
				left: 50%;
				height: 10rem;
				width: 10rem;
				transform: translate(-50%,-50%);
				overflow: hidden;
				resize: both;   /*用戶能夠調節元素的寬度和高度*/
				display: block;
				box-shadow: 0 0 1px 1px #3361D8;
				border-radius: 2px;
			}
		</style>
	</head>

	<body>
		<div class="container" id="main">
			<div class="resize-element">
				改變大小試試
			</div>
			<div class="resize-record">
				窗口觸發了{{firedNum}}次resize事件。
			</div>
		</div>
		<script type="text/javascript">
			const CSS = 'position:absolute;left:0;top:-100%;width:100%;height:100%;margin:1px 0 0;border:none;opacity:0;visibility:hidden;pointer-events:none;';
			function observeResize(element, handler) {
				let frame = document.createElement('iframe');
				frame.style.cssText = CSS;
				frame.onload = () => {
					frame.contentWindow.onresize = () => {
						handler(element);
					};
				};
				element.appendChild(frame);
				return frame;
			}

			let element = document.getElementById('main');
			// listen for resize
			observeResize(element, () => {
				console.log('new size: ', {
					width: element.clientWidth,
					height: element.clientHeight
				});
			});
		</script>
	</body>

</html>
複製代碼
相關文章
相關標籤/搜索