爲了更好的支持移動端和 PC 端的縮放,WebKit 增長了subpixel layout
(次像素/亞像素佈局)爲此他們還改變了 rendering tree。一個次像素單元在 WebKit 內被稱爲 LayoutUnit 用於取代以前使用整數來佈局一個元素在頁面中位置和大小。從 2013 年開始 WebKit 就已經開啓了這個 flag。css
LayoutUnit 是邏輯像素的一種抽象表示,在 WebKit 的實現中它是一個像素的 1/64,這樣咱們就可使用整數來進行佈局計算,避免了使用浮點數計算而丟失精度的問題。html
雖然咱們如今在佈局計算時使用了 LayoutUnit,可是在最終將計算值渲染對應到設備上時仍然會出現計算值不能與物理像素對齊的狀況。由於計算出的值多是一個小數而 1 個物理像素已經不能再進行切割。因此出現了這樣一個問題,次像素如何與物理像素進行對齊?node
回到咱們實際的編程過程當中,咱們會有不少場景遇到次像素的問題,只是不少人不會關注,或者會忽略掉這些細節。好比若是一個 box 的寬度是 10px,咱們把它平均分紅 3 份,那麼裏面的三個盒子的寬度分別是多少呢,3.3333px?再好比咱們在使用 rem 佈局的時候有時候會發現一個正方形設置了 border-raduis 預期讓它展現成一個圓形,在一些設備上卻並不那麼圓,在總體比較小的時候可能會被渲染成一個橢圓形。以及這種時候這個元素還設置了一個 background-size 覆蓋整個容器可是背景卻被切掉了一小塊。這些問題不是那麼容易被發現,可是確實是存在的。git
如今咱們有一個 50px 的容器(DPR 爲 1)將他分紅 3 份,必然會出現小數的狀況,看看每一份渲染在屏幕上的寬度是多少。github
<div class="container"> <div style="background: #111"></div> <div style="background: #222"></div> <div style="background: #333"></div> </div>
.container { display: flex; width: 50px; height: 30px; background: #999; } .container div { flex: 1; height: 100%; }
const getWidth = () => { const container = document.querySelector('.container'); const nodes = Array.prototype.slice.call(container.children); nodes.forEach((i, index) => { console.log( `${index} width: ${i.clientWidth}, computed width: ${ i.getBoundingClientRect().width }` ); }); }; getWidth();
// console 0 width: 17, computed width: 16.671875 1 width: 16, computed width: 16.671875 2 width: 17, computed width: 16.671875
咱們發現三份的 clientWidth 並非同樣的,其中一個會少一像素,可是它們的寬度加起來仍然是容器的寬度。而經過 getBoundingClientRect 得到的計算值倒是同樣的,可是並不像咱們預期的同樣是 50/3 = 16.666666667,而是 16.671875 看起來也並無什麼四捨五入的關係。可是從上面的例子中咱們能夠獲得的一個結論就是,上面三份中的寬度最終在屏幕上並非徹底一致的,這也會致使咱們在其餘場景下遇到相似的問題,好比說在一個頁面中同一個組件渲染出來的元素在頁面的多個位置上可能表現出不一致的狀況,有些元素可能渲染出來會多 1px 或者少 1px,在像素越小的地方對比度就會越明顯,好比一個高度是 3px,另外一個 2px,這樣就會看出明顯的差別。而若是一個是 100px 另外一個是 101px,你可能就沒有感知了。web
上面還有一個問題沒有解決,就是計算值和咱們預期不一致。這裏就能夠經過 LayoutUnit 來解釋。上面咱們提到在佈局的使用會使用 subpixel layout 把一個像素分紅 64 份。這樣咱們看看 WebKit 在佈局的時候是怎麼就算的:算法
1. container width: 50px * 64 => 3200 2. 每個子 div: 3200 / 3 = round(1066.666666667) => 1067 3. 最終計算值: 1067 / 64 => 16.671875
經過上面的計算咱們發現結果和 getBoundingClientRect 得到的值徹底吻合,因此這裏計算元素大小的時候瀏覽器內核使用了 subpixel layout,而不是直接使用原來的 pixels。編程
這裏仍然面臨了另外一個問題,咱們使用 subpixel layout 計算出來的值仍然是一個小數,可是咱們佈局的時候是如何和物理像素進行對齊的呢?上面少掉 1px 的元素僅是由於把 getBoundingClientRect 的值進行四捨五入?那這樣也應該全是 17px,而單單中間的一個元素少了一像素?瀏覽器
在進行 subpixel 和 pixel 之間轉換時,有兩種方式,一種是 enclosingIntRect
另外一種是 pixelSnappedIntRect
在上述的例子中使用了第二種轉換方式。app
上面的圖中,灰色格子表明物理像素,藍色區域表示 subpixel layout 計算值,黑色區域表示最終 subpixel -> pixel 的對齊結果。
enclosingIntRect 算法:
x: floor(x) y: floor(y) maxX: ceil(x + width) maxY: ceil(y + height) width: ceil(x + width) - floor(x) height: ceil(y + height) - floor(y)
這種計算方式很簡單,直接選擇最小的徹底能覆蓋住計算結果的物理像素區域。
pixelSnappedIntRect 算法:
y: round(y) maxX: round(x + width) maxY: round(y + height) width: round(x + width) - round(x) height: round(y + height) - round(y)
pixelSnappedIntRect 的計算也很簡單,它直接 round 到離本身最近的一個物理像素。
接着上面的例子,咱們如今把 50px 分層 6 份來模擬計算下看看每一份的寬度計算值應該是多少:
1. container width: 50px * 64 => 3200 2. 每個子 div: 3200 / 6 = round(533.333333333) => 533 3. 最終計算值: 533 / 64 => 8.328125
// log 0 width: 8, computed width: 8.328125 1 width: 9, computed width: 8.328125 2 width: 8, computed width: 8.328125 3 width: 8, computed width: 8.328125 4 width: 9, computed width: 8.328125 5 width: 8, computed width: 8.328125
看到 js 算出來的值和咱們算出來的是一致的,並不簡單的是 50/6 = 8.333333333。在最終渲染的時候:
和上述 js 獲取的 clientWidth 結果 8,9,8,8,9,8 徹底一致。因此這裏元素的大小能夠經過 pixelSnappedIntRect
對齊方式來解釋爲何有些元素會多/少一像素而且出現是「沒有規則」的。
上面介紹了 2 種對齊方式,那麼在什麼場景下 WebKit 用什麼算法呢?以及全部的佈局都會使用 subpixel layout 麼?
爲了保證一些場景下的一致性渲染,並非全部場景都會使用 subpixel,好比說在計算 border 的時候就不會,這樣避免了咱們設置了一個 border,渲染出來的元素上邊可能會比下邊還多出 1px。以及在大部分的場景中計算元素的大小的時候會使用 pixelSnappedIntRect
。在少數的一些 case 下會使用 enclosingIntRect
計算,好比一個 RenderBlock 中的 SVG 盒子,由於須要保證盒子能徹底包含住子樹。具體的細節能夠參看 WebKit 的文檔或源碼。
佈局計算值有小數帶來渲染結果不一致的狀況常常發生在 rem 佈局中,因爲 DPR 的轉換致使一些設備下不少場景都是小數。好比下面就是一個常見的真實業務場景。
在實現一個 popup 組件或者 dialog 組件時常常會有一些選項,css 也是能繪製出來的,好比上面 9 個紅色的圓點,它們都是一個組件,預期寬高都是 10px,可是經過一系列的換算後第一個卻變成了 9px,第二個又是 10px,9 個原點渲染出來不盡相同。
下面實現的選擇項 icon 是沒有問題的,爲了不這種不一致咱們選擇使用了圖片,svg 或者直接 base64 一張 png 到組件裏面,可是若是將 png 圖做爲一個背景放入一個固定寬高的盒子中仍然可能有問題。
若是有一個容器寬高都是 10px,設置一個 backgroundImage,大小和容器同樣大,可能會出現背景被「割裂」的狀況,由於容器可能會被渲染成 9 x 10 致使背景圖一部份內容不可見。能夠經過給容器設置一點點 padding 來解決這個問題。
還有不少相似這種渲染結果不符合咱們預期的 case 基本都是由於使用 rem 佈局致使的,解決它就是儘可能 rem to px 時讓它不要是一個小數,或者直接使用 px,或者不要使用 rem 佈局!
若有錯誤歡迎指正,文章原文 https://github.com/Jiavan/blo... 轉載請註明出處。