本文主要記錄運行環境和特別函數以及web worker:html
JavaScript只在主線程上運行。也就是說JavaScript同時只能執行一個同步任務,其餘同步任務都必須在後面排隊等待。web
爲了利用多核CPU的計算能力,HTML5 提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此這個新標準並無改變JavaScript單線程的本質。ajax
JavaScript運行時除了一個正在運行的主線程,引擎根據異步任務的類型會存在多個任務隊列(裏面是各類須要當前程序處理的異步任務)。JavaScript會不停地檢查,只要同步任務執行完了,引擎就會去檢查那些掛起來的異步任務,是否是能夠進入主線程。這種循環檢查的機制叫事件循環 (Event Loop)。數組
首先,主線程會去執行全部的同步任務,一直等到同步任務所有執行完。promise
其次,去看任務隊列裏面的異步任務。若是知足條件,那麼異步任務就從新進入主線程開始執行,這時它就變成同步任務了。等到執行完後下一個異步任務再進入主線程開始執行。瀏覽器
最後,一旦任務隊列清空,程序就結束執行。服務器
回調函數是異步操做最基本的方法網絡
function func1(callback) { // todo callback(); } function func2() { // todo } func1(func2);
發佈/訂閱是異步操做很常見的方法多線程
// func1進行以下改寫 function f1() { setTimeout(function () { // todo jQuery.publish('done'); }, 1000); } // func2向信號中心jQuery訂閱done信號。 jQuery.subscribe('done', func2);
若是有多個異步操做,就存在一個流程控制的問題:如何肯定異步操做執行的順序,以及如何保證遵照這種順序。併發
串行執行
能夠編寫一個流程控制函數,讓它來控制異步任務。一個任務完成之後,再執行另外一個,最後執行final函數。這就叫串行執行。缺點是執行時間很長。
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('參數爲 ' + arg +' , 1秒後返回結果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } // 串行函數,它會依次執行異步任務 function exec(item) { if(item) { async(item, function(result) { results.push(result); return series(items.shift()); }); } else { return final(results[results.length - 1]); } } exec(items.shift());
並行執行
能夠編寫一個流程控制函數,讓它來控制異步任務。全部異步任務同時執行,等到所有完成之後,最後執行final函數。這就叫並行執行。缺點是若是並行的任務較多,很容易耗盡系統資源,拖慢運行速度。
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('參數爲 ' + arg +' , 1秒後返回結果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } // 並行函數,會同時發起六個異步任務 function exec(arrs){ arrs.forEach(function(item) { async(item, function(result){ results.push(result); if(results.length === items.length) { final(results[results.length - 1]); } }) }); } exec(items.shift());
混合執行
並行與串行的結合就是設置一個門檻,每次最多隻能並行執行n
個異步任務,這樣就避免過度佔用系統資源。
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; var running = 0; var limit = 2;// 經過調節limit變量,達到效率和資源的最佳平衡 function async(arg, callback) { console.log('參數爲 ' + arg +' , 1秒後返回結果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } function exec() { while(running < limit && items.length > 0) { var item = items.shift(); async(item, function(result) { results.push(result); running--; if(items.length > 0) { exec(); } else if(running == 0) { final(results); } }); running++; } } exec();
以上就是有多個異步操做下流程控制的設計方案。
Promise對象是JavaScript的異步操做解決方案(包含設計方法和流程控制),爲異步操做提供統一接口。它可讓異步操做寫起來,就像在寫同步操做的流程,而沒必要一層層地嵌套回調函數。
Promise本來只是社區提出的一個構想,目前JavaScript原生支持Promise對象。
Promise
接受一個回調函數做參數,該函數的兩個參數resolve
和reject
是函數,由JavaScript引擎提供。ES6出現了generator以及async/await語法,也是基於Promise實現的。
Promise
實例的回調函數屬於微型異步任務,會在同步任務以後且常規異步任務以前執行。正常任務追加到下一輪事件循環,微任務追加到本輪事件循環。這意味着微任務的執行時間必定早於正常任務。
Promise
原型提供了以下的方法:
all
方法會併發保證運行多個Promise實例,所有運行完成後返回一個新的Promise實例。成功時返回的是個數組對象,失敗時返回最早被reject失敗狀態的值。race
方法會併發賽跑運行多個Promise實例,最早運行完成後返回一個新的Promise實例。成功和失敗返回的均是最早執行完的對應的狀態值。then
方法用於指定當前Promise
實例狀態發生改變時的回調函數,而後返回一個新的Promise實例。catch
方法是then(null, failureCallback)
的縮略形式。finally
方法指定無論Promise
實例最後狀態如何都會執行的操做。setTimeout
函數用來指定某個函數在固定時間以後執行,而後結束。返回一個整數,表示定時器的編號。使用clearTimeout
進行清除。
setInterval
函數用來指定某個函數在固定時間以後執行,循環有效。返回一個整數,表示定時器的編號。使用clearInterval
進行清除。
setTimeout
和setInterval
的機制是將指定的代碼移出本輪事件循環,等到下一輪事件循環,再檢查是否到了指定時間。若是到了就執行對應的代碼,若是不到就繼續等待。這意味着setTimeout
和setInterval
指定的回調函數必須等到本輪事件循環的全部同步任務都執行完後纔會開始執行。因爲前面的任務到底須要多少時間執行完是不肯定的,因此沒有辦法保證二者的定時時間是絕對的。因JS是單線程,若是主線程存在時間超過定時時間的任務,那只有在當前主線程全部同步任務執行完成後,此時纔會將任務函數提到主線程上,以用來運行當前的定時任務函數。
二者的語法用法徹底一致:
參數:定時函數接受多個參數,第一個參數
func
是將要推遲執行的函數名,第二個參數delay
是推遲執行的毫秒數(若是省略,則默認爲0),後續的參數是func的參數數值。this指向:定時函數使得回掉函數內部的
this
關鍵字指向全局環境,而不是定義時所在的那個對象。
有時不但願回調函數被頻繁調用。如用戶填入網頁輸入框的內容,但願經過Ajax方法傳回服務器,jQuery 的寫法以下:
$('textarea').on('keydown', ajaxAction);
這樣寫有一個很大的缺點,就是若是用戶連續擊鍵就會連續觸發keydown
事件,形成大量的Ajax通訊。正確的作法應該是設置一個門檻值,表示兩次Ajax通訊的最小間隔時間。若是在間隔時間內,發生新的keydown
事件則不觸發 Ajax 通訊,而且從新開始計時。若是過了指定時間沒有發生新的keydown
事件,再將數據發送出去。
$('textarea').on('keydown', debounce(ajaxAction, 2500)); function debounce(fn, delay){ var timer = null; // 聲明計時器 return function() { var context = this; var args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); }; }
Web Worker的做用,就是爲JS創造多線程環境。在主線程運行的同時,Worker線程在後臺運行,Worker線程一旦新建成功,就會始終運行,不會被主線程上的活動(好比用戶點擊按鈕、提交表單)打斷。等到 Worker線程完成計算任務,再把結果返回給主線程。
Web Worker有如下幾個使用注意點:
document
、window
、parent
這些對象以及各類DOM。可是Worker線程可使用navigator
對象和location
對象。Worker的全局對象WorkerGlobalScope
,不一樣於網頁的全局對象Window
,它不少接口拿不到。file://
),它所加載的腳本,必須來自網絡,而且分配給Worker線程運行的腳本文件,必須與主線程的腳本文件同源。主線程採用new
命令,調用Worker()
構造函數,新建一個 Worker 線程。
var myWorker = new Worker('worker.js', { name : 'myWorker' });
Worker()
構造函數的第一個參數(必須的)是一個腳本文件,該文件就是Worker線程所要執行的任務。因爲Worker不能讀取本地文件,因此這個腳本必須來自網絡。若是下載沒有成功(好比404錯誤),Worker就會失敗。
Worker()
構造函數的第二個參數(可選的)是配置對象。它的做用是指定Worker線程的名稱。
主線程調用worker.postMessage()
方法,向Worker發送消息。
worker.postMessage('Hello World'); worker.postMessage({method: 'echo', args: ['Work']});
postMessage參數能夠是各類數據類型,包括二進制數據。
主線程經過worker.onmessage
方法,接收Worker的消息。經過worker.onerror
方法,接收Worker的異常。
function doAction(data) { // todo } worker.onmessage = function (event) { doAction(event.data); } worker.onerror(function (event) { console.log([ 'ERROR: Line ', event.lineno, ' in ', event.filename, ': ', event.message ].join('')); });
事件對象的data
屬性能夠獲取Worker發來的數據。
主線程在Worker完成任務之後就能夠把它關掉。
worker.terminate();
self.name
屬性爲Worker的名字。它由構造函數指定。
self.importScripts()
用於Worker內部加載JS腳本。
self.addEventListener()
指定監聽函數。監聽函數的參數是個事件對象,它的data
屬性爲發來的數據。
self.postMessage()
方法用於子線程向主線程發送消息。
self.close()
方法用於在Worker內部關閉自身。var self = this; self.importScripts('script1.js', 'script2.js'); // 用於監聽消息 self.addEventListener('message', function (e) { var data = e.data; switch (data.cmd) { case 'start': self.postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': self.postMessage('WORKER STOPPED: ' + data.msg); self.close(); // Terminates the worker. break; default: self.postMessage('Unknown command: ' + data.msg); }; }, false); // 用於監聽錯誤 worker.addEventListener('error', function (event) { // todo });
正常狀況下瀏覽器內部的運行機制是,先將通訊內容串行化,而後把串行化後的字符串發給 Worker,後者再將它還原。可是拷貝方式發送二進制數據,會形成性能問題。如主線程向Worker發送一個 500MB 文件,默認狀況下瀏覽器會生成一個原文件的拷貝。這種方法不會直接轉移數據的控制權。
爲了解決這個問題JS使用一種轉移數據的方法Transferable Objects。JavaScript容許主線程把二進制數據直接轉移給子線程,可是一旦轉移後主線程就沒法再使用這些二進制數據,這是爲了防止出現多個線程同時修改數據的麻煩局面(本質就是值拷貝而不是地址拷貝)。這使得主線程能夠快速把數據交給Worker,對於影像處理、聲音處理、3D 運算等就很是方便,不會產生性能負擔。這種方法會直接轉移數據的控制權。
// 正常發送消息 worker.postMessage('Hello World'); // 特殊發送消息原型爲worker.postMessage(arrayBuffer, [arrayBuffer]); var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]);
一般狀況下,Worker載入的是一個單獨的JavaScript腳本文件,可是也能夠載入與主線程在同一個網頁的代碼。
<!DOCTYPE html> <body> <script id="worker" type="app/worker"> addEventListener('message', function () { postMessage('some message'); }, false); </script> </body> </html>
上面是一段嵌入網頁的腳本,注意必須指定script標籤的type
屬性是一個瀏覽器不認識的值(上例是app/worker
)。而後讀取這一段嵌入頁面的腳本,用Worker來處理。
var blob = new Blob([document.querySelector('#worker').textContent]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); worker.onmessage = function (e) { // e.data === 'some message' };
上面代碼中先將嵌入網頁的腳本代碼,轉成一個二進制對象,而後爲這個二進制對象生成URL,再讓Worker加載這個 URL。這樣就作到了主線程和Worker的代碼都在同一個網頁上面。