你可能知道怎么解决这个问题,但是你真的了解背后的原因吗?
石碑
请查看下面的示例代码,在ul
元素下有三个使用display: inline-block;
水平排列的li
元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CSS Demo</title>
<style>
* {
margin: 0;
padding: 0;
}
#root {
width: 400px;
border: 3px solid #7abdb6;
margin: 40px;
padding: 20px;
}
ul > li {
display: inline-block;
padding: 5px;
background: #27a3ff;
color: aliceblue;
}
</style>
</head>
<body>
<div id="root">
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
</div>
</body>
</html>
线路
请问,为什么li
元素之间有空隙?
通过使用document.querySelector('ul').childNodes
获取ul
元素的子节点,可以发现,在li
元素之间,存在节点类型为3
的文本节点text
,空隙即文本节点占据的空间。
那如何去除li
元素之间的空隙呢?
删除文本节点即可,比如我们可以换成这种写法:
<div id="root">
<ul><li>A</li><li>B</li><li>C</li></ul>
</div>
很好,但是这样代码可读性会降低,有更优雅的方式吗?
可以使用HTML
解析器,自动删除空白字符文本节点。例如,在vue
项目中,我们可以这么写:
<div id="root">
<div id="app"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
(function () {
const res = Vue.compile(`
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
`, { whitespace: 'condense' });
new Vue({
el: '#app',
render: res.render,
staticRenderFns: res.staticRenderFns,
});
}());
</script>
删除空白字符可能会造成误伤,有不删除空白字符的方法吗?
可以给ul
设置font-size: 0;
,让空白字符不占据空间,例如:
ul {
font-size: 0;
}
li {
font-size: initial;
}
虽然默认情况下,Chrome
只允许设置最小12px
字号,但是设置0px
也是允许的。
如果从布局的角度来考虑,还有其它解决方案吗?
有的,可以使用浮动布局,例如:
ul:after {
content: ' ';
display: block;
clear: both;
height: 0;
visibility: hidden;
}
li {
float: left;
}
或者使用弹性盒子布局也可以:
ul {
display: flex;
}
那为什么使用float
或者flex
之后,li
之间的空白字符就不占据空间了呢?
我们换一个更简单的示例:
<p>
<span>A</span>
<span>B</span>
</p>
<p>
<span>C</span><span>D</span>
</p>
可以发现,在浏览器渲染后,A
与B
之间存在空隙,而C
与D
是紧紧挨着的。
让我们来回顾一下人类语言。
对于中文(也包括一些其它的语言)而言,词与词之间是直接相连的。
JavaScript是世界上最好的语言。
但是对于英文(也包括一些其它的语言)而言,词与词之间是使用空格分隔的。
JavaScript is the best language in the world.
所以,浏览器在渲染包括文本在内的行内元素时,为了保证语义正确性,必需保留渲染空白字符。
也就是说,li
之间有空隙,本质上是因为display: inline-block;
将它们转换为了行内元素。所以浏览器在渲染它们时,选择的是和文本渲染相同的策略,把每个li
当成了一个单词来对待,这样它们之间的空格自然也就保留渲染了。而使用float
就意味着使用块布局,它会在某些情况下修改display
属性的计算值。所以,此时li
的display
属性虽然表面上还是inline-block
,但是实际上已经被修改为了block
。对于flex
也是同样的道理。
回到最开始,ul
下有7个子节点,为什么第1个和最后1个文本节点没有占据空间呢?
console.log(document.querySelector('ul > li:first-child').getBoundingClientRect());
// DOMRect {x: 63, y: 63, width: 30.515625, height: 32, top: 63, right: 93.515625, bottom: 95, left: 63}
可以看到,第1个li
元素的x
为63,正好等于margin 40px + border 3px + padding 20px
。
同样用一个更简单的示例:
<div>
Here is an English paragraph
that is broken into multiple lines
in the source code so that it can
more easily read in a text editor.
</div>
可以发现,这一大段文本都渲染在了同一行。
回到互联网刚诞生的时候,网页都是纯静态的,所以网页里面的内容都是直接硬编码出来的,而不像今天我们大多都是动态渲染或静态编译出来的。这样,为了HTML
源码的可读性和易维护性,空格、缩进和换行,以及一些其它空白字符就会被大量使用了。但是浏览器在渲染的时候,不能把这些都渲染出来啊,所以就有了一个折叠连续空白字符的概念。
简言之,就是把连续的空白字符全部折叠为单个的空白字符,并尽量不渲染空白字符。
这里直接引用CSS
规范里面的一段原文。
重点在第1、第3和第4点,行首和行尾的连续可折叠的空白字符在渲染时将被删除,如果行尾仍有可折叠的空白字符,则悬挂到行首。
所以,可以看到,第1个文本节点是无法选中的,但是第7个文本节点可以被选中,且出现在了第1个li
的前面!
注意:这里的第7个文本节点可以被选中且宽度为0,是受到了第5个文本节点的影响。
如果换成下面的写法:
<ul>
<li>A</li><li>B</li><li>C</li>
</ul>
则最后一个文本节点同样无法被选中。
实战
某宇宙公司面试官发了个 codepen 上的源码链接。
请问,为什么div
元素之间有空隙?
不记得了。
问题结束。