第六章 快速響應的使用者介面
本章開篇介紹了瀏覽器UI執行緒的概念,我也突然想到一個小例子,這是寫css3動畫的朋友都經常會碰到的一個問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<head> <meta charset="UTF-8"> <title>Title</title> <style> div{width:50px; height:50px; background:yellow;} .act{width:100px;transition:width 0.5s;} </style> </head> <body> <div class="act"></div> <button>click me</button> <script> var btn = document.querySelector('button'); var div = document.querySelector('div'); btn.onclick = function(){ div.className = ''; div.className = 'act'; } </script> </body> |
如程式碼所示,我們希望點選按鈕的時候,div能通過移除class瞬間變回50px,然後再給其加回class來觸發動畫(0.5秒內,寬度由50px延伸到100px),
不過這段程式碼的執行效果是——沒有效果(錄屏軟體在win10下有點相容bug,滑鼠都偏移了):
其解決方案卻也簡單——套上一個setTimeout即可:
1 2 3 4 5 6 |
btn.onclick = function(){ div.className = ''; setTimeout(function(){ div.className = 'act'; }, 0) } |
執行如下:
原理是,我們通過 setTimeout,將div的第一次UI事件得以優先執行,而非放到 div.className = ‘act’ 的後方執行。
在使用者點選按鈕(未加setTimeout時的程式碼)的時候其實發生了這樣的事情:
⑴ UI事件——更新按鈕的UI,讓使用者能“看到”它被點選了。同時把回撥事件放入事件佇列。
⑵ JS事件A——執行回撥事件,先執行首行的 div.className = ” ,移除div的類名,這時候會生成一個UI事件A(重渲染div)放入事件佇列中等候空閒。
⑶ JS事件B——繼續執行回撥事件,給div加上名為“act”的類,這時候依舊又生成了一個UI事件B(重新渲染div)並放入佇列中等候。
⑷ UI事件A——鑑於瀏覽器的UI執行緒已不存在任何執行中的任務(回撥已執行完畢,處空閒狀態),那麼事件佇列中的UI事件便開始以FIFO的形式進入UI執行緒來被處理。
⑸ UI事件B——跟UI事件A是一樣的,即根據div的當前樣式來做渲染處理。
(製圖的時候沒記清楚,把事件A/B寫為事件1/2了,大家自行腦部替換吧)
而加上 setTimeout 之後則變為:
⑴ UI事件——更新按鈕的UI,讓使用者能“看到”它被點選了。同時把回撥事件(JS事件A和B)放入事件佇列。
⑵ JS事件A——執行回撥事件,先執行首行的 div.className = ” ,移除div的類名,這時候會生成一個UI事件A(重渲染div)放入事件佇列中等候空閒。
⑶ UI事件A——由於JS事件B帶延遲特性,故先放行事件佇列後方的佇列成員,讓UI事件A先執行。這時候div失去了類,依據當前有效樣式,將其渲染為50px寬度。
⑷ JS事件B——繼續執行回撥事件,給div加上名為“act”的類,依舊又生成了一個UI事件B(重新渲染div)並放入佇列中等候。
⑸ UI事件B——div加上了類,故根據當前的有效樣式,將其渲染為100px寬度。
⑹ UI事件C——鑑於div的寬度發生了變化,故觸發動畫事件。
綜上我們稍微瞭解了瀏覽器UI執行緒(主執行緒)的一個工作流程,但常規瀏覽器並不僅僅只有一個執行緒在運作,其主要執行緒可歸類為:
另外我們回過頭看看 setTimeout/setInterval 這兩個時間機制,它們實際上只是把回撥事件放入佇列中以“禮讓”的狀態等候,若後方有事件成員則禮讓給後方先出隊。
這點跟 node 的 setImmediate 是一樣的,不同的是 setImmediate 不受延時限制,當event loop當輪結束時則執行。
那麼給 setTimeout 配置一個數值為 0 的延時,是否就實現了 setImmediate 的功能呢?答案是否定的,在書中“定時器精度”一節有提及,js的時間機制是不精準的,它受到了系統/客戶端定時器解析度(如window下為15毫秒)的影響,所以會存在毫秒級的偏差。
不過這裡需要了解的事實是—— JS中的時間機制並不是一個純粹的非同步事件,它依舊走的UI單執行緒,只是當事件佇列為空時候才“見縫插針”到UI執行緒中去執行,營造出了一種“非同步”的假象。
順道也在這裡提一提,JS中真正走了非同步的應該是下面幾個事件:
1. Ajax
2. event(如監聽click)
3. requestAninmationFrame
4. WebSQL、IndexDB
5. Web Worker
6. postMessage
第七章 Ajax
“動態指令碼注入”一節介紹了JSONP原理——前後端約定好一個回撥名,讓script請求的回包資料包裹在該回撥名內,客戶端拉取到該回包時通過 eval 來即時觸發回撥函式。
除了 JSONP 我們還是能有許多跨域通訊的實現,可參照我的舊文章。
本章提及的“Multipart XHR”其實是域名收斂的一種實現,比如下面的單條請求就一口氣返回了對應的多個指令碼資源:
不過這裡提及了一個有趣的處理——若MXHR響應的出局非常多,等到全部資料返回過來才做處理有點慢,我們可以通過監聽XHR的 readyState 來提前處理。
當 readyState 為3時其實表示客戶端已經開始下載回包(含報頭)了,這時候我們就可以通過輪詢來提前處理(主要是拆開、提取回包中的合併資源):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var req = new XMLHttpRequest(); var getLatestPacketInterval, lastLength = 0; req.open('GET', 'rollup_images.php', true); req.onreadystatechange = readyStateHandler; req.send(null); function readyStateHandler{ if (req.readyState === 3 & getLatestPacketInterval === null) { // 開始輪詢 getLatestPacketInterval = window.setInterval(function() { getLatestPacket(); }, 15); } if (req.readyState === 4) { // 停止輪詢 clearInterval(getLatestPacketInterval); // 獲取最後一個資料包 getLatestPacket(); } } function getLatestPacket() { var length = req.responseText.length; var packet = req.responseText.substring(lastLength, length); processPacket(packet); lastLength = length; } |
接著提及的 Beacons 其實是一種 image ping 技術,常規也是用來跨域通訊的(主要用於統計)。不過這裡提及的服務端響應處理還是值得一看:
1. 服務端返回真實的圖片資料,客戶端可通過判斷圖片寬度來了解狀態;
2. 若客戶端無須瞭解服務端狀態,則返回不帶訊息正文的204即可。
第八章 程式設計實踐
本章提供一些建議,讓讀者能避免使用一些效能上不太好的程式設計習慣。
1. 避免雙重求值
js中提供了某些介面允許你輸入字串來編譯執行,eval是其中最耳熟能詳的方法了。除卻eval還包括如下方法:
⑴ 以 new Function() 的形式來建立函式; ⑵ 讓 setTimeout/setInterval 執行字串。
這些方法都會讓js引擎先做字串解析,再做求值處理,導致了雙重求值,效能開銷會變大,所以常規不建議這麼來使用。
如果不得已要解析服務端返回的大規模json字串,可以開個 Web Worker 做非同步處理。
2. 使用 Object/Array 直接量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//不推薦 var o = {}; o.a = 1; o.b = 2; //推薦 var o = { a: 1, b: 2 } //不推薦 var arr = new Array(); arr[0] = 1; arr[1] = 2; //推薦 var arr = [1, 2]; |
使用“推薦”的直接量處理來定義一個物件將獲得更快的執行速度也有助減小檔案體積。
3. 避免重複工作
大部分開發都會忽略的地方,即封裝在某個方法中的功能分支判斷,在每次方法被呼叫的時候都會重新做一次冗餘判斷:
1 2 3 4 5 6 7 |
function addHandler(target, eventType, handler){ if(target.addEventListener){ target.addEventListener(eventType, handler, false) } else { target.attachEvent('on'+eventType, handler) } } |
如上述的事件繫結介面在每次被呼叫時,都需要做一次事件新增控制程式碼判斷。
解決該問題的方法是內部重寫介面(延遲載入):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function addHandler(target, eventType, handler){ if(target.addEventListener){ addHandler = function(target, eventType, handler){ target.addEventListener(eventType, handler, false) } } else { addHandler = function(target, eventType, handler){ target.attachEvent('on'+eventType, handler) } } addHandler(target, eventType, handler); //延遲載入 } |
4. 用速度最快的部分
⑴ 位操作
JS的位操作會相比其它的計算處理快得多,若妥當使用可以提升指令碼執行速度。
例如常規我們會以 if(i%2) 來判斷 i 是奇數或偶數,若把條件更改為 if(i & 1) 會得到一樣的結果,不過速度快了50%。
本節也提及了“位掩碼”的使用,是種有趣的邏輯識別處理。
打個比方,在手Q web 頁面開發中,我們會通過一個“_wv”的引數來知會客戶端(手Q)是否顯示返回按鈕、分享按鈕,以及如何顯示分享皮膚等功能。
關於這個引數有類似這樣的對映:
當我們給 url 的 _wv 引數取值 21 (即 16 + 4 + 1)的時候,手Q針對該引數的值來隱藏返回按鈕和底欄,並配置分享皮膚中不出現空間的選項。
而常規我們在寫JS時,可以利用位掩碼來實現相同處理。
我們依舊使用上方的對映表,不過不再使用累加處理,而是使用位處理:
1 2 3 4 |
var wv = 16 | 4 | 1; //識別處理 if(wv |
⑵ 原生方法
即多使用原生的 Math 介面來實現複雜的計算,多使用原生的選擇器(如 querySelector)來選擇DOM。
至於後面兩章主要提及的是前端構建和檢測工具,其中部分技術還是淘汰掉的東西就不贅述了。共勉~