JS监控DOM大小的变化
由于浏览器原生不自带DOM元素的resize事件,一直想弄一个JS监控DOM元素大小变化的功能,之前想过很多方法都各有优缺点,但是最近发现了一个很不错的方法,虽然以前有设想过但一直没有时间去实现,知道看到有人实现了和我的设想相同的一种比较优良的监听方法,这篇文章将主要讲述两种监听方法,以及优劣分析
DOM大小的变化的诱因
DOM大小的决定因素
在寻找如何监听DOM大小变化的解决办法之前,这里先列举DOM元素宽高的数值的情况:
- 自己设置的固定宽高,如样式设置
style="width:100px;height:100px"
- 由上层元素决定,如样式设置
style="width:50%;height:50%"
- 有下层元素决定,内容撑大
宽高各自独立,可以混合出现
DOM大小改变的原因
知道了决定的DOM大小的因素的3种情况,可以很快的列出大部分导致DOM大小变化的情况:
-
元素修改自己的宽高样式
元素修改自己的宽高属性,是最简单最直接也是最常见的DOM大小变化的情况
-
当元素大小由上层元素(包括窗口大小)决定,上层元素大小发生变动时
-
当袁术大小由下层元素决定,下层元素大小发生变动时
上面三种是最常见也是最容易想到的,仔细思考还存在其他情况
-
元素的class的内容改变
当元素的class的内容的宽高变化时,元素是不能监听到class内容变化的,虽然这个操作很不好,但是在切换页面皮肤时确实可能存在这样的问题
-
过渡/动画
当元素设置了过渡/动画效果时,元素的宽高是可能在每一帧的出现变化的
这些因素也是最难以监听和获取的可以叫"隐式变化",之前3个比较简单的变化由于可以在节点树上和style上明显看出变化,可以简单叫"显式变化"
显式变化可以通过MutationObserver
监听来达到事件流驱动节约性能的效果,但是隐式变化也是同样需要检测出来的,下面将列举两个检测办法,都各自有优缺点
解决办法
循环比对
最最简单直接有效的方法当然是每一帧去计算和比对需要监听的DOM的大小是否发生改变
height="265" scrolling="no" title="loop-listen" src="//codepen.io/sqchenxiyuan/embed/wYEqmZ/?height=265&theme-id=0&default-tab=html,result" allowfullscreen="true">See the Pen loop-listen by sqchenxiyuan ( @sqchenxiyuan) on CodePen.这个方法很简单,也能很快的思考出来,但是这个方法很多文章或者人都认为有很大的性能消耗,其实这个方法的性能消耗在现在的计算机性能下是很小的,相较于每次DOM大小变化导致的重绘消耗的性能,占比很小
覆盖情况
100%,所有元素都可以这样监听
性能分析
展示性能分析前,先先说下测试展示用的机器的配置,CPU是interl i7-4790k,GPU为GTX960
前6秒为禁止状态,后6秒为改动大小的状态
100节点监听
在前6秒里,可以看到以js的执行为主(毕竟没有重绘的需求),一直在执行dom大小的变化检测,但占据的性能并不是很多,大约1%。
在后4秒里,由于出现了dom的大小变化,页面还是执行渲染,在这段时间JS(这里JS只执行循环对比)的消耗比例很低,大约2.5%(在正常监听的时候会有一些操作,比例是会明显增大的),反而是渲染会占据近乎2倍的消耗
其他数量的节点的测试数据
节点数 | 静态总时间 | 静态JS耗时 | 动态总时间 | 动态JS耗时 |
---|---|---|---|---|
100 | 5990ms | 84.3ms(1.40%) | 4020ms | 111.1ms(2.76%) |
500 | 5928ms | 204.2ms(3.44%) | 4760ms | 209.7ms(4.41%) |
1000 | 6007ms | 342.9ms(5.71%) | 5125ms | 342ms(6.67%) |
2000 | 6580ms | 654ms(9.94%) | 7955ms | 1094.9ms(13.76%) |
5000 | 7667ms | 1880.6ms(24.53%) | 6277ms | 823.3ms(13.11%) |
根据上面的表可以看到静态和动态的时间大体是跟着监听的DOM节点的数量成正比的,动态消耗的时间比静态略多可能是由于对多一层调用的堆栈导致,在5000节点的时候,动态渲染消耗的时间反而少了,是由于5000节点同时改变大小,消耗了大量的资源进行重绘,这时候的帧数也开始明显降低
这个图可以看到资源几乎被完全利用了,而且大部分是渲染消耗的
小结
这个方法可以覆盖全部的变动情况,而且可以监听所有的DOM对象,但是在监听的节点数量很多的情况下(500+)的时候,消耗的性能就很多了,在这之前的消耗还是在可接受的范围内(<5%)的,而且一般实际上是没有这么多需要监听的节点的~~~,一般都在100节点以内,性能消耗是在1%内的,是一个完全满足需求的方案,没必要为了一个几乎不会遇到的需求而放弃这个简单有效的方案
接下来介绍一下,一个基于事件实现的监听方案
DOM滚动事件驱动方案
这个方案是通过监听DOM元素的scroll
事件来进行的,在大家可以在这篇文章了解下详情,这里只简单介绍下原理
scroll事件发生的原理
当一个元素的scrollLeft
或scrollTop
属性发生改变的时候,这个元素就会触发scroll
事件,scroll
事件是不会冒泡的
父元素大小变化时内部元素的对scrollLeft和scrollTop的影响
当父元素大小变化时,内部的元素(内容)在一定条件下会影响scrollLeft
和scrollTop
的值,从而触发scroll函数,下面都以高度为例(宽度其实相同)
父元素变大
在一般情况下父元素变大是不会影响内容的
但是当父元素变大的幅度超过了剩余的内容,那么内容会跟着底部向下移动,导致scrollTop
缩小
这样我们就能监听到scroll
事件啦!
父元素变小
放大了的原理理解了话,那么放小来看下
如果是父元素变小,如果内容的大小不变,那么scrollTop
永远都不会发生变化,为了让其发生变化,这里需要让子元素能跟着父元素变化,而且必须变化幅度大于父元素,才能促使父元素由于超过展示范围,去改变scrollTop
当内容的高度是父元素的100%以上时,由于速度比父元素缩小的块,导致父元素必须修改scrollTop
来达到允许的最大的scrollTop
,通过这个原理我们就可以监听到父元素的缩小啦!!!
在代码中最好使用200%及其以上,因为在浏览器中DOM的宽高都是整数的,如果是用200%一下会导致收缩不明显,而会漏掉一部分
height="265" scrolling="no" title="通过scroll监听DOM的缩小" src="//codepen.io/sqchenxiyuan/embed/mzGjqR/?height=265&theme-id=0&default-tab=html,result" allowfullscreen="true">See the Pen 通过scroll监听DOM的缩小 by sqchenxiyuan ( @sqchenxiyuan) on CodePen.放大放小混合
上面两种方法都只能实现一直情况,但是只要混合一下就能实现同时监听放大缩小了,一方变动的同时需要恢复scrollTop
来让另一方也能跟上
性能分析
这个相较于循环比对
方法要复杂的多,但是节约了DOM没有变动时(大部分情况)的计算性能,使用事件驱动是必然性能优化很多的
小结
这个方法虽然很好的解决了循环比对
的性能问题,但是会污染使用者的DOM树结构(添加了额外的东西),而且必须要求DOM能够在下面插入节点,文本(TextNode)、图片(ImageElement)等等这些元素就么法了,同时需要父元素是一个相对元素,不然就不能模拟捕捉父元素的宽高,限制条件还是很多的
封装源码
循环监听
class DomResizeWatcher{