已知,JavaScript 是單線程的,天生異步,適合 IO 密集型,不適合 CPU 密集型,可是,爲何是異步的喃,異步由何而來的喃,咱們將在這裏逐漸討論實現。html
它主要包括如下進程:前端
瀏覽器的渲染進程是多線程的,頁面的渲染,JavaScript 的執行,事件的循環,都在這個進程內進行:web
setInterval
與 setTimeout
所在線程,注意,W3C 在 HTML 標準中規定,規定要求 setTimeout
中低於 4ms 的時間間隔算爲 4ms 。XMLHttpRequest
鏈接後經過瀏覽器新開一個線程請求,將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由 JavaScript 引擎執行。注意,GUI 渲染線程與 JavaScript 引擎線程是互斥的,當 JavaScript 引擎執行時 GUI 線程會被掛起(至關於被凍結了),GUI 更新會被保存在一個隊列中等到 JavaScript 引擎空閒時當即被執行。因此若是 JavaScript 執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。面試
所謂單線程,是指在 JavaScript 引擎中負責解釋和執行 JavaScript 代碼的線程惟一,同一時間上只能執行一件任務。ajax
問題:首先爲何要引入單線程喃?編程
咱們知道:json
若是 JavaScript 引擎線程不是單線程的,那麼能夠同時執行多段 JavaScript,若是這多段 JavaScript 都修改 DOM,那麼就會出現 DOM 衝突。瀏覽器
你可能會說,web worker 就支持多線程,可是 web worker 不能訪問 window 對象,document 對象等。網絡
緣由:避免 DOM 渲染的衝突多線程
固然,咱們能夠爲瀏覽器引入鎖 的機制來解決這些衝突,但其大大提升了複雜性,因此 JavaScript從誕生開始就選擇了單線程執行。
引入單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。這同時又致使了一個問題:若是前一個任務耗時很長,後一個任務就不得不一直等着。
// 實例1 let i, sum = 0 for(i = 0; i < 1000000000; i ++) { sum += i } console.log(sum) 複製代碼
在實例1中,sum
並不能馬上打印出來,必須在 for 循環執行完成以後才能執行 console.log(sum)
。
// 實例2 console.log(1) alert('hello') console.log(2) 複製代碼
在實例2中,瀏覽器先打印 1
,而後彈出彈框,點擊肯定後才執行 console.log(2)
。
總結:
爲了解決這個問題,JavaScript 語言將任務的執行模式分爲兩種:同步和異步
func(args...) 複製代碼
若是在函數 func
返回的時候,調用者就可以獲得預期結果(即拿到了預期的返回值或者看到了預期的效果),那麼這個函數就是同步的。
let a = 1 Math.floor(a) console.log(a) // 1 複製代碼
若是在函數 func
返回的時候,調用者還不可以獲得預期結果,而是須要在未來經過必定的手段獲得,那麼這個函數就是異步的。
fs.readFile('foo.txt', 'utf8', function(err, data) { console.log(data); }); 複製代碼
總結:
JavaScript 採用異步編程緣由有兩點,
fs.readFile('data.json', 'utf8', function(err, data) { console.log(data) }) 複製代碼
在執行這段代碼時,fs.readFile
函數返回時,並不會馬上打印 data
,只有 data.json
讀取完成時纔打印。也就是異步函數 fs.readFile
執行很快,但後面還有工做線程執行異步任務、通知主線程、主線程回調等操做,這個過程就叫作異步過程。
主線程發起一個異步操做,相應的工做線程接受請求並告知主線程已收到(異步函數返回);主線程繼續執行後面的任務,同時工做線程執行異步任務;工做線程完成任務後,通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)。
工做線程在異步操做完成後通知主線程,那麼這個通知機制又是如何顯現喃?答案就是就是消息隊列與事件循環。
工做線程將消息放在消息隊列,主線程經過事件循環過程去取消息。
主線程不斷的從消息隊列中取消息,執行消息,這個過程稱爲事件循環,這種機制叫事件循環機制,取一次消息並執行的過程叫一次循環。
大體實現過程以下:
while(true) { var message = queue.get() execute(message) } 複製代碼
例如:
$.ajax({ url: 'xxxx', success: function(result) { console.log(1) } }) setTimeout(function() { console.log(2) }, 100) setTimeout(function() { console.log(3) }) console.log(4) // output:4321 或 4312 複製代碼
其中,主線程:
// 主線程 console.log(4) 複製代碼
異步隊列:
// 異步隊列 function () { console.log(3) } function () { // 100ms後 console.log(2) } function() { // ajax加載完成以後 console.log(1) } 複製代碼
事件循環是JavaScript實現異步的具體解決方案,其中同步代碼,直接執行;異步函數先放在異步隊列中,待同步函數執行完畢後,輪詢執行 異步隊列 的回調函數。
其中,消息就是註冊異步任務時添加的回調函數。
$.ajax('XXX', function(res) { console.log(res) }) ... 複製代碼
主線程在發起 AJAX 請求後,會繼續執行其餘代碼,AJAX 線程負責請求 XXX
,拿到請求後,會封裝成 JavaScript 對象,而後構造一條消息:
// 消息隊列裏的消息 var message = function () { callback(response) } 複製代碼
其中 callback
是 AJAX 網絡請求成功響應時的回調函數。
主線程在執行完當前循環中的全部代碼後,就會到消息隊列取出這條消息(也就是 message
函數),並執行它。到此爲止,就完成了工做線程對主線程的 通知
,回調函數也就獲得了執行。若是一開始主線程就沒有提供回調函數,AJAX 線程在收到 HTTP 響應後,也就不必通知主線程,從而也不必往消息隊列放消息。
異步過程當中的回調函數,必定不在當前這一輪事件循環中執行。
消息隊列中的每條消息實際上都對應着一個事件。
其中一個重要的異步過程就是: DOM事件
var button = document.getElementById('button') button.addEventListener('click', function(e) { console.log('事件') }) 複製代碼
從異步的角度看,addEventListener
函數就是異步過程的發起函數,事件監聽器函數就是異步過程的回調函數。事件觸發時,表示異步任務完成,會將事件監聽器函數封裝成一條消息放在消息隊列中,等待主線程執行。
事件的概念實際上並非必須的,事件機制實際上就是異步過程的通知機制。
另外,全部的異步過程也均可以用事件來描述。例如:
setTimeout(func, 1000) // 能夠當作: timer.addEventListener('timeout', 1000, func) 複製代碼
生產者和消費者問題是線程模型中的經典問題:生產者和消費者在同一時間段內共用同一個存儲空間,生產者往存儲空間中添加數據,消費者從存儲空間中取走數據,當存儲空間爲空時,消費者阻塞,當存儲空間滿時,生產者阻塞。
從生產者與消費者的角度看,異步過程是這樣的:
工做線程是生產者,主線程是消費者(只有一個消費者)。工做線程執行異步任務,執行完成後把對應的回調函數封裝成一條消息放到消息隊列中;主線程不斷地從消息隊列中取消息並執行,當消息隊列空時主線程阻塞,直到消息隊列再次非空。
那麼異步的實現方式有哪些喃?
ES7:Async/Await
小編整了些JS面試題資料,小編放個小尾巴給你們點擊領取哦:JS面試題資料