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>
复制代码