《高性能javascript》一書要點和延伸(下)

第六章 快速響應的用戶界面php

本章開篇介紹了瀏覽器UI線程的概念,我也忽然想到一個小例子,這是寫css3動畫的朋友都常常會碰到的一個問題:css

<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),html

不過這段代碼的執行效果是——沒有效果(錄屏軟件在win10下有點兼容bug,鼠標都偏移了)前端

其解決方案卻也簡單——套上一個setTimeout便可:node

    btn.onclick = function(){
        div.className = '';
        setTimeout(function(){
            div.className = 'act';
        }, 0)
    }

執行以下:jquery

原理是,咱們經過 setTimeout,將div的第一次UI事件得以優先執行,而非放到 div.className = 'act' 的後方執行。css3

在用戶點擊按鈕(未加setTimeout時的代碼)的時候其實發生了這樣的事情:web

⑴ UI事件——更新按鈕的UI,讓用戶能「看到」它被點擊了。同時把回調事件放入事件隊列。編程

⑵ JS事件A——執行回調事件,先執行首行的 div.className = '' ,移除div的類名,這時候會生成一個UI事件A(重渲染div)放入事件隊列中等候空閒。json

⑶ 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」實際上是域名收斂的一種實現,好比下面的單條請求就一口氣返回了對應的多個腳本資源:

http://imgcache.gtimg.cn/c/=/club/qv/pkg/qv_1.x.x.js,/clubact/premin/jquery.webStorage.min.js,/clubact/common/oz.js,/clubact/common/aid.js,/clubact/common/mustache.js,/clubact/common/nav/youxi_nav.js

不過這裏說起了一個有趣的處理——若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。

 

至於後面兩章主要說起的是前端構建和檢測工具,其中部分技術仍是淘汰掉的東西就不贅述了。共勉~

donate

相關文章
相關標籤/搜索