《高性能JavaScript》讀書筆記⑥:快速響應的用戶界面

瀏覽器UI線程(The Browser UI Thread)

  • 用于執行JavaScript代碼和更新界面的進程被稱為 “瀏覽器UI線程” 。
  • UI線程的工作基于一個簡單的隊列系統,任務會被保存到隊列中直到線程空閑,一旦空閑隊列就被重新提取出來運行。這些任務要么是運行JavaScript代碼,要么是執行UI更新,包括重繪和重排。瀏覽器每一次執行JavaScript代碼或者響應用戶事件,都可能會導致一個或多個任務加入隊列。
  • 案例:
<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title>Browser UI Thread Example</title>
 </head>
 <body>
 <button onclick="handleClick()">Click Me</button>
 <script type="text/javascript">
 function handleClick(){
 //創建div元素
 var div = document.createElement("div");
 //div元素添加內部文本
 div.innerHTML = "Clicked!";
 //將div元素append到body中
 document.body.appendChild(div);
 }
 </script>
 </body>
</html>

當按鈕被點擊時,會觸發UI線程創建兩個任務并添加到隊列中:

  • 第一個任務就是更新按鈕的UI,瀏覽器需要改變按鈕外觀表明它被點擊了;
  • 第二個任務就是執行handleClick()方法中的代碼。但是在運行過程中,創建了一個新的 <div> 元素并把它追加在 <body> 元素的末尾,這實際上引發了另一次UI變化,一個新的UI更新任務唄添加到隊列中。當方法運行完畢后,UI還會在更新一次。
  • 所有的UI線程任務執行完畢,線程進入空閑狀態是最理想的。因為用戶所有的交互都會立即出發UI更新,而事實上,大多數瀏覽器在JavaScript運行時,會停止把新任務添加到UI線程的隊列中,也就是說JavaScript任務必須盡快結束,以避免對用戶體驗造成不良影響。
瀏覽器限制(Browser Limits)
  • 瀏覽器對JavaScript任務的運行時間進行了限制,這很有必要,它能確保了不讓惡意代碼通過密集的操作鎖住用戶的瀏覽器。

  • 此類限制分為兩種:調用棧大小以及長時間運行(long-running)腳本限制。長時間腳本限制也稱為“長時間運行腳本定時器”或“失控腳本定時器”,顧名思義,它的原理就是瀏覽器記錄腳本的運行時間,當達到一定限度時終止它并彈出提示框,這是可用性的問題。對于開發者來說,應該避免此類情況的出現。

  • 長時間運行腳本限制 的實現方式主要分為兩種:

    1. 通過腳本執行的語句數量來限制(但要注意,在不同的機器,可用的內存和CPU速度執行單個語句的時間是不同的)
    2. 通過腳本執行的總時長來限制(但是,在不同的機器,指定時間內所運行的腳本數量,也有所差異)

    毫無疑問,不同瀏覽器檢測長時間運行腳本的方法也各不相同:

    • IE注冊表中,設置默認限制500萬條語句
      (HKEY_CURRENT_USER\Software\Microsoft\InternetExplorer\StylesMaxScriptStatements)
    • Firfox的默認限制為10秒(dom.max_script_run_time)
    • Safari的默認限制為5秒(Disable Runnaway JavaScript Timer)
    • Opera由于自身的架構,沒有長運行腳本限制
    • Chrome也沒有單獨的長運行腳本限制,而是依賴其通過崩潰檢測系統來處理此類問題
多久才算“太久”(How Long Is Too Long)
  • 即便主流瀏覽器有長運行腳本限制的機制,但不意味著開發者也允許自己代碼的運行時間達到最大限制值。單個JavaScript腳本運行的最大值不應該超過100毫秒 。
  • Jakob Nielsen 在其著作《可用性工程》(Morgan Kaufrnann,1944)中指出,如果界面超過100毫秒未響應,用戶會感覺自己與界面失去聯系。
  • 而主流的瀏覽器行為大致相同,當腳本執行時,UI不隨用戶交互而更新。也就是說,當運行JavaScript代碼的這段時間內,用戶交互行為引發的UI更新會被瀏覽器自動跳過。因此,在JavaScript腳本運行期間,用戶點擊一個按鈕,可能無法看到它已經被按下的樣式,界面呈現“掛起”或“假死”的狀態。

使用定時器讓出時間片段(Yielding with Timeers)

  • 在大型應用的Web界面的實現中,總會有一些復雜的JavaScript任務不能在100毫秒或更短的時間內完成。此時,最理想的方法是停止執行JavaScript腳本,讓出UI線程的控制權,使得UI可以更新,然后再繼續執行JavaScript。
定時器基礎(Timer Basics)
  • 在JavaScript中通過setTimeout()setInterval()方法創建定時器。其中,setTimeout()方法是創建一次性的定時器,setInterval()方法是創建周期性的定時器。
  • 定時器會重置瀏覽器長運行代碼的限制,正是這一特性使得定時器稱為長時間運行JavaScript代碼理想的跨瀏覽器解決方案。
    • 案例:saySomething()方法執行時,先調用oneMethod()方法,然后創建一個定時器,最后調用anotherMethod()方法
function saySomething(){
 oneMethod();
 setTimeout(function(){
 console.info("hello");
 },250);
 anotherMethod();
}

而定時器是創建(調用setTime())后當即開始計時,那么定時器代碼有可能在saySomething()方法處理完成之前執行完成。
如案例所示,如果anotherMethod()方法的執行時間超過50毫秒,那么定時器代碼會搶先進入UI執行隊列。

定時器的精度(Timer Precision)
  • 值得注意的是,當定時器倒計時完成后,往往不會立即執行,而是等待UI隊列中其他任務執行完畢后才會執行。這導致了JavaScript定時器的延遲問題,通常相差大約幾毫秒。正因為JavaScript的定時器不太精準,所以我們應該避免定時器用于測量實際時間。
  • 其中,Windows系統中定時器分辨率為15毫秒,這意味著, 在Windows系統中,設置定時器的時間間隔不能小于15毫秒,否則會導致瀏覽器鎖定。
使用定時器處理數組(Array Processing with Timers)
  • 一種造成長時間運行腳本的起因是耗時過長的循環,通過把循環的工作分解到一系列定時器中執行,是常見的優化方法。
  • 案例:這類循環執行時間過長的原因,不外乎是process()方法的處理太過復雜,或者是items數組的長度太長
var items = [], //數組
    len = items.length; //數組的長度
//遍歷數組中的元素作為參數,執行proccess方法    
for (var i=0;i<len;i++) {
 process(item[i]);
}

如果items數組不需要按順序處理,并且process()方法的處理過程不需要同步,則可以通過定時器來分解任務,即異步代碼模式:

var items = []; //原數組
var todo = items.concat();    //克隆原數組到todo
setTimeout(function(){
    //獲取todo數組的第一個元素,并返回刪除第一個元素后的數組
    procces(todo.shift());
 
    if(todo.length > 0){
    //arguments.callee表示當前執行的函數
    setTimeout(arguments.callee,25);
    }else{
    return items;
    }
},25);

由于分解任務帶來更多代碼,我們可以進一步封裝,以便多出重用:

var items = []; //原數組
function efficiencyLoopFn(items,process,callback){
       var todo = items.concat();    //克隆原數組到todo 
       setTimeout(function(){
    //獲取todo數組的第一個元素,并返回刪除第一個元素后的數組
    process(todo.shift());
    if(todo.length > 0){
    //arguments.callee表示當前執行的函數
    setTimeout(arguments.callee,25);
    }else{
    callback(items);
    }
       },25); 
}
分割任務(Splitting Up Tasks)
  • 如果一個函數運行時間太長,那么可以考慮把它拆分成一系列能在較短時間內完成的子函數。
function saveDocument(id){
       openDocument(id);  //進入
       writeText(id);    //寫入
       closeDocument(id);    //關閉i
       updateUI(id);    //更新界面
}

另外,還可以把獨立的方法通過定時器調用:

function saveDocument(id){
       var tasks = [openDocument,writeText,closeDocument,updateUI];
       setTimeout(function(){
              //執行下一個任務
              var task = tasks.shift();
              task(id);
              //檢查是否還有其他任務
              if(tasks.length >0){
                   setTimeout(arguments.callee,25);
              }else{
                   callback();
              }
       },25);
}

同樣可以封裝成一個公共調用的方法:

function multistep(steps,args,calback){
       var tasks = steps.concat();
       setTimeout(function(){
              //執行下一個任務
              var task = tasks.shift();
              task.apply(null,args || []);
              //檢查是否還有其他任務
              if(tasks.length >0){
                   setTimeout(arguments.callee,25);
              }else{
                   callback();
              }
       },25);
}
function saveDocument(id){

       var tasks =[openDocument,writeText,closeDocument,updateUI];
       multistep(tasks,[id],function(){
              console.info("Save completed!");
       });
}
記錄代碼運行時間(Timed Code)
  • 通過原生Date對象來跟蹤代碼的運行時間,是大多數JavaScript分析工具的工作原理
var start = +new Date(),stop;
comLongProcess();
//加號可以將Date對象轉換成數字
stop = +new Date();
if(stop - start < 50){
       console.info("Just about right");
}else{
       console.info("Taking too long");
}

為了執行JavaScript任務更高效,我們可以進一步優化上面的代碼,保證JavaScript代碼運行在50毫秒以內。

function timedProcessArray(items,process,callback){
       var todo = items.concat();//
       setTimeout(function(){
              var start = +new Date();
              do{
                     process(todo.shift());
              }while(todo.length>0 && (+new Date()-start < 50));

              if(todo.length > 0){
                     setTimeout(arguments.callee,25);
              }else{
                     callback();
              }
       },25);
}

定時器與性能(Timers and Performance)
  • 雖然可以通過定時器提升JavaScript代碼的性能,但過度使用也會對性能造成負面影響。等上一個定時器結束后再創建新的定時器不會導致性能問題,但同時有多個重復的定時器創建,則會發生性能問題。因為只有一個UI線程,而所有的定時器都在爭奪運行時間。

Web Workers

  • 在Web Workers出現之前,沒有辦法在瀏覽器UI線程之外運行代碼,而Web Workers API能使代碼運行且不占用瀏覽器UI線程的時間。這意味著,每個新的Workers都在自己的線程中運行代碼,不會影響其他Worker的運行代碼,并且不會影響瀏覽器UI。
  • Web Workers API已經作為HTML5標準的規范被Firefox、Chrome、Safari瀏覽器支持。
Worker運行環境(Woker Environment)
  • Workers運行環境由如下部分組成:
    • navigator對象,包括四個屬性appName、appVersion、user Agent和platform;
    • location對象(與window.location相同,只不過所有屬性是只讀的);
    • self對象,指向全局的worker對象;
    • importScript()方法,用來加載Worker所用到的外部JavaScript文件;
    • 以及所有的ECMAScript對象(諸如:Object、Array、Date等);
    • XMLHttpRequest構造器
    • setTimeout()setInterval()方法;
    • 停止WebWorker運行的close()方法;
  • 由于Web Workers擁有獨立的運行環境,因此我們需要在一個完全獨立的JavaScript文件中編寫Worker中運行的代碼,然后創建Woker線程中引用它。
var worker = new Worker("code.js");

當以上代碼執行時,會創建一個新的線程和一個新的Worker運行環境,code.js文件會被異步下載,等文件下載并執行完成后,啟動此Woker。

與Worker通信(Worker Communication)
  • 頁面代碼通過事件接口與Woker進行通信,通過postMessage()方法給Worker傳遞數據,通過onmessage事件接收信息。
var worker = new Worker("code.js");
worker.onmessage = function(event){
       console.info(event.data);
};
worker.postMessage("hello");

在Woker代碼中,同樣也是通過onmessage事件接收信息,通過postMessage()方法發送信息。

self.onmessage = function(event){
       console.info(event.data);
};
self.postMessage("hello too!");
  • 上面提到的消息系統是網頁和Worker通信的唯一途徑,并且只有特定類型的數據能通過postMessage()傳遞:原始值(字符串、數字、boolean、null和undefined)和Object和Array的實例。數據傳入傳出Worker時,會經歷序列化和反序列化。
加載外部文件(Loading External Files)
  • Worker通過importScript()方法加載外部一個或多個JavaScript文件。
  • inportScript()的調用過程是阻塞式的,直到素有文件加載并執行完成后,Worker中的腳本才會繼續運行。但由于Worker在UI線程之外運行,所以不用擔心這種阻塞會影響UI響應。
//code1/2.js文件執行后,引入的變量就可以在Worker線程中使用了
importScript('code1.js','code2.js');
self.onmessage = function(event){
       console.info(event.data);
};
實際應用(Practical Uses)
  • Web Worker適合處理純數據的、與瀏覽器UI無關的長時間運行(超過100毫秒)的腳本,比如:
    • 編碼/解碼大字符串;
    • 圖片或視頻處理時的復雜數學運算;
    • 大數據排序
  • 案例:現在需要解析一個數據量足夠大的JSON字符串,耗時至少需要500毫秒。為了避免干擾用戶體驗,Worker稱為最理想的解決方案。
<!DOCTYPE html>
<html>
       <head>
              <meta charset="{CHARSET}">
              <title>Test4Web Worker</title>
       </head>
       <body>
              <script type="text/javascript">
                     var worker = new Worker("jsonParse.js");
                     //注意:需要先定義onmessage事件,在postMessage()推消息
                     worker.onmessage = function(event){
                            console.info(event.data);
                     };
                     worker.postMessage("{'name':'William','age':26,'sex':'man'}");
              </script>
       </body>
</html>

外部jsonParse.js文件:

self.onmessage = function(event){
       var jsonText = event.data;
       console.info(jsonText);
       self.postMessage("parse done!");
};

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容

  • 最近在閱讀這本Nicholas C.Zakas(javascript高級程序設計作者)寫的最佳實踐、性能優化類的書...
    undefinedR閱讀 2,164評論 0 30
  • 用一周的時間看完了《高性能Javascript》,涉及到了javascript很多個方面的性能問題,但由于書比較薄...
    陳堅生閱讀 613評論 0 1
  • 蔥油,紅油,辣椒油是涼菜最常用的三個油,并稱涼菜調味三劍客。蔥油的熬制方法比較簡單,關鍵是火候。 大蔥、小蔥、洋蔥...
    廖相暉閱讀 727評論 0 0
  • Working Holiday Visa,簡稱WHV,即打工度假簽證。 持有某國的打工度假簽證WHV,可以在規定時...
    阿嗖閱讀 2,434評論 1 6
  • 最近有個舅舅買了房,自己東拼西湊付了首付,然后還要貸款。最近打電話給我媽,也就是他的大表姐要借兩萬塊錢付中介費。我...
    語桐醬閱讀 324評論 0 0