福禄网络技术团队

福禄网络技术团队

0作为一个特殊的符号,经常会跟浏览器打交道,在不同的场景下,0代表的意思不尽相同,因此浏览器眼中的0不一定就是符合人们感官上的认识,那究竟浏览器会怎么对待它呢,今天我们就来探究一下各种场景中0的含义及浏览器的处理方式。

1.setTimeout

setTimeout在js中常用来推迟任务的执行,可以通过第二个参数设置延迟的毫秒数(如果不设置,默认为0),在一些代码中,可以看到delay=0的情况,如下:

window.setTimeout(() => { ...... }, 0);

了解js的同学应该知道,setTimeout的回调函数不会在定时器超时后立即执行,如果delay大于0,比较好理解,但delay是0的时候呢,浏览器会怎么对待呢,这里要分两种情况:
1.)timer嵌套
'Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.'也就是setTimeout嵌套超过5层的,并且延迟不到4ms,才会变成4ms,同样适用于setInterval,因此在这种情况下,delay=0其实会被设置成4;
2.)timer没有嵌套
在没有嵌套情况下,对于chrome来说,delay=0也会设置成1;
说完浏览器的处理方式之后,我们来看看网上搬过来的一个例子吧:

setTimeout(()=>{console.log(5)},5)
setTimeout(()=>{console.log(4)},4)
setTimeout(()=>{console.log(3)},3)
setTimeout(()=>{console.log(2)},2)
setTimeout(()=>{console.log(1)},1)
setTimeout(()=>{console.log(0)},0)

chrome打印结果:1 0 2 3 4 5;
firefox打印结果:0 1 2 3 4 5;
edge打印结果:1 2 0 3 4 5;
qq浏览器打印结果:1 0 2 3 4 5;
360浏览器打印结果:1 0 2 3 4 5;
从上面的打印结果来看,firefox是符合代码预期的,edge打印与chrome稍有不同,应该是edge处理delay=0情况稍有不同(设置成了2),qq和360浏览器跟chrome保持一致。
0ms定时器
在MDN文档上,还说到一种实现0ms延时的定时器的实现方案,大体思路是自定义一个setZeroTimeout 方法,通过 postMessage 来触发定时回调的执行,具体可看 https://dbaron.org/log/20100309-faster-timeouts
node的setTimeout
说完浏览器中的setTimeout,我们再来看看nodejs中的是否一样呢,可以通过nodejs的源代码窥探一二:

// https://github.com/nodejs/node/blob/master/lib/internal/timers.js
function Timeout(callback, after, args, isRepeat, isRefed) {
    after *= 1; // Coalesce to number or NaN
    if (!(after >= 1 && after <= TIMEOUT_MAX)) { // const TIMEOUT_MAX = 2 ** 31 - 1;
        if (after > TIMEOUT_MAX) {
            process.emitWarning(`${after} does not fit into` +' a 32-bit signed integer.' + '\nTimeout duration was set t 1.', 'TimeoutOverflowWarning');
}
    after = 1; // Schedule on next tick, follows browser behavior
}

看过源代码后,就知道node的处理策略了:如果delay=0,会设置为1,注释也说得很清楚了,是为了遵循浏览器的行为。

2.+0&-0

虽然很少用到,但是js中确是存在+0和-0的,那么有什么区别呢:

+0 === -0; // true
+0 === 0; // true
-0 === 0; // true

可以看到,通过全等比较,+0,-0和0都是相等的,那是否就可以认为这三者就是一样的呢,还不能这么轻易下结论,有时候很有必要区分三者,那么如何判断呢,es6新增了一个方法Object.is(value1, value2),可以用来判断,具体效果如下:

Object.is(0, -0); // false
Object.is(+0, -0); // false
Object.is(+0, 0); // true

这里还需要说明的一点就是+0和0其实就是一样的,因为+0等效Number(0),因此Object.is(+0, 0)是符合预期的,这里顺带说一下Object.is的比较逻辑,根据MDN文档描述,
Object.is() 方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:

  • 都是 undefined
  • 都是 null
  • 都是 truefalse
  • 都是相同长度的字符串且相同字符按相同顺序排列
  • 都是相同对象(意味着每个对象有同一个引用)
  • 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 或都是非零而且非 NaN 且为同一个值

== 运算不同。 == 运算符在判断相等前对两边的变量(如果它们不是同一类型) 进行强制转换 (这种行为的结果会将 "" == false 判断为 true ), 而 Object.is 不会强制转换两边的值。
=== 运算也不相同。 === 运算符 (也包括 == 运算符) 将数字 -0+0 视为相等 ,而将Number.NaNNaN视为不相等.

最后来看下如何生成-0和+0吧:

1/-Infinity; // -0
0*-1; // -0
1/+Infinity; // +0
0*+1;// +0

3. +[]

由于存在类型转换,因此在判断相等时,会有这样的情况:

'' == 0; // true
[] == 0; // true
+[] == 0; // true

这里我们说一下+[]的情况,很容易理解,+相当于Number([]),最终会转换成0,这没什么大不了,如果是下面这段代码呢:

(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[+!+[]+[!+[]+!+[]+!+[]]]+[+!+[]]+([+[]]+![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[!+[]+!+[]+[+[]]]

其实代表的就是alert(1)http://www.jsfuck.com/ 这个奇葩网站专门干这事的,原理就是使用的类型转换,把我们熟悉的代码变成了这样,真是骚操作。

4.font-size

font-size在css中使用频繁,浏览器通常会默认设置成14px或者16px。如果设置成0呢?

.foo {
    font-size: 0;
}

<span class="foo">foo</span>

文字真的就没有了,这符合预期,但如果设置的值小于12(非负),各浏览器处理稍有不同,chrome/edge浏览器会设置最小字体为12px,firefox严格按照给定的值来显示。
font-size:0的用处
在布局过程中,经常会生成空白字符,例如:

<div class="box1">
    <span>a</span>
    <span>b</span>
</div>
<div class="box2">
	<img src="xxx.jpg">
</div>

box1和box2高度其实是略高于实际的内容的,而且box1的两个span中间有间隙,没有紧挨着,因为空白字符的原因,引用css规范说明:On a block container element whose content is composed of inline-level elements, 'line-height' specifies the minimal height of line boxes within the element. The minimum height consists of a minimum height above the baseline and a minimum depth below it, exactly as if each line box starts with a zero-width inline box with the element's font and line height properties. We call that imaginary box a "strut.",大意就是一个块级容器元素内容区域由inline-level元素组成的;而这些linline-level元素被放在每一行的line-box里面,line-box高度是由它所有子元素的高度计算得出的。浏览器会计算这一行里每个子元素的高度,再得出 line-box 的高度(具体来说就是从子元素的最高点到最低点的高度)。默认情况下,一个 line-box 总是有足够的高度来容纳它的子元素,而每一个行框可以想象为默认会有一个宽度为0的空白节点,字体大小和行高会影响该节点,具体规范可查看 https://www.w3.org/TR/CSS2/visudet.html#line-height。
说完规范后,再来分析一下box1和box2的效果,为了便于理解,下面的代码手动加入了一个[x]符号,代表strut:

<div class="box1">
	<span>a</span> <span>b</span>
	[x]
</div>
<div class="box2">
	<img src="xxx.jpg">[x]
</div>

box1的两个span发生了换行,相当于中间有个空格,因此会有间隙,如果span不换行,那么中间的空隙也就没有了。由于存在[x],而且vertical-align:baseline的缘故,span/img和[x]的baseline对齐之后,[x]处于baseline以下的部分会撑开整个line-box的高度,因此会外部块级容器的高度会略高一些,说得不清楚,我们还是上一张图来说明吧:
浏览器眼中的0-LMLPHP
粉色的线是图片的baseline,红色的线是strut的baseline,对齐之后strut的baseline下面还有一部分高度,box的最终高度就是img的顶部到strut底部之间的距离。分析完成因后,终于可以使用font-size:0来解决问题了,在box上设置font-size:0后,strut就相当于没有了,因此就不存在高度撑开的问题了,当然这里还可以改变img的vertical-align属性来修复这个问题,比如img{ vertical-align:bottom; }那么img的粉色线就跟strut的底部对齐了,也就不会撑开容器高度了,这里说了box2,box1原理差不多,这里涉及到vertical-align的知识了,可查阅https://www.w3.org/TR/CSS2/visudet.html#vertical-align 进行了解。

5.width&height

width和height也是可以设置成0的,效果也是符合我们预期的,但如果我们的意图是想通过设置0把元素隐藏的话,一般情况下会采用如下方案:

.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
}

有些屏幕阅读器会忽略width和height等于0的元素,因此这里特意设置成1px,当然关于元素的隐藏还有很多实现方案,有兴趣可参考【1】;

6.line-height

line-height及行高,原意是baseline之间的高度,在css中就是一行的高度。默认情况下,line-height是跟具体字体定义相关联的,一般都是font-size的1.x倍,如果设置成0,在不同类型元素上的情况是不一样的,可分为如下三种情况:
1.)非置换行类元素
line-height定义的是最终参与计算line-box(行盒)的高度的值,而不会影响non-replaced inline element(非置换行类元素)的实际高度;

<style>
    .foo {
        line-height:0;
    }
</style>

<div>
    <span class="foo">a</span> // class为foo的span高度不会受样式影响
</div>

2.)行类块级元素
会影响元素高度,如果line-height设置为0,那么该元素高度就变成了0,如果设置了height,那么height将会起作用;
3.)块级元素
当块级元素包含inline-level(display:inline|inline-block)元素时, 行高定义的每一个line-box(行盒)的最小高度,此外height也能影响块级元素的最终高度,height比line-height有更高优先级,当没有height情况下,line-height起作用;
上面提到了line-box,如果有不了解的同学,可以看一下文末的链接【2】;

7.transform

transform常用来做样式变化和动画,在有时候,会设置成如下形式:

transform: translateZ(0);

这其实是为了启用GPU加速渲染,元素会单独在一个绘制层(Layer)里进行绘制,而不会对其他层产生影响,因此也就少了很多计算和合成的功能,而且不会阻塞主线程,动画会更加流畅,当然元素设置太多会导致性能降低,因为需要内存的维护。

参考资料:

【1】https://css-tricks.com/comparing-various-ways-to-hide-things-in-css/
【2】https://www.w3.org/TR/CSS2/visuren.html#line-box

福禄ICH·架构组 福袋
12-04 21:15