当前位置: 首页 > Web前端 > HTML5

如何优雅的监控容器高度变化

时间:2023-04-05 23:38:52 HTML5

前言老手:如何监控DOM元素的高度变化?菜鸟:哈哈哈哈哈哈哈,这个我都不知道,用onresize事件鸭!老鸟抬眼,空气静了几秒,菜鸟才回过神来。是的,普通DOM元素是没有onresize事件的,只有window对象才有这个事件。哈哈哈哈,以上纯属虚构,不过在最近的项目中,遇到容器高度(宽度)变化监测:在使用iscroll或better-scroll滚动插件时,如果容器内部元素的高度发生变化,则外部包必须及时更新Container,即调用refresh()方法。否则会导致滚动错误(滚动不到底部或滚出底部)。也许我们的一般做法是:每次更新(删除或插入)DOM节点时调用refresh()来更新外部容器。对于加载异步资源(比如图片),使用onload监听每次加载完成,然后调用refresh()更新外部容器。这样我们会发现,如果容器内部元素越复杂,调用就会越来越繁琐,甚至考虑到用户使用的每一次操作都可能导致内部元素的宽高变化,然后调整外部容器,调用refresh()。事实上,无论对元素进行什么样的操作,它的属性、后代节点、文本节点都会发生变化。如果能监听到这个变化,只需要比较容器的宽高就可以实现变化。容器宽高的监控不需要与其外部行为相关。DOM3事件规范为我们提供了MutationObserver接口的能力,可以监控对DOM树所做的更改。MutationObserverMutationObserverAPI用于监控DOM变化。这个API可以通知DOM的任何变化,比如节点的增减、属性的变化、文本内容的变化。PSMutationObserverAPI已经有很好的浏览器兼容性,如果IE10及以下没有要求。MutationObserver特性只要DOM发生变化,就会触发MutationObserver事件。但与事件不同的是:事件是同步触发的,DOM变化会立即触发相应的事件;MutationObserver是异步触发的,DOM变化不会立即触发,而是在当前所有DOM操作完成后触发。总的来说,有以下特点:等待所有脚本任务完成后再运行(即异步触发方式)。它将DOM变化记录封装成一个数组进行处理,而不是一个一个地处理DOM变化。可以观察DOM所有类型的变化,也可以指定只观察某一类变化。MutationObserver构造函数MutationObserver构造函数的实例被传递一个回调函数,它接受两个参数,第一个是变化的数组,第二个是观察者的实例。varobserver=newMutationObserver(function(mutations,observer){mutations.forEach(function(mutaion){console.log(mutation);})})MutationObserver实例的observe()方法用于执行监控,接受两个参数:第一个参数是观察到的DOM节点;第二个参数是一个配置对象,指定要观察的特征。var$tar=document.getElementById('tar');varoption={childList:true,//子节点的变化(新的、删除的或改变的)attributes:true,//属性的变化characterData:true,//变化innodecontentornodetextsubtree:true,//是否对节点的所有后代节点应用观察者attributeFilter:['class','style'],//观察特定属性attributeOldValue:true,//观察属性时变化,是否需要记录变化前的属性值?characterDataOldValue:true//观察characterData的变化,需要记录变化前的值吗?mutationObserver.observe($tar,选项);option中必须有childList、attributes和characterData中的一个或多个,否则会报错。各属性含义如下:childList布尔值,表示是否应用于子节点的变化(增删改查);attributes布尔值,表示是否应用到属性变化;characterData布尔值,表示是否应用于节点内容或节点文本的变化;subtree布尔值,表示是否对该节点的所有后代节点应用观察者;attributeFilter数组,表示观察特定的属性;attributeOldValue布尔值,表示是否记录观察属性前的变化属性值;characterDataOldValue布尔值,表示是否观察characterData的变化,是否记录变化前的值;childList和子树属性childList属性表示是否适用于子节点的变化(增删改查),不能监听子节点变化的后代节点。varmutationObserver=newMutationObserver(function(mutations){console.log(mutations);})mutationObserver.observe($tar,{childList:true,//子节点的变化(增删改)})var$div1=document.createElement('div');$div1.innerText='div1';//添加新的子节点$tar.appendChild($div1);//可以监听//删除子节点$tar.childNodes[0].remove();//可以监听var$div2=document.createElement('div');$div2.innerText='div2';var$div3=document.createElement('div');$div3.innerText='div3';//添加新的子节点$tar.appendChild($div2);//可以监听//替换子节点$tar.replaceChild($div3,$div2);//可以监听//添加孙节点$tar.childNodes[0].appendChild(document.createTextNode('Addgrandchildrentextnode'));//无法监听attributes和attributeFilter属性。attributes属性表示是否应用DOM节点属性监视器的值变化。attributeFilter属性用于过滤要监控的属性键。//...mutationObserver.observe($tar,{attributes:true,//属性改变attributeFilter:['class','style'],//观察特定属性})//...//改变样式属性$tar.style.height='100px';//可以监听//改变类名$tar.className='tar';//可以监听//改变数据集$tar.dataset='abc';//characterData和subtree属性无法监听。characterData属性指示是否应用于节点内容或节点文本的更改。subtree是否将观察者应用于该节点的所有后代节点。为了更好的观察节点文本的变化,将两者结合应用富文本监控是一个不错的选择。简单的富文本,例如一个简单的编辑器

var$tar=document.getElementById('tar');varMutationObserver=window.MutationObserver||window.webkitMutationObserver||窗户。MozMutationObserver;varmutationObserver=newMutationObserver(function(mutations){console.log(mutations);})mutationObserver.observe($tar,{characterData:true,//节点内容或节点文本的变化subtree:true,//是否将观察者应用于该节点的所有后代节点})takeRecords(),disconnect()方法MutationObserver实例上还有另外两个方法,takeRecords()用于清除记录队列并返回一个变异记录数组。disconnect()用于停止观察。调用该方法后,如果DOM发生变化,则不会触发观察者。var$text5=document.createTextNode('新文本节点5');var$text6=document.createTextNode('新文本节点6');//新文本节点$tar.appendChild($text5);varrecord=mutationObserver.takeRecords();console.log('record:',record);//返回记录新的文本节点操作,并清空监听队列//替换文本节点$tar.replaceChild($text6,$text5);mutationObserver.disconnect();//这里不再收听了//删除文本节点$tar.removeChild($text6);//有两个属性attributeOldValue和characterDataOldValue无法监听,但实际上在影响takeRecords()方法返回一个MutationRecord实例。如果设置了这两个属性,则在记录返回对象中的oldValue之前,会对应属性和数据的旧值。比如用tar替换className原来的值aaa,oldValue记录为aaa。record:[{addedNodes:NodeList[]attributeName:"class"attributeNamespace:nullnextSibling:nulloldValue:"aaa"previousSibling:nullremovedNodes:NodeList[]target:div#tar.tartype:"attributes"}]MutationObserver的应用容器本身和内部元素的属性变化、节点变化和文本变化是影响容器高和宽的重要因素(当然还有其他因素)。以上了解了MutationObserverAPI的一些细节,可以监听容器宽高的变化。var$tar=document.getElementById('tar');varMutationObserver=window.MutationObserver||window.webkitMutationObserver||window.MozMutationObserver;varrecordHeight=0;varmutationObserver=newMutationObserver(function(mutations){console.log(mutations);letheight=window.getComputedStyle($tar).getPropertyValue('height');if(height===recordHeight){return;}recordHeight=height;console.log('heightchanged');//然后更新外部容器等操作})mutationObserver.observe($tar,{childList:true,//child的变化nodes(增删改)attributes:true,//属性的变化characterData:true,//Nodes内容或节点文本的变化subtree:true//是否将观察者应用到该节点的所有后代节点})slip通过网络:动画(animation,transform)改变容器的高度(宽度)除了容器内部元素节点和属性的改变,另外,css3动画会影响容器的高度和宽度。由于动画不会引起元素属性的变化,因此MutationObserverAPI无法对其进行监控。将#tar容器添加到下面的css动画@keyframeschangeHeight{to{height:300px;}}#tar{背景色:浅绿色;边框:1px实心#ccc;animation:changeHeight2sease-in1s;}可以看出,没有打印输出,无法监听动画的高宽。所以,在这里对付这条“漏网之鱼”是很有必要的。处理很简单,监听动画(transitionend、animationend)停止事件触发时的高宽变化即可。这里使用Vue自定义指令如下:/***监听元素高度变化,更新滚动容器*/Vue.directive('observe-element-height',{insert(el,binding){constMutationObserver=window.MutationObserver||window.webkitMutationObserver||window.MozMutationObserverletrecordHeight=0constonHeightChange=_.throttle(function(){//_.throttle节流函数letheight=window.getComputedStyle(el).getPropertyValue('height');if(height===recordHeight){return}recordHeight=heightconsole.log('Theheighthaschanged')//更新外部容器及之后的其他操作},500)el.__onHeightChange__=onHeightChangeel.addEventListener('animationend',onHeightChange)el.addEventListener('transitionend',onHeightChange)el.__observer__=newMutationObserver((mutations)=>{onHeightChange()});el.__observer__.observe(el,{childList:true,subtree:true,characterData:true,attributes:true})},unbind(el){if(el.__observer__){el.__observer__.disconnect()el.__observer__=null}el.removeEventListener('animationend',el.__onHeightChange__)el.removeEventListener('transitionend',el.__onHeightChange__)el.__onHeightChange__=null}})ResizeObserver自宽高集装箱区对监控有硬性要求,有相关规范吗?答案是肯定的,ResizeObserver接口可以监听Element的内容区域或者SVGElement的boundingbox。要更改内容区域,需要减去内边距填充。目前还是实验性接口,各大浏览器对ResizeObserver的兼容不够,实际应用需谨慎。ResizeObserverPolyfill实验性API不足,总有一个Polyfill来弥补。ResizeObserverPolyfill利用事件冒泡来监控顶层文档上的动画transitionend;监听窗口调整大小事件;其次,使用MutationObserver监控文档元素;兼容IE11及以下,通过DOMSubtreeModified监控文档元素。使用MapShim(类似ES6中的Map)的数据结构,key为被监控元素,value为ResizeObserver实例,映射监控关系。顶层文档或窗口监听触发事件,可以通过绑定元素监听元素大小变化。部分源代码如下:/***初始化DOM侦听器。**@private*@returns{void}*/ResizeObserverController.prototype.connect_=function(){//如果在非浏览器环境中运行或者如果已经添加了侦听器,则不执行任何操作。如果(!isBrowser||this.connected_){返回;}//订阅“Transitionend”事件用作延迟转换的解决方法。这样就可以至少捕获//元素的最终状态。document.addEventListener('transitionend',this.onTransitionEnd_);window.addEventListener('resize',this.refresh);如果(mutationObserverSupported){this.mutationsObserver_=newMutationObserver(this.refresh);this.mutationsObserver_.observe(document,{attributes:true,childList:true,characterData:true,subtree:true});}else{document.addEventListener('DOMSubtreeModified',this.re新鲜的);this.mutationEventsAdded_=true;}this.connected_=true;};PS:不过,作者这里好像没有处理动画,也就是动画改变元素大小还是监听不到。不知道是不是我没有综合考虑,这个问题已经向笔者提出了。使用iframe模拟windowresizewindowresize没有兼容性问题。按照这个思路,可以使用隐藏的iframe模拟window来填充需要监控的容器元素。当容器大小发生变化时,iframe大小自然也会发生变化。通过contentWindow.onresize()可以监控。functionobserveResize(element,handler){letframe=document.createElement('iframe');constCSS='position:absolute;left:0;top:-100%;width:100%;height:100%;margin:1px00;border:none;opacity:0;visibility:hidden;pointer-events:没有任何;';frame.style.cssText=CSS;frame.onload=()=>{frame.contentWindow.onresize=()=>{处理程序(元素);};};元素.appendChild(框架);returnframe;}letelement=document.getElementById('main');//监听resizeobserveResize(element,()=>{console.log('newsize:',{width:element.clientWidth,height:element.客户端高度});});该方案常用的插件有iframe-resizer、resize-sensor等,但该方案并不是特别优雅。它需要插入一个iframe元素并定位父元素。页面上可能还有其他意想不到的问题。仅供参考。综上所述,归根结底,还是要优雅地监听元素的宽高变化,不是基于交互行为,而是从元素本身出发。关键是理解MutationObserver接口。其次,需要考虑到元素动画可能会引起宽高变化。兼容IE11及以下,通过DOMSubtreeModified监听。使用iframe模拟windowresize是一种参考方案。小功课,欢迎指正,完成~