当前位置: 首页 > 科技观察

如何监控页面DOM变化并高效响应

时间:2023-03-20 21:20:14 科技观察

最近在做chrome插件开发。既然是插件,难免不能对已有的页面做一些控制,比如事件监听,调整布局,增删改查DOM元素等等,其中一个需求比较有意思,所以我整理一下,复习一下涉及到的知识点。需求是这样的:在一个包含懒加载资源和动态DOM元素的页面中,需要给页面上已经存在的元素加上属性显示标签。从DOM变化事件监控开始,首先假设大家已经知道了JavaScript中事件的发生阶段(capture-***-bubble),附上一张图拍下这段内容,我们直接进入寻找解决方案的过程。使用DOM事件流在DOM树中分派的事件的图形表示一开始,我一直在寻找涉及窗口状态更改的事件。找了一圈,发现离onload事件最近,再看看MDN。事件定义:当一个资源及其依赖的资源完成加载时,加载事件被触发。如何理解资源及其依赖的资源加载完毕?简单的说,如果一个页面涉及到图片资源,那么onload事件就会在页面完全加载完成后被触发(包括图片、css文件等)。一个简单的监听事件用JavaScript写应该是这样的(注意不同环境下load和onload的区别):});//window.onload=function(){console.log("Allresourcesfinishedloading!");};//HTML//jQuery$(window).on("load",handler)当然,说到onload事件,jQuery中还有一个类似的事件不得不提——就绪事件。此事件在jQuery中定义如下:指定一个函数,当DOM完全加载时执行。需要知道的是,jQuery定义的ready事件本质上是为DOMContentLoaded事件设计的,所以当我们讲加载时,应该区分的事件其实是onload(接口UIEvent)和DOMContentLoaded(接口Event),MDN描述DOMContentLoaded是这样的:当初始HTML文档完全加载和解析时,DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架完成加载。不同的事件加载应该只用于检测完全加载的页面。所以你可以知道,当一个页面加载时,应该先触发DOMContentLoaded,然后onload。相似事件和不同点包括以下几类:DOMContentLoaded:当初始HTML文档完全加载和解析时,触发DOMContentLoaded事件,无需等待样式表、图像和子框架加载;readystatechange:文档的Document.readyState属性描述文档的加载状态。当这个状态改变时,事件就会被触发;load:当一个资源及其依赖的资源加载完成后,会触发load事件;beforeunload:当浏览器窗口、文档或其资源即将被卸载时,会触发beforeunload事件。unload:卸载文档或子资源时会触发unload事件。细心的你会发现,上面介绍事件的时候,提到了UIEvent和Event。这是什么?这些是事件——可以被JavaScript检测到的行为。其他的事件接口还有KeyboardEvent/VRDisplayEvent(是的,没错,这就是你感兴趣和熟悉的VR)等;它们分为以下几类:UI事件、焦点事件、鼠标和滚轮事件、键盘和文本事件、复合事件、更改事件、HTML5事件、设备事件、触摸和手势事件,但这有点乱,一些它们是DOM3定义的事件,有的单独列出来,如果你觉得熟悉,你会发现这是JavaScript高级编程中的一种叙述方式。在我看来,理解这些事件可以根据DOM3事件和其他事件来区分:其中,DOM3级别的事件规定了以下类事件——UI事件、焦点事件、鼠标事件、滚轮事件、文本事件、键盘事件、复合事件事件、更改事件、更改名称事件;其余如HTML5事件可以单独理解。开头提到的Event,作为一个主接口,是很多事件的实现父类。相关的WebAPI接口可以在这里找到,可以看到里面有很多Event的话。好了,事件说了这么多,还是没有解决开头提出的问题。监听页面中动态生成的元素怎么样?考虑到动态生成的元素需要通过网络请求获取资源,是否可以监听所有的HTTP请求呢?查看jQuery文档可知,每当Ajax请求完成时,jQuery都会触发ajaxComplete事件。此时,将使用.ajaxComplete()方法注册并执行所有处理程序。但是谁能保证所有的ajax都会远离jQuery?所以你应该在改变事件中做出选择。我们来看看DOM2定义的以下变化事件:DOMSubtreeModified:当DOM结构发生任何变化时。该事件在其他事件触发后触发;DOMNodeInserted:当一个节点作为子节点插入到另一个节点时触发;DOMNodeRemoved:当节点从其父节点中移除时触发;DOMNodeInsertedIntoDocument:直接插入节点时在插入文档或通过子树间接插入文档后触发。该事件在DOMNodeInserted之后触发;DOMNodeRemovedFromDocument:在直接或通过子树间接从文档中删除节点之前触发。该事件在DOMNodeRemoved之后触发;DOMAttrModified:属性修改后触发;DOMCharacterDataModified:当文本节点的值改变时触发;所以,使用DOMSubtreeModified似乎是正确的。师兄提醒我使用MutationObserver,于是我发现了新大陆。MDN是这样描述MutationObserver的:MutationObserver为开发者提供了在一系列DOM树发生变化时做出适当响应的能力。此API旨在取代DOM3事件规范中引入的Mutation事件。DOM3事件规范中的Mutation事件可以简单的看成是DOM2事件规范中定义的Mutation事件的扩展,不过这些都不重要,因为它们会被MutationObserver取代。好了,下面详细介绍一下MutationObserver。《Mutation Observer API》一文中比较详细的介绍了MutationObserver的使用方法,所以我挑几个可以直接解决我们需求的点。既然我们要监听DOM的变化,那么我们来看看Observer的作用:它等待所有的脚本任务完成后再运行,也就是使用了异步的方式。它将DOM变化记录封装成一个数组进行处理,而不是一个一个地处理DOM变化。它可以观察DOM中发生的所有类型的变化,也可以观察某些类型的变化。MutationObserver的构造函数比较简单,传入一个回调函数即可(回调函数接受两个参数,第一个是变化数组,第二个是观察者实例):letobserver=newMutationObserver(callback);观察者实例使用observe方法进行监听,disconnect方法停止监听,takeRecords方法清除变化记录。letarticle=document.body;letoptions={'childList':true,'attributes':true};observer.observe(文章,选项);observe方法中的第一个参数是要观察的变化的DOM元素,第二个参数接收要观察的变化类型(子节点变化和属性变化)。变化类型包括以下几种:childList:子节点的变化。attributes:对属性的更改。characterData:节点内容或节点文本的变化。子树:对所有后代节点的更改。您想要观察哪种类型的变化,请在选项对象中将其值指定为true。需要注意的是,如果设置观察子树的变化,必须同时指定childList、attributes、characterData中的一项或多项。disconnect方法和takeRecords方法可以不传入参数直接调用。好了,DOM变化的监听就完成了,刷新代码看看效果,因为页面是由很多动态生成的产品组成的,那我应该在body上加上变化监听,所以选项应该这样设置:varoptions={'attributes':true,'subtree':true}嗯?页面稍微下拉一点,观察者就被触发了几十次?DOM怎么受得了?查看页面的变更记录后发现,每一个新资源的底层都会调用Node.insertBefore()方法引入。。。说说JavaScript中的节流/节流功能。现在遇到的一个麻烦就是DOM变化太频繁了。监控每一个变化太耗费资源了。一个简单的解决方案是放弃监听,使用setInterval方法定时执行更新逻辑。是的,虽然该方法有点原始,但它的性能相对于Observer来说是“提升”了很多。这时,学长的又一助攻来了:“使用拦截功能”。记得之前看《JavaScript 语言精粹》的时候,看到setTimeout方法是通过调用自身来解决setInteval频繁执行导致资源消耗的现象。不知道这两者有没有关系。上网一搜发现有两个“流量函数”。需求出自这里:在前端开发中,页面有时会绑定一些频繁触发的事件,比如scroll或resize事件,也就是说在正常操作中,绑定的程序会被多次调用,但有时javascript需要有一个很多事情要处理,频繁的离开会导致性能下降,页面卡顿,甚至浏览器崩溃。如果我们复用setTimeout和clearTimeout方法,我们似乎可以解决这种频繁触发的执行。每次触发事件,我先判断当前是否有setTimeout定时器。如果是,我们先清除它,然后创建一个新的setTimeout计时器来延迟我的响应行为。这听起来不错,因为我们并不是每次都立即执行我们的响应,我们可以在频繁触发的过程中让响应函数一直存在(并且只有一个)。除了一些延迟响应外,它没有任何问题。是的,这就是拦截功能(debounce),有一篇博客用这个小故事来介绍它:图像的类比是一个橡皮球。如果用手指握住橡皮球,它会一直受力,不松手就不会反弹。debounce的重点是idle区间。在我的业务中,在观察者实例中调用下面写的拦截函数就可以了将内容传递出去的闭包returnfunction(){if(timer){//清除计时器clearTimeout(timer);}//设置一个新的计时器timeout);}}当然,它解决了自己的问题,但是还有一个概念没有提到——“节流功能”。同一篇博文还用了一个例子来说明:形象比喻是水龙头或机关枪,你可以控制它的流量或频率。throttle的重点是连续执行之间的时间。functionthrottling的原理也很简单,还是一个定时器。当我触发一个时间的时候,先setTimout让这个事件延迟执行一段时间,如果在这个时间间隔内触发了另一个事件,那么我们就把原来的定时器清空,然后setTimeout一个新的定时器延迟执行一段时间。functionthrottling的出发点是防止一个function执行的太频繁,减少一些过快的调用throttle。这里用AlloyTeam的节流代码实现解释一下://参数同上newDate();//清除定时器clearTimeout(timer);//函数初始化判断if(!t_start){t_start=t_curr;}//超时(指定时间间隔)判断if(t_curr-t_start>=mustRunDelay){fn.apply(context,args);t_start=t_curr;}else{timer=setTimeout(function(){fn.apply(context,args);},delay);}};};当然这里AlloyTeam的文章会说的节流功能,作为V1.0版本的节流功能,你也可以这么想。毕竟,设置必须触发执行的时间间隔(即mustRunDelay函数)可以防止拦截函数在“疯狂事件”的情况下死循环。一旦Observer与拦截功能结合,问题就解决了嘿嘿。当然,坑还是很多的,下次开篇再说吧。参考https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoadedhttps://developer.mozilla.org/zh-CN/docs/Web/Events/loadhttps://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListenerhttp://www.cnblogs.com/fsjohnhuang/p/4147810.htmlhttp://www.alloyteam.com/2012/11/javascript-throttle/