灵魂拷问——关于DOM元素中的空白符

697

你可能知道怎么解决这个问题,但是你真的了解背后的原因吗?

石碑

请查看下面的示例代码,在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元素之间有空隙?

image

通过使用document.querySelector('ul').childNodes获取ul元素的子节点,可以发现,在li元素之间,存在节点类型为3的文本节点text,空隙即文本节点占据的空间。

image

那如何去除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>

可以发现,在浏览器渲染后,AB之间存在空隙,而CD是紧紧挨着的。

让我们来回顾一下人类语言。

对于中文(也包括一些其它的语言)而言,词与词之间是直接相连的。

JavaScript是世界上最好的语言。

但是对于英文(也包括一些其它的语言)而言,词与词之间是使用空格分隔的。

JavaScript is the best language in the world.

所以,浏览器在渲染包括文本在内的行内元素时,为了保证语义正确性,必需保留渲染空白字符。

也就是说,li之间有空隙,本质上是因为display: inline-block;将它们转换为了行内元素。所以浏览器在渲染它们时,选择的是和文本渲染相同的策略,把每个li当成了一个单词来对待,这样它们之间的空格自然也就保留渲染了。而使用float就意味着使用块布局,它会在某些情况下修改display 属性的计算值。所以,此时lidisplay属性虽然表面上还是inline-block,但是实际上已经被修改为了block。对于flex也是同样的道理。

image

回到最开始,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规范里面的一段原文。

image

重点在第1、第3和第4点,行首和行尾的连续可折叠的空白字符在渲染时将被删除,如果行尾仍有可折叠的空白字符,则悬挂到行首。

所以,可以看到,第1个文本节点是无法选中的,但是第7个文本节点可以被选中,且出现在了第1个li的前面!

注意:这里的第7个文本节点可以被选中且宽度为0,是受到了第5个文本节点的影响。

image

如果换成下面的写法:

<ul>
    <li>A</li><li>B</li><li>C</li>
</ul>

则最后一个文本节点同样无法被选中。

实战

某宇宙公司面试官发了个 codepen 上的源码链接。

请问,为什么div元素之间有空隙?

不记得了。

问题结束。

地图