canvas 是 HTML5 新增的一個標簽, 表示畫布
●canvas 也是 h5 的畫布技術(shù), 我們通過代碼的方式在畫布上描繪一個圖像
canvas 標簽
●向進行 canvas 繪圖, 首先我們先要了解到 canvas 標簽
●是 html5 推出的一個標簽
<html>
<head>
...
</head>
<body>
<canvas></canvas>
</body>
</html>
○canvas 默認是一個行內(nèi)塊元素
○canvas 默認畫布大小是 300 * 150
○canvas 默認沒有邊框, 背景默認為無色透明
canvas 畫布大小
●我們在繪圖之前, 先要確定一個畫布的大小
○因為畫布默認是按照比例調(diào)整
○所以我們調(diào)整寬度或者高度的時候, 調(diào)整一個, 另一個自然會按照比例自己調(diào)整
○我們也可以寬高一起調(diào)整
●調(diào)整畫布大小有兩種方案
○第一種 : 通過 css 樣式 ( 不推薦 )
<html>
<head>
<style>
canvas {
width: 1000px;
height: 500px;
}
</style>
</head>
<body>
<canvas></canvas>
</body>
</html>
○第二種 : 通過標簽屬性 ( 推薦 )
<html>
<head>
...
</head>
<body>
<canvas width="1000" height="500"></canvas>
</body>
</html>
●兩種方案的區(qū)別
○通過 css 樣式的調(diào)整方案, 不推薦
是因為這個方案其實并沒有設(shè)置了畫布的大小
而是把原先 300 * 150 的畫布, 將他的可視窗口變成了 1000 * 500
所以真實畫布并沒有放大, 只是可視程度變大了
舉個例子 : 就是你把一個 300 * 150 的圖片, 放大到 1000 * 500 的大小來看
所以這個方式我們及其不推薦
○通過屬性的調(diào)整方案, 推薦
這個才是真正的調(diào)整畫布的的大小
也就是我們會在一個 1000 * 500 的畫布上進行繪制
●畫布的坐標
○canvas 畫布是和我們 css 的坐標系一樣的
○從 canvas 的左上角為 0 0 左邊, 分別向右向下延伸為正方向
canvas 初體驗
●準備工作已經(jīng)完成了, 我們可以開始體驗一下繪制了
●其實 canvas 畫布很簡單, 就和我們 windows 電腦的畫板工具是一樣的道理
●思考 :
我們在 windows 這個畫板上繪制內(nèi)容的時候
我們一定是先選定一個工具 ( 畫筆, 矩形, 圓形, ... )
設(shè)定好樣式 ( 粗細, 顏色 )
然后開始繪制
●其實在 canvas 繪制也是一個道理
拿到一個畫布工具箱
從工具箱中選定工具
設(shè)定樣式
開始繪制
●初體驗步驟
●index.html
<html>
<head>
...
</head>
<body>
<canvas id="canvas" width="600" height="300"></canvas>
<script src="./index.js"></script>
</body>
</html>
●index.js
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
// 語法: canvas 元素.getContext('2d')
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制
// 2-1. 講畫筆移動到一個指定位置開始下筆
// 語法: 工具箱.moveTo(x軸坐標, y軸坐標)
ctx.moveTo(100, 100)
// 2-2. 將筆移動到一個指定位置, 畫下一條軌跡
// 注意: 這里是沒有顯示的, 因為只是畫了一個軌跡
// 語法: 工具箱.lineTo(x軸坐標, y軸坐標)
ctx.lineTo(300, 100)
// 2-3. 設(shè)定本條線的樣式
// 設(shè)定線的寬度
// 語法: 工具箱.lineWidth = 數(shù)字
ctx.lineWidth = 10
// 設(shè)定線的顏色
// 語法: 工具箱.strokeStyle = '顏色'
ctx.strokeStyle = '#000'
// 2-4. 描邊
// 把上邊畫下的痕跡按照設(shè)定好的樣式描繪下來
// 語法: 工具箱.stroke()
ctx.stroke()
●至此我們的第一個線段就繪制完畢, 畫布上就會出現(xiàn)一條線段
○從坐標 ( 100, 100 ) 繪制到坐標 ( 300, 100 )
○線段長度為 200px
○線段寬度為 10px
○線段顏色為 '#000' ( 黑色 )
canvas 線寬顏色問題
●剛才我們經(jīng)過了初體驗, 畫了一個線段
●看似沒有問題, 也出現(xiàn)了線段, 但是其實內(nèi)在是有一些問題的
●我們先來觀察
●這次我們再來畫一個線段
○從坐標 ( 100, 100 ) 繪制到坐標 ( 300, 100 )
○線段長度為 200px
○線段寬度為 1px
○線段顏色為 '#000' ( 黑色 )
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制
ctx.moveTo(100, 100)
// 2-2. 將筆移動到一個指定位置, 畫下一條軌跡
ctx.lineTo(300, 100)
// 2-3. 設(shè)定本條線的樣式
// 設(shè)定線的寬度
ctx.lineWidth = 10
// 設(shè)定線的顏色
ctx.strokeStyle = '#000'
// 2-4. 描邊
ctx.stroke()
●效果出現(xiàn)了, 沒有什么問題
●只是看上去不太想 1px, 而且顏色有些淺
●不著急, 我們再來畫一個線段
○從坐標 ( 100, 100 ) 繪制到坐標 ( 300, 100 )
○線段長度為 200px
○線段寬度為 2px
○線段顏色為 '#000' ( 黑色 )
●這個時候問題就出現(xiàn)了
○兩次畫出來的線段, 一次設(shè)置 1px 一次設(shè)置 2px
○感覺上 線寬度 一樣
○兩次畫出來的線段, 兩次都是設(shè)置為 '#000' 的顏色
○但是感覺上顏色不太一樣
●這是因為瀏覽器在描述內(nèi)容的時候, 最小的描述單位是 1px
●我們來模擬一下瀏覽器繪制的內(nèi)容
○假設(shè)這是我們?yōu)g覽器描述的畫布中的像素點
○我們來做一個坐標的標記
○現(xiàn)在呢不關(guān)注線的長度和坐標, 我們就畫一個寬度為 1px 的線段
○我們來剖析一下問題
因為在描繪這個線段的時候, 會把線段的最中心點放在這個像素點位上
也就是說, 在描述線寬的時候, 實際上會從 0.5px 的位置繪制到 1.5px 的位置
合計描述寬度為 1px
但是瀏覽器的最小描述為 1px
這里說的不是最小寬度為 1px, 是瀏覽器不能在非整數(shù)像素開始描述
也就是說瀏覽器沒辦法從 0.5 開始繪制, 也沒有辦法繪制到 1.5 停止
那么就只能是從 0 開始繪制到 2
所以線寬就會變成 2px 了
因為本身一個像素的黑色被強制拉伸到兩個像素寬度, 所以顏色就會變淺
就像我們一杯墨水, 倒在一個杯子里面就是黑色
但是到在一個杯子里面的時候, 又倒進去一杯水, 顏色就會變淺
○實際描繪出來的樣子
○這就變成了我們剛才看到的樣子
●所以, 我們在進行 canvas 繪制內(nèi)容的時候, 涉及到線段的時候
●我們一般不會把線段寬度設(shè)置成奇數(shù), 一般都是偶數(shù)的
canvas 繪制平行線
●剛才我們繪制了線段, 接下來我們來繪制一個平行線, 也就是兩個線段
●小伙伴: " 一個簡單的效果, 想到就搞 "
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制第一個線段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 開始繪制第二個線段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
●沒有問題, 效果實現(xiàn)了
●接下來, 咱們稍微增加一下需求
○第一個線段線寬 2px, 黑色
○第二個線段線寬 10px, 紅色
●這也簡單啊, 稍微修改一下
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制第一個線段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 開始繪制第二個線段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 10
ctx.strokeStyle = 'red'
ctx.stroke()
●這是什么鬼, 為什么兩個線段都變了, 不是應(yīng)該只改變一個嗎 ?
這是因為我們并沒有告訴他這是兩個不一樣的線段
所以在設(shè)置線段樣式的時候, 會默認按照最后一次設(shè)置的樣式來繪制所有的線段
我們要想讓第一個線段繪制完畢以后, 和第二個沒有關(guān)系
我們需要告訴畫布, 我的這個線段結(jié)束了, 后面的不要和我扯上關(guān)系
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制第一個線段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
ctx.stroke()
// 3. 結(jié)束之前的繪制內(nèi)容
// 語法: 工具箱.beginPath()
ctx.beginPath()
// 4.. 開始繪制第二個線段
ctx.moveTo(100, 100)
ctx.lineTo(300, 100)
ctx.lineWidth = 10
ctx.strokeStyle = 'red'
ctx.stroke()
●這樣才是我們的需求
canvas 繪制三角形
●畫完了線段, 咱們就來畫一個簡單的圖形, 畫一個三角形
●其實就是由三個線段組成, 用三個線段圍成一個封閉圖形即可
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制第一個線段
// 三角形第一個點
ctx.moveTo(100, 100)
// 三角形第二個點
ctx.lineTo(200, 100)
// 三角形第三個點
ctx.lineTo(200, 200)
// 回到第一個點
ctx.lineTo(100, 100)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
// 描邊
ctx.stroke()
●看似沒啥問題, 一個三角形就出來了
●但是我們仔細觀察一下三角形的第一個角
●因為這是兩個線段, 只是畫到了一個點, 不可能重疊出一個 尖兒~~
●這個時候, 我們就不能這樣繪制三角形了
當我們要繪制閉合圖形的時候
我們不要手動繪制最后一個路徑, 而是描述出形狀
通過 canvas 讓他自動閉合
●首先, 我們繪制出形狀, 不要閉合最終路徑
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制第一個線段
// 三角形第一個點
ctx.moveTo(100, 100)
// 三角形第二個點
ctx.lineTo(200, 100)
// 三角形第三個點
ctx.lineTo(200, 200)
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
// 描邊
ctx.stroke()
●接下來, 讓 canvas 來幫我們閉合這個封閉圖形
// 0. 獲取到頁面上的 canvas 標簽元素節(jié)點
const canvasEle = document.querySelector('#canvas')
// 1. 獲取當前這個畫布的工具箱
const ctx = canvasEle.getContext('2d')
// 2. 開始繪制第一個線段
ctx.moveTo(100, 100)
ctx.lineTo(200, 100)
ctx.lineTo(200, 200)
// 自動閉合圖形
// 語法: 工具箱.closePath()
ctx.closePath()
ctx.lineWidth = 2
ctx.strokeStyle = '#000'
// 描邊
ctx.stroke()
●這個時候, 我們發(fā)現(xiàn)一個正常的三角形就出現(xiàn)了
●注意 : 閉合路徑
closePath() 這個方法
是從當前坐標點, 直接用線段的方式連接到 modeTo() 的位置
也就是從當前坐標點直接連接到開始坐標點
在討論性能優(yōu)化之前,我們有必要了解一些瀏覽器的渲染原理。不同的瀏覽器進行渲染有著不同的實現(xiàn)方式,但是大體流程都是差不多的,我們通過 Chrome 瀏覽器來大致了解一下這個渲染流程。
關(guān)鍵渲染路徑是指瀏覽器將 HTML、CSS 和 JavaScript 轉(zhuǎn)換成實際運作的網(wǎng)站必須采取的一系列步驟,通過渲染流程圖我們可以大致概括如下:
DOM(Document Object Model——文檔對象模型)是用來呈現(xiàn)以及與任意 HTML 或 XML 交互的 API 文檔。DOM 是載入到瀏覽器中的文檔模型,它用節(jié)點樹的形式來表現(xiàn)文檔,每個節(jié)點代表文檔的構(gòu)成部分。
需要說明的是 DOM 只是構(gòu)建了文檔標記的屬性和關(guān)系,并沒有說明元素需要呈現(xiàn)的樣式,這需要 CSSOM 來處理。
獲取到 HTML 字節(jié)數(shù)據(jù)后,會通過以下流程構(gòu)建 DOM Tree:
詞法分析和語法分析在每次處理 HTML 字符串時都會執(zhí)行這個過程,比如使用 document.write 方法。
HTML 結(jié)構(gòu)不算太復雜,大部分情況下識別的標記會有開始標記、內(nèi)容標記和結(jié)束標記,對應(yīng)一個 HTML 元素。除此之外還有 DOCTYPE、Comment、EndOfFile 等標記。
標記化是通過狀態(tài)機來實現(xiàn)的,狀態(tài)機模型在 W3C 中已經(jīng)定義好了。
想要得到一個標記,必須要經(jīng)歷一些狀態(tài),才能完成解析。我們通過一個簡單的例子來了解一下流程。
<a href="www.w3c.org">W3C</a>
通過上面這個例子,可以發(fā)現(xiàn)屬性是開始標記的一部分。
在創(chuàng)建解析器后,會關(guān)聯(lián)一個 Document 對象作為根節(jié)點。
我會簡單介紹一下流程,具體的實現(xiàn)過程可以在 Tree construction 查看。
解析器在運行過程中,會對 Tokens 進行迭代;并根據(jù)當前 Token 的類型轉(zhuǎn)換到對應(yīng)的模式,再在當前模式下處理 Token;此時,如果 Token 是一個開始標記,就會創(chuàng)建對應(yīng)的元素,添加到 DOM Tree 中,并壓入還未遇到結(jié)束標記的開始標記棧中;此棧的主要目的是實現(xiàn)瀏覽器的容錯機制,糾正嵌套錯誤,具體的策略在 W3C 中定義。更多標記的處理可以在 狀態(tài)機算法 中查看。
在構(gòu)建 DOM Tree 的過程中,如果遇到 link 標記,瀏覽器就會立即發(fā)送請求獲取樣式文件。當然我們也可以直接使用內(nèi)聯(lián)樣式或嵌入樣式,來減少請求;但是會失去模塊化和可維護性,并且像緩存和其他一些優(yōu)化措施也無效了,利大于弊,性價比實在太低了;除非是為了極致優(yōu)化首頁加載等操作,否則不推薦這樣做。
CSS 的加載和解析并不會阻塞 DOM Tree 的構(gòu)建,因為 DOM Tree 和 CSSOM Tree 是兩棵相互獨立的樹結(jié)構(gòu)。但是這個過程會阻塞頁面渲染,也就是說在沒有處理完 CSS 之前,文檔是不會在頁面上顯示出來的,這個策略的好處在于頁面不會重復渲染;如果 DOM Tree 構(gòu)建完畢直接渲染,這時顯示的是一個原始的樣式,等待 CSSOM Tree 構(gòu)建完畢,再重新渲染又會突然變成另外一個模樣,除了開銷變大之外,用戶體驗也是相當差勁的。另外 link 標記會阻塞 JavaScript 運行,在這種情況下,DOM Tree 是不會繼續(xù)構(gòu)建的,因為 JavaScript 也會阻塞 DOM Tree 的構(gòu)建,這就會造成很長時間的白屏。
通過一個例子來更加詳細的說明:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script>
var startDate = new Date();
</script>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
<script>
console.log("link after script", document.querySelector("h2"));
console.log("經(jīng)過 " + (new Date() - startDate) + " ms");
</script>
<title>性能</title>
</head>
<body>
<h1>標題</h1>
<h2>標題2</h2>
</body>
</html>
首先需要在 Chrome 控制臺的 Network 面板設(shè)置網(wǎng)絡(luò)節(jié)流,讓網(wǎng)絡(luò)速度變慢,以便更好進行調(diào)試。
下圖說明 JavaScript 的確需要在 CSS 加載并解析完畢之后才會執(zhí)行。
因為 JavaScript 可以操作 DOM 和 CSSOM,如果 link 標記不阻塞 JavaScript 運行,這時 JavaScript 操作 CSSOM,就會發(fā)生沖突。更詳細的說明可以在 使用 JavaScript 添加交互 這篇文章中查閱。
CSS 解析的步驟與 HTML 的解析是非常類似的。
CSS 會被拆分成如下一些標記:
函數(shù)形式是需要再次計算的,在進行詞法分析時會將它變成一個函數(shù)標記,由此看來使用十六進制的確有所優(yōu)化。
每個 CSS 文件或嵌入樣式都會對應(yīng)一個 CSSStyleSheet 對象(authorStyleSheet),這個對象由一系列的 Rule(規(guī)則) 組成;每一條 Rule 都會包含 Selectors(選擇器) 和若干 Declearation(聲明),Declearation 又由 Property(屬性)和 Value(值)組成。另外,瀏覽器默認樣式表(defaultStyleSheet)和用戶樣式表(UserStyleSheet)也會有對應(yīng)的 CSSStyleSheet 對象,因為它們都是單獨的 CSS 文件。至于內(nèi)聯(lián)樣式,在構(gòu)建 DOM Tree 的時候會直接解析成 Declearation 集合。
所有的 authorStyleSheet 都掛載在 document 節(jié)點上,我們可以在瀏覽器中通過 document.styleSheets 獲取到這個集合。內(nèi)聯(lián)樣式可以直接通過節(jié)點的 style 屬性查看。
通過一個例子,來了解下內(nèi)聯(lián)樣式和 authorStyleSheet 的區(qū)別:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
body .div1 {
line-height: 1em;
}
</style>
<link rel="stylesheet" href="./style.css">
<style>
.div1 {
background-color: #f0f;
height: 20px;
}
</style>
<title>Document</title>
</head>
<body>
<div class="div1" >test</div>
</body>
</html>
可以看到一共有三個 CSSStyleSheet 對象,每個 CSSStyleSheet 對象的 rules 里面會有一個 CSSStyleDeclaration,而內(nèi)聯(lián)樣式獲取到的直接就是 CSSStyleDeclaration。
在解析 Declearation 時遇到屬性合并,會把單條聲明轉(zhuǎn)變成對應(yīng)的多條聲明,比如:
.box {
margin: 20px;
}
margin: 20px 就會被轉(zhuǎn)變成四條聲明;這說明 CSS 雖然提倡屬性合并,但是最終還是會進行拆分的;所以屬性合并的作用應(yīng)該在于減少 CSS 的代碼量。
因為一個節(jié)點可能會有多個 Selector 命中它,這就需要把所有匹配的 Rule 組合起來,再設(shè)置最后的樣式。
為了便于計算,在生成 CSSStyleSheet 對象后,會把 CSSStyleSheet 對象最右邊 Selector 類型相同的 Rules 存放到對應(yīng)的 Hash Map 中,比如說所有最右邊 Selector 類型是 id 的 Rules 就會存放到 ID Rule Map 中;使用最右邊 Selector 的原因是為了更快的匹配當前元素的所有 Rule,然后每條 Rule 再檢查自己的下一個 Selector 是否匹配當前元素。
idRules
classRules
tagRules
...
*
一個節(jié)點想要獲取到所有匹配的 Rule,需要依次判斷 Hash Map 中的 Selector 類型(id、class、tagName 等)是否匹配當前節(jié)點,如果匹配就會篩選當前 Selector 類型的所有 Rule,找到符合的 Rule 就會放入結(jié)果集合中;需要注意的是通配符總會在最后進行篩選。
上文說過 Hash Map 存放的是最右邊 Selector 類型的 Rule,所以在查找符合的 Rule 最開始,檢驗的是當前 Rule 最右邊的 Selector;如果這一步通過,下面就要判斷當前的 Selector 是不是最左邊的 Selector;如果是,匹配成功,放入結(jié)果集合;否則,說明左邊還有 Selector,遞歸檢查左邊的 Selector 是否匹配,如果不匹配,繼續(xù)檢查下一個 Rule。
先思考一下正向匹配是什么流程,我們用 div p .yellow 來舉例,先查找所有 div 節(jié)點,再向下查找后代是否是 p 節(jié)點,如果是,再向下查找是否存在包含 class="yellow" 的節(jié)點,如果存在則匹配;但是不存在呢?就浪費一次查詢,如果一個頁面有上千個 div 節(jié)點,而只有一個節(jié)點符合 Rule,就會造成大量無效查詢,并且如果大多數(shù)無效查詢都在最后發(fā)現(xiàn),那損失的性能就實在太大了。
這時再思考從右向左匹配的好處,如果一個節(jié)點想要找到匹配的 Rule,會先查詢最右邊 Selector 是當前節(jié)點的 Rule,再向左依次檢驗 Selector;在這種匹配規(guī)則下,開始就能避免大多無效的查詢,當然性能就更好,速度更快了。
設(shè)置樣式的順序是先繼承父節(jié)點,然后使用用戶代理的樣式,最后使用開發(fā)者(authorStyleSheet)的樣式。
放入結(jié)果集合的同時會計算這條 Rule 的優(yōu)先級;來看看 blink 內(nèi)核對優(yōu)先級權(quán)重的定義:
switch (m_match) {
case Id:
return 0x010000;
case PseudoClass:
return 0x000100;
case Class:
case PseudoElement:
case AttributeExact:
case AttributeSet:
case AttributeList:
case AttributeHyphen:
case AttributeContain:
case AttributeBegin:
case AttributeEnd:
return 0x000100;
case Tag:
return 0x000001;
case Unknown:
return 0;
}
return 0;
因為解析 Rule 的順序是從右向左進行的,所以計算優(yōu)先級也會按照這個順序取得對應(yīng) Selector 的權(quán)重后相加。來看幾個例子:
/*
* 65793 = 65536 + 1 + 256
*/
#container p .text {
font-size: 16px;
}
/*
* 2 = 1 + 1
*/
div p {
font-size: 14px;
}
當前節(jié)點所有匹配的 Rule 都放入結(jié)果集合之后,先根據(jù)優(yōu)先級從小到大排序,如果有優(yōu)先級相同的 Rule,則比較它們的位置。
authorStyleSheet 的 Rule 處理完畢,才會設(shè)置內(nèi)聯(lián)樣式;內(nèi)聯(lián)樣式在構(gòu)建 DOM Tree 的時候就已經(jīng)處理完成并存放到節(jié)點的 style 屬性上了。
內(nèi)聯(lián)樣式會放到已經(jīng)排序的結(jié)果集合最后,所以如果不設(shè)置 !important,內(nèi)聯(lián)樣式的優(yōu)先級是最大的。
在設(shè)置 !important 的聲明前,會先設(shè)置不包含 !important 的所有聲明,之后再添加到結(jié)果集合的尾部;因為這個集合是按照優(yōu)先級從小到大排序好的,所以 !important 的優(yōu)先級就變成最大的了。
結(jié)果集合最后會生成 ComputedStyle 對象,可以通過 window.getComputedStyle 方法來查看所有聲明。
可以發(fā)現(xiàn)圖中的聲明是沒有順序的,說明書寫規(guī)則的最大作用是為了良好的閱讀體驗,利于團隊協(xié)作。
這一步會調(diào)整相關(guān)的聲明;例如聲明了 position: absolute;,當前節(jié)點的 display 就會設(shè)置成 block。
在 DOM Tree 和 CSSOM Tree 構(gòu)建完畢之后,才會開始生成 Render Object Tree(Document 節(jié)點是特例)。
在創(chuàng)建 Document 節(jié)點的時候,會同時創(chuàng)建一個 Render Object 作為樹根。Render Object 是一個描述節(jié)點位置、大小等樣式的可視化對象。
每個非 display: none | contents 的節(jié)點都會創(chuàng)建一個 Render Object,流程大致如下:生成 ComputedStyle(在 CSSOM Tree 計算這一節(jié)中有講),之后比較新舊 ComputedStyle(開始時舊的 ComputedStyle 默認是空);不同則創(chuàng)建一個新的 Render Object,并與當前處理的節(jié)點關(guān)聯(lián),再建立父子兄弟關(guān)系,從而形成一棵完整的 Render Object Tree。
Render Object 在添加到樹之后,還需要重新計算位置和大小;ComputedStyle 里面已經(jīng)包含了這些信息,為什么還需要重新計算呢?因為像 margin: 0 auto; 這樣的聲明是不能直接使用的,需要轉(zhuǎn)化成實際的大小,才能通過繪圖引擎繪制節(jié)點;這也是 DOM Tree 和 CSSOM Tree 需要組合成 Render Object Tree 的原因之一。
布局是從 Root Render Object 開始遞歸的,每一個 Render Object 都有對自身進行布局的方法。為什么需要遞歸(也就是先計算子節(jié)點再回頭計算父節(jié)點)計算位置和大小呢?因為有些布局信息需要子節(jié)點先計算,之后才能通過子節(jié)點的布局信息計算出父節(jié)點的位置和大小;例如父節(jié)點的高度需要子節(jié)點撐起。如果子節(jié)點的寬度是父節(jié)點高度的 50%,要怎么辦呢?這就需要在計算子節(jié)點之前,先計算自身的布局信息,再傳遞給子節(jié)點,子節(jié)點根據(jù)這些信息計算好之后就會告訴父節(jié)點是否需要重新計算。
所有相對的測量值(rem、em、百分比...)都必須轉(zhuǎn)換成屏幕上的絕對像素。如果是 em 或 rem,則需要根據(jù)父節(jié)點或根節(jié)點計算出像素。如果是百分比,則需要乘以父節(jié)點寬或高的最大值。如果是 auto,需要用 (父節(jié)點的寬或高 - 當前節(jié)點的寬或高) / 2 計算出兩側(cè)的值。
眾所周知,文檔的每個元素都被表示為一個矩形的盒子(盒模型),通過它可以清晰的描述 Render Object 的布局結(jié)構(gòu);在 blink 的源碼注釋中,已經(jīng)生動的描述了盒模型,與原先耳熟能詳?shù)牟煌瑵L動條也包含在了盒模型中,但是滾動條的大小并不是所有的瀏覽器都能修改的。
// ***** THE BOX MODEL *****
// The CSS box model is based on a series of nested boxes:
// http://www.w3.org/TR/CSS21/box.html
// top
// |----------------------------------------------------|
// | |
// | margin-top |
// | |
// | |-----------------------------------------| |
// | | | |
// | | border-top | |
// | | | |
// | | |--------------------------|----| | |
// | | | | | | |
// | | | padding-top |####| | |
// | | | |####| | |
// | | | |----------------| |####| | |
// | | | | | | | | |
// left | ML | BL | PL | content box | PR | SW | BR | MR |
// | | | | | | | | |
// | | | |----------------| | | | |
// | | | | | | |
// | | | padding-bottom | | | |
// | | |--------------------------|----| | |
// | | | ####| | | |
// | | | scrollbar height ####| SC | | |
// | | | ####| | | |
// | | |-------------------------------| | |
// | | | |
// | | border-bottom | |
// | | | |
// | |-----------------------------------------| |
// | |
// | margin-bottom |
// | |
// |----------------------------------------------------|
//
// BL = border-left
// BR = border-right
// ML = margin-left
// MR = margin-right
// PL = padding-left
// PR = padding-right
// SC = scroll corner (contains UI for resizing (see the 'resize' property)
// SW = scrollbar width
box-sizing: content-box | border-box,content-box 遵循標準的 W3C 盒子模型,border-box 遵守 IE 盒子模型。
它們的區(qū)別在于 content-box 只包含 content area,而 border-box 則一直包含到 border。通過一個例子說明:
// width
// content-box: 40
// border-box: 40 + (2 * 2) + (1 * 2)
div {
width: 40px;
height: 40px;
padding: 2px;
border: 1px solid #ccc;
}
Render Layer 是在 Render Object 創(chuàng)建的同時生成的,具有相同坐標空間的 Render Object 屬于同一個 Render Layer。這棵樹主要用來實現(xiàn)層疊上下文,以保證用正確的順序合成頁面。
滿足層疊上下文條件的 Render Object 一定會為其創(chuàng)建新的 Render Layer,不過一些特殊的 Render Object 也會創(chuàng)建一個新的 Render Layer。
創(chuàng)建 Render Layer 的原因如下:
另外以下 DOM 元素對應(yīng)的 Render Object 也會創(chuàng)建單獨的 Render Layer:
如果是 NoLayer 類型,那它并不會創(chuàng)建 Render Layer,而是與其第一個擁有 Render Layer 的父節(jié)點共用一個。
軟件渲染是瀏覽器最早采用的渲染方式。在這種方式中,渲染是從后向前(遞歸)繪制 Render Layer 的;在繪制一個 Render Layer 的過程中,它的 Render Objects 不斷向一個共享的 Graphics Context 發(fā)送繪制請求來將自己繪制到一張共享的位圖中。
有些特殊的 Render Layer 會繪制到自己的后端存儲(當前 Render Layer 會有自己的位圖),而不是整個網(wǎng)頁共享的位圖中,這些 Layer 被稱為 Composited Layer(Graphics Layer)。最后,當所有的 Composited Layer 都繪制完成之后,會將它們合成到一張最終的位圖中,這一過程被稱為 Compositing;這意味著如果網(wǎng)頁某個 Render Layer 成為 Composited Layer,那整個網(wǎng)頁只能通過合成來渲染。除此之外,Compositing 還包括 transform、scale、opacity 等操作,所以這就是硬件加速性能好的原因,上面的動畫操作不需要重繪,只需要重新合成就好。
上文提到軟件渲染只會有一個 Graphics Context,并且所有的 Render Layer 都會使用同一個 Graphics Context 繪制。而硬件渲染需要多張位圖合成才能得到一張完整的圖像,這就需要引入 Graphics Layer Tree。
Graphics Layer Tree 是根據(jù) Render Layer Tree 創(chuàng)建的,但并不是每一個 Render Layer 都會有對應(yīng)的 Composited Layer;這是因為創(chuàng)建大量的 Composited Layer 會消耗非常多的系統(tǒng)內(nèi)存,所以 Render Layer 想要成為 Composited Layer,必須要給出創(chuàng)建的理由,這些理由實際上就是在描述 Render Layer 具備的特征。如果一個 Render Layer 不是 Compositing Layer,那就和它的祖先共用一個。
每一個 Graphics Layer 都會有對應(yīng)的 Graphics Context。Graphics Context 負責輸出當前 Render Layer 的位圖,位圖存儲在系統(tǒng)內(nèi)存中,作為紋理(可以理解為 GPU 中的位圖)上傳到 GPU 中,最后 GPU 將多張位圖合成,然后繪制到屏幕上。因為 Graphics Layer 會有單獨的位圖,所以在一般情況下更新網(wǎng)頁的時候硬件渲染不像軟件渲染那樣重新繪制相關(guān)的 Render Layer;而是重新繪制發(fā)生更新的 Graphics Layer。
Render Layer 提升為 Composited Layer 的理由大致概括如下,更為詳細的說明可以查看 無線性能優(yōu)化:Composite —— 從 PaintLayers 到 GraphicsLayers。
由于重疊的原因,可能會產(chǎn)生大量的 Composited Layer,就會浪費很多資源,嚴重影響性能,這個問題被稱為層爆炸。瀏覽器通過 Layer Squashing(層壓縮)處理這個問題,當有多個 Render Layer 與 Composited Layer 重疊,這些 Render Layer 會被壓縮到同一個 Composited Layer。來看一個例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
div {
position: absolute;
width: 100px;
height: 100px;
}
.div1 {
z-index: 1;
top: 10px;
left: 10px;
will-change: transform;
background-color: #f00;
}
.div2 {
z-index: 2;
top: 80px;
left: 80px;
background-color: #f0f;
}
.div3 {
z-index: 2;
top: 100px;
left: 100px;
background-color: #ff0;
}
</style>
<title>Document</title>
</head>
<body>
<div class="div1"></div>
<div class="div2"></div>
<div class="div3"></div>
</body>
</html>
可以看到后面兩個節(jié)點重疊而壓縮到了同一個 Composited Layer。
有一些不能被壓縮的情況,可以在 無線性能優(yōu)化:Composite —— 層壓縮 中查看。
果如下:
高清大圖!
不同分值效果如下:
我們來看產(chǎn)品的制作過程吧!
1、vue中,<template lang="pug">里的代碼如下:
canvas#baseCanvas是底部的灰色圓環(huán)
canvas#myCanvas是上邊的彩色圓環(huán)
需要用css樣式幫助我們把彩色圓環(huán)蓋到灰色圓環(huán)上邊。
2、css樣式:
3、js-canvas的樣式繪制代碼
這段代碼也很簡單,看canvas的api即可
3-1、vue組件中,script標簽頂部定義需要用的變量
3-2、vue的methos對象中,定義方法三個:
drawBaseCanvas:用來繪制底部灰色圓環(huán)。由于灰色圓環(huán)沒有動畫效果,所以一開始就繪制一個完整的灰色圓環(huán)即可。
drawClrCanvas:用來繪制上邊的彩色圓環(huán)。
clearCanvas:用來清空畫布。這是彩色圓環(huán)動畫需要。
因為我們圓環(huán)動畫效果的核心就是,每隔一段時間就把彩色圓環(huán)清空一下,然后把結(jié)束角度值增大、重畫,這樣連續(xù)起來就是動畫。
以下是三個方法的代碼:
上邊三個方法里邊的代碼,幾乎都是對canvas API的應(yīng)用,看教程即可。
只有draoClrCanvas方法中,canvas圓形的繪制時,arc的參數(shù)里關(guān)于開始值、結(jié)束值的設(shè)置。
開始值決定了圓環(huán)的起始繪制位置,結(jié)束值決定了結(jié)束的位置,這個結(jié)束值的計算,對于我來說還是比較麻煩的。
this.grade是100以內(nèi)的正整數(shù),表示分值。被定義在data中,默認是0分。
所以一開始彩色圓環(huán)就看不見,因為起始點和結(jié)束點都是0點。
如果更改grade的值,從0-100,canvas彩色圓環(huán)的值也就會更改。
這樣,只要我們逐漸修改grade的值,重新繪制,彩色圓環(huán)就會逐漸遞增,實現(xiàn)動畫效果。
由于我這里需求特殊,需要用戶每次翻到canvas所在swiper時,才會觸發(fā)動畫(后來更麻煩一點需要柱狀圖和canvas部分有個入場效果后,動畫才開始。效果就是上圖中最長的那張gif動畫那樣)。
所以我得借助swiper才能實現(xiàn)。在swiper切換的回調(diào)函數(shù)中,從0開始不停遞增grade分數(shù),并重新觸發(fā)彩色圓環(huán)的繪制,進而實現(xiàn)動畫效果。
vue中我用的swiper是'vue-awesome-swiper'。她的用法我在其他文章中寫過步驟。
swiper在vue-data中的配置里,有一個on對象。在on對象中的slideChange函數(shù),就是每次翻頁swiper時會觸發(fā)的回調(diào)函數(shù)。
這里我說一下幾個比較特殊的點:
(1)vm:是我早就在vue的script中存儲的變量,初始化為null,然后在mounted中,將其賦值為vue實例對象。
初始化數(shù)據(jù)、繪制灰色圓環(huán)
通過這種方法,我在vue實例對象 - data - swiper - 回調(diào)函數(shù)中去拿vue實例對象 - data中的grade和gradeTarget屬性值,并對其進行修改。
(2) (this.activeIndex == 2 && vm.isStar) || (this.activeIndex == 1 && !vm.isStar)
這里是因為業(yè)務(wù),才這么判斷,可以忽略。
this在swiperChange函數(shù)中指向swiper對象。this.activeIndex是swiper實例的屬性,用官方的話說“返回當前活動塊(激活塊)的索引。”可以理解他指的是當前翻到的是哪一頁,就是當前你所看的swiper-slide的下標。
我因為用戶的身份,會判斷性的決定當前canvas所在swiper前一頁是否展示。 如果不展示就根本不會繪制前一頁,那么相應(yīng)的當前頁的swiper的下標就會變成(index-1)。
總而言之,當滿足條件、用戶翻到canvas所在swiper頁面后,我就要觸發(fā)if里邊的圓環(huán)繪制邏輯。否則就走到else里初始化數(shù)據(jù)頁面的狀態(tài)、清除定時器暫停動畫、并把彩色圓環(huán)清空。
(3)vm.aniShow
在我上篇《純css繪制柱狀圖》里邊說了,柱狀圖的動畫要跟canvas的動畫一起說。因為他們的動畫實現(xiàn)需要配合swiper的切換。說的就是這里的代碼:
vue - data - aniShow屬性變?yōu)閠rue時,div.row就會添加ani這個class類名:
同樣,aniShow為true,progress的高度就會附上自己的目標值,也就是這個progress的實際高度經(jīng)過百分制轉(zhuǎn)化后被賦予給了style屬性的height。(具體換算規(guī)則還是見上篇《純css繪制柱狀圖》)
此時,因為progress的transition監(jiān)聽了height變化,就開始有了高度漸增的柱狀圖遞增動畫了。
而ani類名下,progress的transition-delay實現(xiàn)了其高度錯開遞增效果。
可能只看文字描述很晦澀,再看一眼效果:
(4)彩色圓環(huán)繪制代碼部分
gradeTarget是實際分值,是最終要繪制到的結(jié)果。
grade從0開始,自增到gradeTarget的大小。
這里我沒有直接++vm.grade,我也不知道自己當時咋想的。
if判斷,如果grade遞增到了目標值gradeTarget或者大于目標值,就停止遞增,并讓grade=gradeTarget。屬于臨界值的判斷。在運動功能中,又算碰撞檢測。
反之,不到目標的話,就清除上一次繪制的canvas畫布,在grade遞增變化后重新繪制新的彩色圓環(huán)。
(5)所有這些放到setTimeout中,暫停500毫秒再執(zhí)行,是為了等柱圖和環(huán)圖入場后,在開始繪制圓環(huán)的遞增效果。
其實上邊代碼都是很簡單的邏輯處理,看官們讀一遍代碼應(yīng)該就差不離了。
新想法:
這個效果是我很久以前做的,今天在整理制作方法的時候,我想到自己代碼的一種優(yōu)化方案:
其實沒必要在定時器里重新調(diào)用彩色圓環(huán)繪制方法。我們直接改的是this.grade屬性,監(jiān)聽這個屬性的改變就好了其實。這樣此屬性在定時器中被修改,圓環(huán)方法就會自動執(zhí)行。
這還是一個想法,還需要我的實踐。
因為grade是每次遞增的分數(shù),所以利用vue的雙向數(shù)據(jù)綁定,直接把grade當作分數(shù)值綁定到對應(yīng)dom視圖處即可。
最后,圓環(huán)和上邊柱狀圖的動畫結(jié)合,就是animation控制一下動畫延遲即可。很簡單的。
index.vue源碼:
(注,源碼稍作整理,單獨提取。為了完整性也為了保護其他業(yè)務(wù)代碼,部分變量名做了修改,可能會和之前截圖中略微不同)
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。