乐趣区

寻根问底之元素隐藏你知多少

老生常谈之 display: none

相信小伙伴们都被问过这样一个问题:让一个元素隐藏起来,有多少种方法呢?常规来讲,我们有三种方法 display: noneopacity: 0visibility: hidden ,基于 display: none 的副作用,已经是个被说烂的问题,主要是有以下缺点:

一、切换显隐时会导致 reflow(回流),从而引起 repaint(重绘),当页面中 reflow 增多至一定程度时,会导致 cpu 使用率飙高。

二、无法对元素设置过渡动画,也无法进行方位测量(包括 clientWidth, clientHeight, offsetWidth, offsetHeight, scrollWidth, scrollHeight, getBoundingClientRect(), getComputedStyle())

原因是:浏览器会解析 HTML 标签生成 DOM Tree,解析 CSS 生成 CSSOM,然后将 DOM Tree 和 CSSOM 合并生成 Render Tree,最后才根据 Render Tree 的信息布局来渲染界面,但设置了 display: none 的元素,是不会被加入 Render Tree 中的,自然也无法渲染过渡动画。

三、用它来设置显隐切换时,会因为与 display: flexdisplay: grid 冲突而使人困扰。

你真的了解 opacity 和 visibility 吗

如此说来,我们设置元素显隐时,使用 opacity 或 visibility 似乎是更好的选择,但小伙伴们有没有考虑过,opacity: 0visibility: hidden 这两者又有何具体区别呢?
既然标题写着寻根问底,那么我们就通过 几轮 PK来深挖一下这两者的具体区别:

第一轮:动画属性

常见的动画效果中,使用最广泛的应该就属淡入和淡出了,这时候,我们应该只有一种选择:opacity 配合 animation,因为 visibility 这个属性是无法进行动画过渡的,要满足动画过渡,必须在两个值之间存在连续不断的值,即连续区间,visibility 显然不满足,因为在可见 / 不可见两个状态之间不存在中间态,它是“布尔隐藏”的。

第二轮:子元素的表现

设置了 opacity: 0visibility: hidden 的元素,它们的子元素会受到怎样的不同影响呢?

首先,opacity 属性是不可以被子元素继承的,而 visibility 属性可以被继承,详见 CSS3 规范 opacity 和 visibility 中的属性介绍。

其次,一旦父级元素设置了 opacity,那么子元素的最大透明度将无法超过父级,意味着,父级的 opacity 为 0.5,那么子级的 opacity 就算设置为 1,其实际透明度也会是 0.5 * 1 = 0.5,所以,只要父级透明度为 0,那么子级没有任何办法可以重新设置为可见;

但 visibility 的子级却仍有“翻身”的机会,即使父级元素设置了 visibility: hidden,子元素仍可通过visibility: visible 重新设置为可见。

第三轮:层叠上下文(Stacking Context)

HTML 中的元素都有自身的层叠水平,但是某些情况下,元素会形成层叠上下文(接下来用 SC 代替),直接“拔高”自身以及子元素的层叠水平。而元素间不同的层叠水平,在它们发生重叠的时候,就会决定谁将在 Z 轴上更高一筹,也就是谁离用户的眼睛更近。

至于什么情况下元素会形成 SC,可以参考 MDN 文档的详细说明。而在这份文档中我们可以看到:当元素的 opacity 属性值小于 1 时,会形成 SC。我们可以观察如下代码:

<div style="position: relative;">
    <div style="position: absolute;background: green;
                top: 0;width: 200px;height: 200px">
    </div>
    <div style="background: red;width: 100px;height: 100px"></div>
</div>

这种情况下,设置了绝对定位的绿色方块形成了 SC,所以其层叠水平自然比红色方块高,所以此时我们看不到红色方块:

而当我们为红色方块设置了 opacity 属性后,比如:

<div style="position: relative;">
    <div style="position: absolute;background: green;
                top: 0;width: 200px;height: 200px">
    </div>
    <div style="opacity: 0.5;background: red;width: 100px;height: 100px">
    </div>
</div>

此时,红色方块会层叠在绿色方块之上。因为红色方块的 opacity 小于 1,形成了 SC,且两者都未设置 z -index,属于相同层叠水平,所以按照后来居上的原则,红色方块就会叠在上方,如图所示:

同理,opacity 为 0 的元素也会创建 SC,而 visibility 属性则不会创建 SC,也不会影响到元素的层叠水平。

说了半天,有人可能会问,既然元素都隐藏了,看不见了,谁还管它在上在下呢?通常情况下是如此,但经过 第四轮的 PK后,你就会知道,有时候你的确不能忽视这个问题。

第四轮:可交互性 / 可访问性

这一轮我们比较的是 可交互性 / 可访问性,先说visibility: hidden,设置了这个属性的元素,其绑定的监听事件将会忽略 event.target 为自身的事件触发。这句话比较拗口,通俗点说就是,这个元素会接收到子元素的事件冒泡,但无法触发自身的事件,可以通过这个在线 demo 体验一下这个效果。

当然,除了无法触发自身的事件之外,它还无法通过 tab 键访问到,也就是无法 focus;此外,它还会失去 accessibility,也就是不能进行无障碍访问,比如屏幕阅读软件将无法访问到这个元素。

反观设置了 opacity: 0 的元素,则完全没有以上的限制。现在你知道我们为啥不能忽视上一轮提出的问题了,因为设置了 opacity: 0 的元素即使看不见了,它仍然可以被点击被访问,有时会产生意料之外的 bug。

取长补短

既然两者都有各自的优缺点,我们能否将其结合,并取长补短呢?

答案是当然可以。但首先要明确我们想取什么长,补什么短。一般来讲,我们既希望元素可以使用淡入淡出的动画效果,又希望在消失后不要保留可 交互性 / 可访问性,其实做法很简单:

.box {animation: fade 0.5s linear 0s forwards;}
@keyframes fade {
  0% {opacity: 1;}
  100% {
    opacity: 0;
    visibility: hidden;
  }
}

我们仍然使用 opacity 来做动画过渡,但在最后一个动画帧,我们把 visibility: hidden 加上,就可以达到我们想要的效果了。此时,当元素淡出后,也不会意外地触发事件了。并且,在使用 opacity 属性进行动画效果时,浏览器还会将该元素提升为 composite layer(合成层),使用 gpu 进行硬件加速渲染,两全其美~

当然,如果你的确需要这个元素保留页面中的占位,就不能这样做了。

总结

总而言之,如果你没有动画需求,使用 visibility 进行显隐切换可能更省心,但如果有动画需求,则最好使用两者结合的方式。另外,以后会有更多的寻根问底系列的文章,目的就是要对小的知识点也进行深入剖析,从而获得更加系统性的认识,而不是停留在表面。

ps:欢迎关注微信公众号——前端漫游指南,会定期发布优质原创文章和译文,关注公众号福利:回复 666 可以获得精选前端进阶电子书,感谢~

退出移动版