JS监控DOM大小的变化_getdomsize js_sqchenxiyuan的博客-程序员秘密

技术标签: web前端学习笔记  JS  resize  

JS监控DOM大小的变化

由于浏览器原生不自带DOM元素的resize事件,一直想弄一个JS监控DOM元素大小变化的功能,之前想过很多方法都各有优缺点,但是最近发现了一个很不错的方法,虽然以前有设想过但一直没有时间去实现,知道看到有人实现了和我的设想相同的一种比较优良的监听方法,这篇文章将主要讲述两种监听方法,以及优劣分析

DOM大小的变化的诱因

DOM大小的决定因素

在寻找如何监听DOM大小变化的解决办法之前,这里先列举DOM元素宽高的数值的情况:

  1. 自己设置的固定宽高,如样式设置style="width:100px;height:100px"
  2. 由上层元素决定,如样式设置style="width:50%;height:50%"
  3. 有下层元素决定,内容撑大

宽高各自独立,可以混合出现

DOM大小改变的原因

知道了决定的DOM大小的因素的3种情况,可以很快的列出大部分导致DOM大小变化的情况:

  1. 元素修改自己的宽高样式

    元素修改自己的宽高属性,是最简单最直接也是最常见的DOM大小变化的情况

  2. 当元素大小由上层元素(包括窗口大小)决定,上层元素大小发生变动时

  3. 当袁术大小由下层元素决定,下层元素大小发生变动时

上面三种是最常见也是最容易想到的,仔细思考还存在其他情况

  1. 元素的class的内容改变

    当元素的class的内容的宽高变化时,元素是不能监听到class内容变化的,虽然这个操作很不好,但是在切换页面皮肤时确实可能存在这样的问题

  2. 过渡/动画

    当元素设置了过渡/动画效果时,元素的宽高是可能在每一帧的出现变化的

这些因素也是最难以监听和获取的可以叫"隐式变化",之前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事件发生的原理

当一个元素的scrollLeftscrollTop属性发生改变的时候,这个元素就会触发scroll事件,scroll事件是不会冒泡的

父元素大小变化时内部元素的对scrollLeft和scrollTop的影响

当父元素大小变化时,内部的元素(内容)在一定条件下会影响scrollLeftscrollTop的值,从而触发scroll函数,下面都以高度为例(宽度其实相同)

父元素变大

在一般情况下父元素变大是不会影响内容的

但是当父元素变大的幅度超过了剩余的内容,那么内容会跟着底部向下移动,导致scrollTop缩小

这样我们就能监听到scroll事件啦!

height="265" scrolling="no" title="通过scroll监听DOM变大" src="//codepen.io/sqchenxiyuan/embed/gBdKjp/?height=265&theme-id=0&default-tab=html,result" allowfullscreen="true">See the Pen 通过scroll监听DOM变大 by sqchenxiyuan ( @sqchenxiyuan) on CodePen.

父元素变小

放大了的原理理解了话,那么放小来看下

如果是父元素变小,如果内容的大小不变,那么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来让另一方也能跟上

height="297" scrolling="no" title="通过Scroll监听元素大小变化" src="//codepen.io/sqchenxiyuan/embed/GYXXoQ/?height=297&theme-id=0&default-tab=html,result" allowfullscreen="true">See the Pen 通过Scroll监听元素大小变化 by sqchenxiyuan ( @sqchenxiyuan) on CodePen.

性能分析

这个相较于循环比对方法要复杂的多,但是节约了DOM没有变动时(大部分情况)的计算性能,使用事件驱动是必然性能优化很多的

小结

这个方法虽然很好的解决了循环比对的性能问题,但是会污染使用者的DOM树结构(添加了额外的东西),而且必须要求DOM能够在下面插入节点,文本(TextNode)、图片(ImageElement)等等这些元素就么法了,同时需要父元素是一个相对元素,不然就不能模拟捕捉父元素的宽高,限制条件还是很多的

封装源码

循环监听

class DomResizeWatcher{
    
    constructor(){
    
        this.datas = new Map()

        this._init()
    }

    _init(){
    
        this._check()
    }

    _check(){
    
        Array.from(this.datas.values()).forEach(data => {
    
            data.check()
        })

        requestAnimationFrame(this._check.bind(this))
    }

    addResizeEventListener(dom, fun){
    
        let data = this.datas.get(dom)
        if(!data){
    
            data = new DomResizeWatcherData(dom)
            this.datas.set(dom, data)
        }
        data.addResizeEventListener(fun)
    }

    removeResizeEventListener(dom, fun){
    
        let data = this.datas.get(dom)
        if(!data) return

        data.removeResizeEventListener(fun)

        if(data.getFunCount() > 0) return

        this.datas.delete(dom)
    }
}

class DomResizeWatcherData{
    
    constructor(dom){
    
        this.dom = dom
        this.funs = new Set()
        this.size = this._getDomSize()
    }

    _getDomSize(){
    
        let height = this.dom.clientHeight
        let width = this.dom.clientWidth

        return {
    
            height,
            width
        }
    }

    _trigger(){
    
        let dom = this.dom
        this.funs.forEach(fun => {
    
            fun.apply(dom)
        })
    }

    getFunCount(){
    
        return this.funs.size
    }

    addResizeEventListener(fun){
    
        this.funs.add(fun)
    }

    removeResizeEventListener(fun){
    
        this.funs.delete(fun)
    }

    check(){
    
        let size = this._getDomSize()
        if(size.height !== this.size.height 
            || size.width !== this.size.width){
    
            this._trigger()
            this.size = size
        }
    }
}

scroll事件驱动

class FrameCounter{
    
    constructor(){
    
        this.time = 0
        this._run = false
    }

    _runFun(){
    
        if(this._run){
    
            this.time++
            requestAnimationFrame(this._runFun.bind(this))
        }
    }

    start(){
    
        this._run = true
        this._runFun()
    }

    stop(){
    
        this._run = false
    }
}

class DOMResizeWatcher{
    
    constructor(){
    
        this.datas = new Map()
        this.timer = new FrameCounter()
        this.timer.start()
    }

    addResizeEventListener(dom, fun){
    
        let data = this.datas.get(dom)
        if(!data){
    
            data = new DOMResizeWatcherData(dom, this.timer)
            this.datas.set(dom, data)
        }
        data.addResizeEventListener(fun)
    }

    removeResizeEventListener(dom, fun){
    
        let data = this.datas.get(dom)
        if(!data) return
        
        if(fun){
    
            data.removeResizeEventListener(fun)

            if(data.getFunCount() > 0) return
        }

        data.destory()
        this.datas.delete(dom)
    }
}

class DOMResizeWatcherData{
    

    constructor(dom, timer){
    
        this.dom = dom
        this.funs = new Set()
        this.trigged = false
        this.timer = timer

        this.insideBigEl = null
        this.insideSmallEl = null

        this._init()
    }

    _init(){
    
        //监听变大的DOM
        let insideBig = document.createElement("div")
        insideBig.style = "position: absolute;top:0;left: 0;bottom: 0;right: 0;overflow: hidden;visibility: hidden;z-index:-1"
        insideBig.innerHTML = `<div style="width:${
      DOMResizeWatcherData.bigNumber}px;height:${
      DOMResizeWatcherData.bigNumber}px"></div>`

        //监听变小的DOM
        let insideSmall = document.createElement("div")
        insideSmall.style = "position: absolute;top:0;left: 0;bottom: 0;right: 0;overflow: hidden;visibility: hidden;z-index:-1"
        insideSmall.innerHTML = `<div style="width:300%;height:300%"></div>`

        this.insideBigEl = insideBig
        this.insideSmallEl = insideSmall

        try{
    
            this.dom.appendChild(insideBig)
            this.dom.appendChild(insideSmall)
        } catch(e) {
    
            throw new Error("DOMElement can't appendChild! try another way!")
        }
        
        insideSmall.scrollTop = DOMResizeWatcherData.bigNumber
        insideSmall.scrollLeft = DOMResizeWatcherData.bigNumber
        insideBig.scrollTop = DOMResizeWatcherData.bigNumber
        insideBig.scrollLeft = DOMResizeWatcherData.bigNumber

        insideBig.addEventListener("scroll", _ => {
    
            insideSmall.scrollTop = DOMResizeWatcherData.bigNumber
            insideSmall.scrollLeft = DOMResizeWatcherData.bigNumber
            this._trigger()
        })
        insideSmall.addEventListener("scroll", _ => {
    
            insideBig.scrollTop = DOMResizeWatcherData.bigNumber
            insideBig.scrollLeft = DOMResizeWatcherData.bigNumber
            this._trigger()
        })
    }

    _trigger(){
    
        if(this.triggertime === this.timer.time) return
        this.triggertime = this.timer.time
        
        let dom = this.dom
        this.funs.forEach(fun => {
    
            fun.apply(dom)
        })
    }

    getFunCount(){
    
        return this.funs.size
    }

    addResizeEventListener(fun){
    
        this.funs.add(fun)
    }

    removeResizeEventListener(fun){
    
        this.funs.delete(fun)
    }

    destory(){
    
        try{
    
            this.dom.removeChild(this.insideBigEl)
            this.dom.removeChild(this.insideSmallEl)
        } catch(e){
    }
    }
}
DOMResizeWatcherData.bigNumber = 9999999

参考资料

scrolling官方规范

巧妙监测元素尺寸变化

END

2018-10-23 完成

2018-10-17 立项

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ddkddkchen/article/details/83343512

智能推荐

CCF A类 B类 C类 中国计算机学会推荐中文科技期刊目录【迷惑了好久】_ccfa类期刊_榆枋不止的博客-程序员秘密

引言老是听别人说毕业条件,CCF C类,B类及以上的话,确不清楚他们具体是哪些一绝CCF官网2020-07-02 的推荐目录A 类序号期刊名称主办单位网址1软件学报中国科学院软件研究所http://www.jos.org.cn中国计算机学会2计算机学报中国计算机学会http://cjc.ict.ac.cn中国科学院计算技术研究所3中国科学:信息科学中国科学院http://infocn.scichina.com国家

python定时启动程序方法_python 定时启动_梧桐凰的博客-程序员秘密

第一种办法是最简单又最暴力。那就是在一个死循环中,使用线程睡眠函数 sleep()。缺点:占CPU内存,死循环 + 阻塞线程def doSth(): # 把爬虫程序放在这个类里 print(u'这个程序要开始疯狂的运转啦')# 一般网站都是1:00点更新数据,所以每天凌晨一点启动def main(h=1, m=0): while True: n...

DSP做的modbus rtu 主从站协议_dspmodbus程序_JiaoCL的博客-程序员秘密

DSP做的modbus rtu 主从站协议 2012-05-03 7:36 modbus RTU 协议:先后做1,3,15,16四个命令码,有主从站协议解析。 原文链接:http://blog.21ic.com/user1/3338/archives/2008/47236.html //————————————————————————— // FILE: ModbusRTU.c //

服务器上搭建jupyter notebook运行环境_躺着的懒货的博客-程序员秘密

如何在云服务器上搭建jupyter notebook运行环境(解决配置完成后,登陆密码错误的问题)最近在学习机器学习,于是在云端搭建一个jupyter notebook的运行环境。由于网上已有详细的各种资料可供参考,因此本文的目的是整合一个完整的搭建思路及纠正博客中的一些问题。搭建环境主要分为两步:安装anaconda安装jupyter包及配置环境Linux服务器(无桌面)安装anaconda由于...

Oracle中的层次查询详解_weixin_30955341的博客-程序员秘密

1 语法格式select [level], column, expr... from table  [where condition]  start with condition  connect by [prior column1= column2 |  column1 = prior column2];2 语法解释层次查询是通过start with...

js 获取浏览器的url中的参数_善林的博客-程序员秘密

用下面这个函数: /** * 获取到本页浏览器的url参数中的值. * 注意:直接获取到的url实际上是浏览器encode编码以后的,需要用decodeuri解码才是中文 * @param key url参数中的键 * @returns {*} 返回该key对应的值 */function getQueryString(key){ var reg = new RegExp

随便推点

将实朴主流程移植到九安的过程中遇到的问题及解决方法_weixin_30335575的博客-程序员秘密

  经过一个星期的努力,已完成将实朴主流程移植到九安的工作任务,期间遇到了不少问题。由于是新入职的新手,遇到的问题都是第一次接触到的,通过向老同事虚心讨教,基本上都找到了解决的方法。现将我遇到的问题及解决的方法大致罗列如下:  一、进入StarLims时报错  ① 报错信息:ORA-01033:Oracleinitialization or shutdown in progress ...

新一代直播传输协议SRT_清流_46837673的博客-程序员秘密

SRT协议是基于UDT的传输协议,保留了UDT的核心思想和机制,抗丢包能力强,适用于复杂的网络。在LiveVideoStack线上分享中,新浪音视频架构师 施维对SRT协议的原理、优缺点特性以及在流媒体中的应用进行了详细解析。文 / 施维整理 / LiveVideoStack视频回放 https://www2.tutormeetplus.com/v2/render/playback?mode...

zabbix3.2.6安装_weixin_33851604的博客-程序员秘密

zabbix的安装方式有很多,这次用个比较简单的,通过阿里云进行安装。我的环境是centos7.3 1.3.10.0-514.26.2.el7.x86_641.由于默认安装的PHP是 5.4,漏洞较多,所以先把PHP5.6 安装上,安装连接http://adamcrab.blog.51cto.com/842470/19465162.使用阿里yum源[[email protected]

stack, queue, dequeue, heap和priority_queue_stack heap queue_CS_ChenLI的博客-程序员秘密

1. 名词解释stack 栈queue 队列dequeue (deque,全名double-ended queue)是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。heap 堆priority_queue 优先队列2. STL中Stack用法stack 模板类的定义在头文

解决kali apt-get 提示无法安全的用该源进行更新_linyiwen999的博客-程序员秘密

**解决kali-linux 更新源无法使用的问题kali版本为:linux 4.19.0-kali3-amd64版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。自学渗透测试过程中一直遇到问题,一直也是百度,经常看博客解决问题,为感谢大家的无私,我决定也写一些关于市场上还没很好解决的问题。按照流程修改前请确认自己用的版本号:#un...

Springboot(一)SpringBoot介绍_佐杰的博客-程序员秘密

Spring boot介绍官方文档:Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.Spring Boot使创建独立的、基于生产级Spring的应用程序变得很容易,你可以“直接运行”这些应用程序。We tak...

推荐文章

热门文章

相关标签