瀏覽器眼中的0

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 來觸發定時回調的執行,具體可看 //dbaron.org/log/20100309-faster-timeouts
node的setTimeout
說完瀏覽器中的setTimeout,我們再來看看nodejs中的是否一樣呢,可以通過nodejs的源代碼窺探一二:

// //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)//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的空白節點,字體大小和行高會影響該節點,具體規範可查看 //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的高度,因此會外部塊級容器的高度會略高一些,說得不清楚,我們還是上一張圖來說明吧:
圖片描述
粉色的線是圖片的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的知識了,可查閱//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】//css-tricks.com/comparing-various-ways-to-hide-things-in-css/
【2】//www.w3.org/TR/CSS2/visuren.html#line-box

福祿ICH·架構組
福袋
Tags: