,突然有新想法怎么办?窗外阳光明媚,下了一个星期的雨,终于放晴了,所以,显然,代码已经准备好值得这个周末了,嘿嘿嘿。1、会达到什么样的效果?一般文档页数都比较长。为了方便定位,会根据标题生成导航。比如我的博文:Ant.designorVue.js文档页面有类似的功能:我之前写了一个jQuery插件来实现这个功能,详见:《jQuery小插件titleNav.js》。后来自己也写原生JS,写了这个功能。IE9+支持,监听滚动事件,判断没有title元素和滚动窗口的位置,位置最近的会高亮显示。这是本博客目前使用的代码。滚动时实时计算每个title元素的位置,代码比较啰嗦,性能一般。今天在做Mobilebone的新文档,需要实现类似的交互效果。我想知道是否有一种更简单的方法可以在不进行实时计算的情况下实现突出显示哪个导航元素。于是我在脑海里遍历了一遍自己的知识储备,然后一个API浮出水面。这个API就是IntersectionObserver,可以观察元素和表单的交集。看来有戏了。2.什么是IntersectionObserver?Web领域有很多Observer,俗称观察者,可以对网页的某些交互变化进行实时反馈。比如MutationObserver,可以观察DOM元素的增删改查和属性变化,可以在《浅谈JSDOM变化的监控、检测和应用》一文中找到;再比如ResizeObserver,可以观察元素的大小变化,可以参考《JSResizeObserverAPI介绍》一文。这里要介绍的IntersectionObserver是观察元素和窗体的相交状态,非常适合在滚动相关的交互事件中使用。比如图片的懒加载效果,或者无限滚动加载效果等等,V1版本规范的兼容性还是很好的,移动端几乎可以放心使用。嗯,2年后可以放心使用,iOS的兼容性略显落后。有关详细信息,请参见下面的屏幕截图。套路很简单,如下:varzxxObserver=newIntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){//entry.target元素已经进入区域}});});//观察元素1,2,...zxxObserver.observe(ele1);zxxObserver.observe(ele2);...用文字解释一下这两个步骤:定义元素交叉后要做什么;需要观察那些元素;在实际开发中,主要工作是对entries.forEach的代码进行处理。其中,entry对象包括以下参数:entry.boundingClientRect为当前观察元素的矩形区域,top/right/bottom/left属性可以获得此时相对于视口的距离,width/height属性包含大小。此属性与API方法Element.getBoundingClientRect()非常相似。entry.intersectionRatio当前元素相交的比率。如果要比的很详细,需要在IntersectionObserver()函数的第二个可选参数中设置thresholds参数,即设置触发交叉事件的阈值。entry.intersectionRect与视口相交的矩形的大小。entry.isIntersecting如果为true,则表示元素从视口外进入视口。entry.rootBounds表单根元素的矩形区域对象。entry.target当前相交的元素。entry.time当前时间戳。本例中主要使用entry.isIntersecting表示当前元素与目标区域相交。3.具体实现过程记录假设我们需要观察的title元素都是
元素,代码可以这样处理:varzxxObserver=newIntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){//active()是自定义高亮方法entry.target.active();}});});//观察标??题元素document.querySelectorAll('h3').forEach(函数(ele){zxxObserver.observe(ele);});这样,title元素在进入视口时就会高亮显示。但是,上面代码实现的最终效果有些慢。比如一屏同时有多个title元素,中间title元素的高亮会被跳过(一次只能高亮一个元素)。最好在title元素进入屏幕中间区域时触发相交检测。有一种方法,可以使用IntersectionObserver()函数的第二个可选参数。新的IntersectionObserver(回调,选项);这里是option可选参数。支持以下属性值:root是用于交叉检测的根元素,默认是浏览器窗口元素。rootMargin检测区域的偏移量。支持1-4个值,与margin属性表示的方向完全一致。但是正负值的含义是不一样的,今天就被这个骗了。比如一个元素设置margin:100px,它自己的区域大小只会减少100px,但是rootMargin参数不同。正值增加视口的检测区域,负值减小它。thresholds触发回调函数的执行阈值,是一个数组,比如[0.00,0.01,0.02,...,0.99,1.00],相交区域从1%到100%都会触发回调,默认只会在100%时触发一次。该参数支持函数类型,返回对应的数组即可。回到本文的案例,那么如果想要交集检测区域在浏览器窗口的中间部分,可以使用rootMargin参数。相关代码如下:varzxxObserver=newIntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){entry.target.active();}});},{rootMargin:'-33%0%-33%0%'});取得的效果。结果嘿嘿嘿,发现第一个navigation元素无法高亮显示,因为上面没有足够的空间让第一个title元素进入页面中间1/3。底部的last1nav元素也有类似的问题。我整个人都不好了,想知道有没有什么办法可以检测出第一个元素和最后一个元素的位置,然后专门处理,耗费了大量脑细胞后发现不可以,并且通过构建一些隐藏元素来扩大标题元素区域的方法是不可复制的。或者在滚动容器中创建一个等高的0宽元素,配合thresholds参数,这样就可以实时感知滚动行为,从而可以进行非常细致的处理,但是这样做本质上和滚动事件。不愿意,不想用scroll事件。最后……我还是妥协了,用scroll事件实现了一些功能。和传统的滚动交互实现是一样的。要使容器滚动到顶部,必须突出显示第一个元素,然后滚动到底部。是最后高亮的元素,相关代码如下:window.addEventListener('scroll',function(){varroot=document.scrollingElement;if(root.scrollTop==0){elements[0].active();}elseif(root.scrollTop+root.clientHeight>root.scrollHeight-1){elements[elements.length-1].active();}});然而,事情并没有想象中的那么顺利,虽然scrolling开始和结束位置的高亮显示。触发了Heading2的交集行为,所以Heading2不能一直高亮。然后我通过设置冗余标志解决了这个问题。基本上体验还可以,就是偶尔还是会出现跳题的问题,悲催。。。看了看时间,已经是凌晨1点了,答应leader在家12点睡觉:30,所以先戴上,关掉电脑睡觉。时间到了今天,2010年星期一,2020年11月的最后一天,下班回到家,打开昨天写的代码,心想这段歪歪扭扭的代码和不得不用的滚动事件代码能不能被优化,有没有办法完全通过交叉点检测来实现这一点。刷微博喂鱼,看似流浪,实则寻找灵感。我问自己一个问题,为什么我后来折腾了那么多鬼东西?这个问题很容易回答:“一开始应该高亮Title1,但是如果Title2高亮了,那么默认两者都应该在屏幕上,因为一次只能高亮一个,所以,后面的标题2被突出显示。”就在这时,我的脑海里突然冒出了一个火花,这只小手,像是失去了控制一般,又恢复了原状。设置rootMargin参数的state,然后在entries.forEach中间加一个小的reverse(),如下截图所示:然后又体验了一遍,我擦。这差不了多少,前卷基本符合预期,至少在进入时,或滚动到顶部时,第一个标题元素会突出显示。但是,有一个新的问题。当Title1在屏幕外,而Title2在屏幕上时,Title2不会高亮,因为Title2一直在屏幕上;也就是说,当Title1和Title2同时在屏幕上时,当Title1滚动离开时,Title2不会触发entry.isIntersecting,因为IntersectionObserverAPI中的回调只有在交点变化时才会触发。这个问题很好解决,我立马灵机一动,在元素移出屏幕的时候做一个高亮处理。于是核心代码就变成了这样: