老生常谈之display: none

相信小伙伴们都被问过这样一个问题:让一个元素隐藏起来,有多少种方法呢?常规来讲,我们有三种方法display: none、opacity: 0和visibility: 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: flex、display: grid冲突而使人困扰。

 

你真的了解opacity和visibility吗

如此说来,我们设置元素显隐时,使用opacity或visibility似乎是更好的选择,但小伙伴们有没有考虑过,opacity: 0和visibility: hidden 这两者又有何具体区别呢?

既然标题写着寻根问底,那么我们就通过几轮PK来深挖一下这两者的具体区别:

第一轮:动画属性

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

第二轮:子元素的表现

设置了opacity: 0和visibility: 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进行显隐切换可能更省心,但如果有动画需求,则最好使用两者结合的方式。另外,以后会有更多的寻根问底系列的文章,目的就是要对小的知识点也进行深入剖析,从而获得更加系统性的认识,而不是停留在表面。