目录

  1. A Primer on Element Resize
  2. overflow & underflow
  3. 大大大
  4. 小一点
  5. bomb!撞在一起
  6. 你看的见我,又看不见我了,偷偷摸摸搞点事情
  7. ResizeObserver
  8. 还有别的办法么?
  9. 结语
  10. Refs

A Primer on Element Resize

监听元素尺寸的变化一直以来,都是一个很常见的需求,但是又不那么容易去实现。因为浏览器都有实现针对窗口变化的监听,而却没有针对元素变化的监听。这个常常发生在一些内部元素大小变化的情况。

比如飞书 Admin 的管理页面,当左侧侧边栏收起或者展开的时候,右侧的宽度会发生变化,而浏览器的窗口并没有变化。

如果我们从实际角度出发,监听元素的变动其实大部分是为了监听因为窗口变化而导致的大小变化,此时最简单的方案就是直接监听浏览器窗口的 resize 事件,这个就不细说了。

其次针对上文说道的情况,社区有很多的实现方案,但很多基于 JQuery 实现的,而且性能较差,因为是使用计数器去拉取元素宽度。不过会有很多优化方案,比如当点击某些可能会导致宽度发生变化的时候才启动定时器去检查,并且检查一段时间发现没有变化的时候,就停止检查。但是不管怎样,性能都是不太好的。

overflow & underflow

于是,社区中有另外一种检测方案,那就是基于事件检测,用的就是 overflowunderflow 事件。不知道这两个事件是什么的,可以去看 MDN 或者这篇博客

简单来讲,就是当内容超出外部容器的时候,会触发 overflow 事件,当内容又小于容器宽度的时候,会触发 underflow 事件。

那怎么检测呢?也很简单,先从扩大变化检测说起

假设有这样一个容器,长度宽度为 100px,内部有一个元素,长宽分别为 101px,也就是比父容器各大 1px。那么如果你将外部容器扩大的一瞬间,长宽至少会增加一个像素,也就是说至少比内部元素大,≥ 101px,此时会触发 underflow 事件。扩大检测就实现完成了,在检测完成之后重置内部元素大小依旧比外部容器大即可。

同理缩小检测也很简单,外部容器不变,内部元素宽度和父容器一直,当缩小的时候,父容器肯定会小于内部元素,于是触发 overflow。缩小检测也完成了,再重置宽度即可。

看似一切美好,但实际上,这两个事件的兼容到现在都巨差无比。但别担心,还有别的办法

大大大

我们现在考虑下扩大的时候,如果父容器有滚动条,有一样东西是会发生变化的。那就是可滚动区域的大小——这不是废话么。别急,先想想可滚动区域变化的话,什么也会跟着变化呢?

没错,就是父元素的 scrollTopscrollLeft。具体怎么讲呢,这边我们先考虑 scrollTop 的情况。

假设有这样一个容器(绿色),高宽为 100px,内部有一个元素(红色),高 200px,宽 100px,也就是比容器固定大 100px。容器滚动到最底部,此时容器的 scrollTop: 100px,如左图所示。如果此时容器扩大 10px,很明显随着滚动区域的变大,scrollTop 也会自动变化成 90px,如右图。

那么问题来了,scrollTop 怎么监听呢?

神奇就神奇在这里,因为元素尺寸的变化而导致 scrollTop 的变化竟然会触发容器的 scroll 事件,而且这个事件触发还是跨平台的,兼容性甚至可以下探到 ie7

小一点

这个时候你会发现,想要变小一点,那么问题就麻烦了。你会发现上面那个方式,你不管怎么搞,都无法让他在缩小的时候触发 scrollTop 的改变。

但是换个思路来考虑,你会发现另外一种让 scrollTop 发生改变的情况。依旧从上面那个方式来讲,如果容器缩小了 10px,那么内容至少要缩小 11px 才能导致滚动发生变化,那么我们会想到什么呢?

百分比对不对?这里假设设置内容高度为 200%,就可以做到当容器缩小 10px 的时候,内部元素可以缩小 20px,也就是 200px(100px 200%) ⇒ 180px(90px 200%)。如图所示

bomb!撞在一起

把上面两个结合起来,就可以实现监听 resize 事件了。但实际上使用需要处理一些样式问题,不过这个问题很简单,不单独说了。

如果前面你能看懂,那么到这里你心中有没有考虑一个情况,就是在重置的时候,会导致 scrollTop/Left 又发生了修改,从而又导致的 scroll 事件的触发,而这个怎么消除呢?

有这么两种办法:

  1. 通过检测元素是否发生变动,如果有变动那么就是因为元素缩放导致的,而如果没有缩放,那么就是通过重置导致的。

  2. 通过 raf,在同一帧内,先触发因为元素变动导致的 scroll,再触发因为重置导致的事件,然后将对事件的处理统一推迟到下一阵开始的时间。从而消除循环事件

你看的见我,又看不见我了,偷偷摸摸搞点事情

众所周知,当你给一个元素设置 display:none; 的时候,元素不会渲染,同理也不会监听外部容器尺寸的变化。那么这就给了被监听元素可乘之机,他可以让 DOM 先隐藏,然后再改变大小,再显示 DOM,此时尺寸就乱了,于是就无法很好的监听 resize 了。

不过,也不是没有办法。

当 DOM 显示在浏览器的时候,会执行一个操作——播放动画

而动画事件是可以被监听到的,于是骚操作来了,给 DOM 元素添加一个 1ms 的动画,然后监听动画开始的事件,然后重置并检测大小变化。

ResizeObserver

这么通用的需求,怎么能逃过 w3c 的双眼,于是就推出了 ResizeObserver,方便做 resize 的监听。

而这个 api 所能做的远比我们想象的强大很多:

  • 能够监听不同 box-sizing 的变化,比如 border-box/content-box

  • 能够监听元素挂载和卸载

  • 能够监听非容器类型的 DOM,比如 input canvas 等等

正好说到了这个,顺便讲下一个有意思的事情。自古监听,从来没逃过循环计算的问题,大家熟悉的 angular 脏检查的次数限制,而 ResizeObserver 是怎么解决这个问题的呢。

这里先说明几个概念:

  • observer 每一个 ResizeObserver 实例

  • observation 当 observer 实例调用 observe 方法之后创建的每一个监听,每个 observer 可能包含多个 observation

  • target 被监听的 DOM 元素

  • depth 深度,表示一个 DOM 元素距离根元素的距离,在网页中,也就是距离 html 标签的距离。

在每一次 Event Loop 中,会检查每一个 observationtargetdepth ,并取一个最小值。然后顺便检查有那些 observation 产生了变化,并创建对应的 entity ,最后作为参数传给 observer 的回调。当上面这一操作之后,就完成了一轮检测,然后会再重复一遍这样的操作,只不过这次有个要求,不仅仅要求 observation 有变化,还要要求对应的 depth 比上次检查的最小值还要大,才可以创建 entity。就这样一直一直循环检测跑下去,直到没有任何东西被检测到发生变化。

用一点通俗的话来说,除了第一轮的检查外,其他的每一轮检查都要求元素的高度要大于在上一轮检查元素的高度最小值,从而保证每一次检查,深度都会越来越大,直到达到最小的根节点,进而检查结束。

不过也许你会好奇,难道有些元素就会被跳过不检查么?其实不会的,对于那些深度小于上一次的最小深度的 observation 会自动到下一个 Event Loop 的时机去检查。

以上内容确实有点绕,我自己写的再看第二遍都看不懂了,原本好像补一个图仔细讲讲,但是发现上图也讲不清,算了弃疗,大家有兴趣的看看 Spec 吧。还有就是兼容性有点差,微软家的和苹果家的都不支持,摊手

还有别的办法么?

当然有了,还有我们 IntersectionObserver 呀。

怎么用?和最上面使用 scroll 的差不多,但是要略微简单一些,其实也没简单多少,因为该 api 判断的是相交的面积,也就是不能通过一个哨兵来判断,而是一个方向需要有一个哨兵。

至于兼容性么,除了 IE 不支持,其他都支持,这就很完美了么,对吧。(诡异的双眼,实际上坑很多,不建议尝试)

结语

本文没有特别深入的去讲解一些细节,只是讲了一下我觉得有意思的地方,如果你觉得哪里不清楚或者想了解更多,可以尝试去看源码。/ 我又没说包教包会 /

另外,我也是没懂为啥各大浏览器厂商这次这么一致,针对容器大小变化导致的 scrollTop 变化会触发 scroll 事件。可能是我太年轻了吧,这种上古事件的在各大浏览器之间的爱情纠葛我实在是无从考证。

另外,上面说 HC 很多,要多多推荐人,我也不喜欢专门打广告这种事情,但……杭州 Lark 部门招人,前端后端设计产品,应该(因为我也不确定,逃)是都要的。另外我觉得字节跳动是个好公司,如果你有其他部门或者其他城市的部门想要内推或者了解详情的,我也可以帮忙引荐下,只要进了字节我觉得就是对字节最大的贡献,不一定要来我们部门的,滑稽脸。

Refs

注释和共享

目录

  1. position: sticky 这究竟是一个什么鬼?
  2. 正确的使用姿势
  3. 举个栗子 - 通讯录列表头部
  4. 拓展思考
    1. 如何检测是否已经被固定?
    2. 那能不能实现表格头/列固定呢?

不知何时,曾经我们认为的东西便会被打破,如果我们不坚持着去学习,那么我们终将会被社会所淘汰。于是我决定写《后知后觉系列》来记录一下我曾经跟不上的知识和关键点,内容不一定复杂,内容含量不一定高,也许别人已经写过一个一样的教程了,但是希望你能从我的笔记中获取你认为重要的东西,在纷繁复杂的工作中留下一个真正极客的世界,希望某一天这些东西都能够运用到工作当中。——XGHeaven

记得先看一下目录,找到你喜欢好奇的内容去针对性阅读,毕竟我不是来写教程的。

position: sticky 这究竟是一个什么鬼?

最近公司在用 Regular 封装一个表格组件,需要实现固定表头的功能。这个是几乎所有的组件库都会实现的一个效果,所以实现方式有很多种:

  1. 因为 thead/tr 的 position 属性是无效的,所以需要单独用 div 创建一个表头。然后设置这个表头的 position: absolute,同时 top: 0。同时这种模式下,需要用户指定每一列的宽度,保证自制的表头和下面原生的表格一一对应起来。如果不指定的话,也可以等待 dom 渲染完成之后,再测量宽度。比如 Ant Design 就是使用的这种方式。

  2. 因为上面那种方案的难点在于无法很好的保证自制表头和原生表格宽度的一致性,所以我们组的大佬提出了使用原生 thead,监听 scroll 事件,设置 transform 属性使得表头进行偏移,从而实现 fixHeader 的问题,这种方式解决了第一个的问题,但是需要手动监听 scroll 事件,在快速滚动的情况下,可能会有一定的性能问题。而且不够优雅。如果后面的表格内容中有 position: relative 的元素,会覆盖到表头。

不管是哪种方式,我总感觉不是很完美,于是我就在思考,除了手动更新的方式,难道就没有一些比较好的方式去做。然后我就去翻看了 github 的固定表头的方式,顿时豁然开朗。于是就延伸出了这篇文章,position: sticky 属性。

Pay Attention:后面所讲的内容就不怎么和表格固定表头相关,如果你对表格固定表头或者固定列有一定问题,可以查看网易考拉的这篇文章 《一起来聊聊table组件的固定列》

当第一眼看到这个熟悉的时候,第一句话就是“我 CA”,这 TMD 是什么鬼属性,position 什么时候有了这个属性。于是去看了 MDN 的介绍,可以理解为,这个属性是实现固定顶部最简单的实现方式

他其实是一种 position:relativeposition: fixed 的结合体,一定要配合 top/right/bottom/left 的属性一起才有作用,设置对应方向的最小值。当大于最小值的时候,他就像 relative 一样,作为文档流的一部分,并且 top/right/bottom/left 属性也会失效。否则当小于设置的值的时候表现的像 fixed,只不过这个 fixed 不再现对于窗口,而是相对于最近的可滚动块级元素。

如果你看过其他关于 sticky 的文章,大部分都会以黏贴的意思来解释他,那么很明显,确实也是这个意思,如果你觉得看了其他教程能够清楚的话,那么可以不用看我这篇了,如果你没看懂的话,可以来我这里看看。

废话少说,我们先来看一下如何正确使用 sticky。

正确的使用姿势

以下的代码预览请使用最新 Chrome 查看,或者支持 position: sticky 的浏览器查看。部分网站不支持 iframe,可以去我的 Blog 查看

  1. position: sticky 只相对于第一个有滚动的父级块元素(scrolling mechanism,通过 overflow 设置为 overflow/scroll/auto/overlay 的元素),而不是父级块元素。

  2. position: sticky 只有当设置对应的方向(top/right/bottom/left),才会有作用,并且可以互相叠加,可以同时设置四个方向。

  3. 即使设置了 position: sticky,也只能显示在父级块元素的内容区域,他无法超出这个区域,除非你设置了负数的值。

  4. position: sticky 并不会触发 BFC,简单来讲就是计算高度的时候不会计算 float 元素。

  5. 当设置了 position: sticky 之后,内部的定位会相对于这个元素

  6. 虽然 position: sticky 表现的像 relative 或者 fixed,所以也是可以通过 z-index 设置他们的层级。当这个元素的后面的兄弟节点会覆盖这个元素的时候,可以通过 z-index 调节层级。

    See the Pen position: sticky 通过 z-index 调节层级 by Bradley Xu (@xgheaven) on CodePen.

当你懂了这几个之后,其实这个属性就用起来就很简单了。

举个栗子 - 通讯录列表头部

no code no bb,直接上代码。

拓展思考

如何检测是否已经被固定?

最常见的需求就是,当还在文档流当中的时候,正常显示,但是当固定住的时候,添加一些阴影或者修改高度等操作。要想实现这个效果,第一反应可能就是手动监听 scroll 事件,判断位置,这当然是没有问题的,但是随之而来的确实性能的损耗。

最好的方式是使用 IntersectionObserver,这是一个可以监听一个元素是否显示在视窗之内的 API,具体内容见阮老师的《IntersectionObserver API 使用教程》。基本原理就是在一段滚动的头部和尾部分别添加两个岗哨,然后通过判断这两个岗哨的出现和消失的时机,来判断元素是否已经被固定。

例子详见此处

那能不能实现表格头/列固定呢?

–理想很丰满,显示很骨感,因为 thead/tbody 对 position 无爱,所以也就不支持 sticky 属性,所以我们还是要单独创建一个头部。–

后来经过网友提醒,自己又去研究了一下,发现还是有办法做到固定表头和列的。

首先针对 Firefox,它本身就支持 thead/tbody 的 position 属性,所以可以直接通过对 thead/tbody 设置 position 来实现。而对于 Chrome 浏览器来讲,可以通过设置 thead 内的 th 来实现。具体见 Demo.

See the Pen position sticky 通过设置 td 来实现固定表头 by Bradley Xu (@xgheaven) on CodePen.

然后好像就没有了,谢谢观看水水的《后知后觉系列》

注释和共享

  • 第 1 页 共 1 页

XGHeaven

一个弱弱的码农


杭州电子科技大学学生一枚


Weifang Shandong, China