瀏覽器眼中的0

福祿網路技術團隊發表於2020-12-04

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的高度,因此會外部塊級容器的高度會略高一些,說得不清楚,我們還是上一張圖來說明吧:
圖片描述
粉色的線是圖片的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·架構組 福袋

相關文章