第六章 快速響應的用戶界面
本章開篇介紹了瀏覽器UI線程的概念,我也突然想到一個小例子,這是寫css3動畫的朋友都經常會碰到的一個問題:
<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即可:
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時其實表示客戶端已經開始下載回包(含報頭)了,這時候我們就可以通過輪詢來提前處理(主要是拆開、提取回包中的合並資源):
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 直接量
//不推薦 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. 避免重復工作
大部分開發都會忽略的地方,即封裝在某個方法中的功能分支判斷,在每次方法被調用的時候都會重新做一次冗余判斷:
function addHandler(target, eventType, handler){ if(target.addEventListener){ target.addEventListener(eventType, handler, false) } else { target.attachEvent('on'+eventType, handler) } }
如上述的事件綁定接口在每次被調用時,都需要做一次事件添加句柄判斷。
解決該問題的方法是內部重寫接口(延遲加載):
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時,可以利用位掩碼來實現相同處理。
我們依舊使用上方的映射表,不過不再使用累加處理,而是使用位處理:
var wv = 16 | 4 | 1; //識別處理 if(wv & 1){ //隱藏返回按鈕 } if(wv & 2){ //隱藏分享按鈕 } ...//省略4和8的分支 if(wv & 16){ //分享面板隱藏空間分享 }
⑵ 原生方法
即多使用原生的 Math 接口來實現復雜的計算,多使用原生的選擇器(如 querySelector)來選擇DOM。
至於后面兩章主要提及的是前端構建和檢測工具,其中部分技術還是淘汰掉的東西就不贅述了。共勉~