在我们实际开发中,有时候不可避免地需要去监听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>