IntersectionObserver
这个 api
可以说是比较火了,以至于近几年无论是面试、工作场景,提到“懒加载”、“虚拟滚动”、“曝光统计”等实现总是少不了它的身影。到底是一个什么样神奇的 api
,到底有没有坑呢?我们接着往下看!
1. 了解 IntersectionObserver
相信 IntersectionObserver
很多同学已经非常熟悉了,说不定已经在日常项目实战中落地使用过了,这里就稍微介绍一下。MDN
是这么介绍这个 api
:提供一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。感兴趣的可以自己到 MDN 中进行相关查阅。
由这句介绍可以了解到,交叉监视器可以异步地检测目标元素与视口的交叉关系。如果没有这个 api
,回想一下我们会如何实现这种需求?
- 监听滚动。
xxx.addEventListener('scroll', () => {})
- 获取目标元素当前的
top
,自身元素的高度height
。 - 一堆麻烦的数学运算来计算当前元素是否处于可视区域。
该说不说,就那一堆数学公式来计算就让笔者感觉很烦恼。于是这个 api
的出现能极大地帮助我们解决这样的问题,应对上面的场景,我们只需要实例一个 IntersectionObserver
,然后用其观测目标元素就能完成,根本不需要再获取元素位置、再进行一堆计算...
废话不多说,直接上一个在线案例,简单实现:目标元素进入视窗 body
的背景色为深色,离开为浅色。核心代码如下,具体效果大家可以在码上掘金中自己玩玩:
const callback = (entries) => {
console.log(entries[0].intersectionRatio)
if (entries[0].intersectionRatio > 0) {
// intersectionRatio 存在交集,背景改为深色
document.body.style.backgroundColor = '#333'
} else {
document.body.style.backgroundColor = '#fff'
}
}
可以发现上述案例如果使用交叉监视器来做会非常简单,极大减轻了原本位置计算给我们开发者带来的不便。一切看起来都是这么美好的 IntersectionObserver
,是否真的这么无懈可击呢?我们接着往后看。
2. 快速滚动 IntersectionObserver
不执行
回想两年前面试小*书三面,面试官看我有实现虚拟列表来优化项目性能,上来便问我 IntersectionObserver
相关内容,那时候年少无知的笔者不知道这个 api
,根本回答不出来,而且虚拟列表的实现也是通过 onScroll
那套去实现的,结果整场面试10分钟左右就结束了...面评是:长期自我实践,缺乏业界视野。
好吧,既然这样那我就学习、找机会实战 IntersectionObserver
呗?终于在一次接手的一个项目中,有一个页面由于选择器初始化加载了大量数据导致交互特别卡顿,于是虚拟列表的场景又再次出现了!笔者立马上号掘金发现了一篇好文:一个简洁、有趣的无限下拉方案。这篇文章思路清晰实现简单,毫无疑问地成为了笔者的方案首选,然后笔者就按照这个文章的思路包装了 el-select
解决了当时的卡顿问题,于是后来也有了笔者的这篇小作文:虚拟列表实战封装el-select。
在笔者成功实战 IntersectionObserver
的一段时间里,还沉浸在其带来的丝滑开发、用户体验的快乐中,突然被找上门来,业务方在使用的过程中发现一个问题:列表只要拖动滚动条就会出现白屏的情况。还有这回事?笔者马上对代码进行检查,最终定位到是 IntersectionObserver
在快速拖动滚动条时没有及时触发回调函数,由于对自己的代码有怀疑,所以笔者也把 一个简洁、有趣的无限下拉方案 的实现源码拉下里本地跑了跑,问题确实存在。接下来,笔者就和大家一起重现这个问题。
为了更清晰的看出来回调在快速拖动滚动条时未执行,笔者在回调中加了一些 console
:
笔者首先将列表滑动到一定高度后,快速的拖动滚动条回到最顶部,大家可以发现出现白屏,且控制台也没有再打印出相应的内容:
其实这个问题很简单就能复现,比如回到本文的码上掘金中,笔者也有打印相关的内容,在我们快速拖动滚动条的时候,会出现不打印的情况:
为了更清晰的复现出问题,笔者建议大家可以适当加大码上掘金滚动区域的高度再快速拖动,或者去下载一下上述文章的源码去试试。笔者在这里只能说,如果不允许用户拖动滚动条,仅仅靠滑动加载内容,采用上述网易团队的方案来实现虚拟列表确实体验非常丝滑非常舒服~可惜可惜,用户是不会轻易放过你的。
3. IntersectionObserver
的异步执行机制
从笔者自己的实践情况来看,IntersectionObserver
确实在上述场景是存在“坑”的,也因此会导致一些问题如白屏...所以如果要直接使用这个 api
目前来讲是存在场景限制的,搞不好就会出现一些异常的页面情况,除非是有采用其他方案来优化这个 api
的不足。
事出必有因,要探究这个问题出现的本质,我们还需要了解一下 IntersectionObserver
的执行机制。首先需要确定的一点就是触发时机,笔者在 W3C 规范文档中发现了这么一段描述:
An Intersection Observer processing step exists as a substep within the "Update the rendering" step, in the HTML Event Loops Processing Model。关键点:其执行阶段位于事件循环 "Update the rendering" 步骤的子步骤中。
事件循环大家都懂,但是 Update the rendering
步骤是个什么鬼?既然不知道,那就接着 google 一下这个步骤呗~于是笔者找到 processing-model
定义的 event-loop
过程:
简单看一下大概就是事件循环的细节实现过程,我们这里大概知道一些节点即可,主要还是要找我们想找的东西。可以看到笔者的截图中,共有 1-6
这么几个点,刚好 Update the rendering
出现在第 7
步(一屏截不完的尴尬...),我们接着来看看:
如图可知,在第 7
步中,有非常多的小步骤,笔者看了看共有 17
个,于是笔者搜了一下 intersectionObserver
发现了如下:
可以看到啊,在 event-loop
规范中,第 7
步 Update the rendering
的第 15
子步骤中运行交叉观察步骤。如果说大家对这些子步骤都没啥概念也没关系,正好第 13
步我们看到了一个熟悉的身影:run the animation frame callbacks
,这不就是我们比较熟悉的 requestAnimationFrame
这个 api
吗?如果还不知道,那 React
中模拟的更新机制的起源 requestidlecallback
总该知道了吧?
所以这里得出一个结论,交叉监视器的异步执行基于 event-loop
,处于 动画帧回调函数requestAnimationFrame
之后,又在 requestidlecallback
之前。
在经过这些资料查询之后,我们可以明确这个 api
的执行跟事件循环相关,那接下来就很好分析本文的问题起源了。经常背八股文都知道大部分设备中浏览器的刷新频率为 60FPS
,大概是 16.6ms
一次,也就是说如果我们拖动滚动条的速度快于这个更新频率 IntersectionObserver
确实是有可能不会执行到的,因此就会出现本文的问题。
基于此,stackoverflow 中的一个回答也阐述了这个问题的产生原因:
如上。我们圈重点的看
- 如果滚动速度比交叉监视器检查的频率快,可能检测不到变化,甚至目标元素可能还没有被渲染就滑走了。
- 根据交叉观察器的规范, IO 的主要目标是检查一个元素是否对人眼可见。
好吧,完成破案,看来 IntersectionObserver
在这种快速拖动滚动条的场景中确实有点小坑~
写在最后
对于一线开发来说,实战经验还是很重要的。每当我们了解一些新的 api
,不去实战一下可能很难发现它的一些坑,笔者一开始搜索这个问题的原因也很难找到相关的说明讲解(可能是我的关键词不行!)虽然说 IntersectionObserver
在一些场景中有所不足,但它的能力对于开发者来说真的太好了,我只想说用了一次之后就感觉以后再需要做类似的东西可以抛弃 onScroll
那套了。而且针对这个缺陷,应该还是有相应的解决方案的,只是笔者还没有去调研、尝试...(记得当时自己的第一反应就是把滚动条隐藏起来,老子不让你拖还不行!)好吧,本文就到这里了,感谢你的阅读,如果后续还有相关的实战和优化方案,笔者会再来写一篇,再会!
参考文献
Intersection Observer fails sometimes when i scroll fast