【DOM进阶】监听DOM元素的size变化

在我们实际开发中,有时候不可避免地需要去监听DOM元素尺寸变化,但是w3c中并没有对此接口的规范,实际上大部分浏览器厂商并没有去实现这个事件,只有IE中实现了,但是呢,因为大家对IE的态度,所以。。。

今天我们就来通过别的方式间接地来实现DOM元素的resize事件。 

实现方向

对于DOM元素resize的监听实现,大概有两种方向。

一种是轮询,就是设置定时器不断地去查询目标元素的尺寸有没有变化。

另一种就是通过某种方式,让DOM尺寸变化时触发某些已有的事件来间接地实现resize事件。

对于轮询方案,我们肯定是不建议的,因为如果目标元素很多,就会建立很多的timer,势必影响整体性能;其次有的元素可能一直不会发生尺寸更改,但是却有一个定时器在运行,资源浪费。

有人也会说我讲所有元素的定时器与触发器做集中处理不就行了,但是这样做的话,也会有另外一个问题:“响应不及时”。因为定时器轮询有一个间隔,没有在本次轮询中触发,只能等待下一个轮询,这个间隔时间设置小了,占用太多资源;设置太大,又造成响应不及时,所以轮询绝对只能做最下策的选择。

实现原理

其实这次我们要实现的原理其实就是借用“scroll”事件。因为在元素尺寸变化的时候,最直接作用的就是scroll的值,因为父元素的尺寸变化会导致滚动条的出现或者消失。

比如在元素扩大时,如果父元素的client尺寸大于子元素的scroll尺寸,那么父元素的scrollTop/scrollLeft就一定是0;如果父元素的client尺寸小于子元素的scroll尺寸,那么父元素一定会出现滚动条。

这里我们将两种情况分开来讨论。

如果元素扩大时,内部存在一个子元素,子元素的尺寸比父元素大10px,那么可设置的scrollTop/scrollLet就是10px + scrollbar(滚动条宽度),随着父元素不断地扩大,子元素可设置的scrollTop/scrollLeft就会不断地变小,直到变成0,在scrollTop/scrollLeft不断地变小的过程中,元素会不停地发出scroll事件,我们就正好可以利用。

再比如,如果元素缩小时,内部存在一个子元素,子元素尺寸总为父元素的2倍,那么可设置的最大scrollTop/scrollLeft则为父元素client + scrollbar,随着父元素不断的变小,可设置的最大scrollTop/scrollLeft只会越来越小,这时候也会触发scroll事件。

代码实现

理解了上面的原理后,其实用代码实现就不难了,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Resize Listen</title>
	<style>
		#target {
			height: 100px;
			width: 100px;
			background: red;
		}

		.resize-triggers,
		.resize-triggers > div,
		.minify-trigger:before {
			display: block;
			position: absolute;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			overflow: hidden;
			visibility: hidden;
			opacity: 0;
			z-index: -1;
		}

		.minify-trigger:before {
			content: '';
			width: 200%;
			height: 200%;
		}
	</style>
</head>
<body>
	<div id="target"></div>
	<script>
		let sizeList = ['100px', '50px'];
		window.setInterval(() => {
			sizeList = sizeList.reverse()
			document.getElementById('target').style.height = sizeList[0]
		}, 2000)
	</script>
	<script>
		const targetEl = document.getElementById('target');

		function resetTriggers(el) {
			const triggersEl = el.__resizeTriggers__;
			const expandTrigger = triggersEl.firstElementChild;
			const expandChild = expandTrigger.firstElementChild;
			expandChild.style.width = expandTrigger.offsetWidth + 1 + 'px';
			expandChild.style.height = expandTrigger.offsetHeight + 1 + 'px';
			expandTrigger.scrollLeft = 1;
			expandTrigger.scrollTop = 1;

			const minifyTrigger = triggersEl.lastElementChild;
			minifyTrigger.scrollLeft = minifyTrigger.scrollWidth;
			minifyTrigger.scrollTop = minifyTrigger.scrollHeight;
		}

		const checkTriggers = function(el) {
			return el.offsetWidth !== el.__resizeLast__.width || el.offsetHeight !== el.__resizeLast__.height;
		};

		function scrollHandler(event) {
			resetTriggers(this);
			if (this.__resizeTimer__) window.clearTimeout(this.__resizeTimer__);
			this.__resizeTimer__ = window.setTimeout(() => {
				if (checkTriggers(this)) {
					this.__resizeLast__.width = this.offsetWidth;
					this.__resizeLast__.height = this.offsetHeight;
					this.__resizeListeners__.forEach((fn) => {
						fn.call(this, event);
					});
				}
			}, 0);
		}

		function addResizeListener(el, fn) {
			if (!el.__resizeTriggers__) {
				const triggersEl = el.__resizeTriggers__ = document.createElement('div');
				el.__resizeListeners__ = []
				el.__resizeLast__ = {}

				if (getComputedStyle(el).position === 'static') {
					el.style.position = 'relative';
				}

				triggersEl.className = 'resize-triggers'
				triggersEl.innerHTML = '<div class="expand-trigger"><div></div></div><div class="minify-trigger"></div>'

				el.appendChild(triggersEl)
				resetTriggers(el)
				el.addEventListener('scroll', scrollHandler, true)
			}

			el.__resizeListeners__.push(fn);
		}

		addResizeListener(targetEl, () => {
			console.log('OK')
		})
	</script>
</body>
</html>