摘要:概述本篇主要介紹的運行機制單線程事件循環結論先在中利用運行至完成和非阻塞完成單線程下異步任務的處理就是先處理主模塊主線程上的同步任務再處理異步任務異步任務使用事件循環機制完成調度涉及的內容有單線程事件循環同步執行異步執行定時器的事件循環開始
1.概述
本篇主要介紹JavaScript的運行機制:單線程事件循環(Event Loop).
結論先: 在JavaScript中, 利用運行至完成和非阻塞IO 完成單線程下異步任務的處理. 就是先處理主模塊(主線程)上的同步任務, 再處理異步任務. 異步任務使用事件循環機制完成調度.
涉及的內容有: 單線程, 事件循環, 同步執行, 異步執行, 定時器, nodeJS的事件循環
開始之前, 先看下面的代碼, 給出結果:
// 當前時間 console.log("A: " + new Date()); // 1秒(1000毫秒)后執行的定時器 // 異步執行的代碼 setTimeout(function() { console.log("B: " + new Date()); }, 1000); // 循環3秒(3000毫秒) var end = Date.now() + 3000; while(Date.now() < end) { } // 當前時間 console.log("C: " + new Date());
在瀏覽器中的結果為(chrome-50.0.2661.102):
A: Thu May 25 2017 13:48:26 GMT+0800 (中國標準時間) C: Thu May 25 2017 13:48:29 GMT+0800 (中國標準時間) B: Thu May 25 2017 13:48:29 GMT+0800 (中國標準時間)
在NodeJS(v7.7.2 win-x64)中的結果為:
>node scriptsasync.js A: Thu May 25 2017 13:50:55 GMT+0800 (中國標準時間) C: Thu May 25 2017 13:50:58 GMT+0800 (中國標準時間) B: Thu May 25 2017 13:50:58 GMT+0800 (中國標準時間)
tip: 瀏覽器下和NodeJS結果一致.
分析上面的代碼與結果, 注意的要點:
雖然設置的定時器為1秒后執行, 但實際的執行時間在3秒以后, 看結果中B:的輸出, 在A:的3秒后.
B:的輸出在C:的輸出之后. 可見, 雖然在while循環后, 時間已經到了定時器代碼需要執行的時間, 但并沒有立即執行, 而是等到了console.log("C: ")執行完, 再執行的定時器的代碼.
本篇就是說明為什么會出現以上的現象. 下面請一步步的看.
2.單線程單線程, 指的是JavaScript在一個時間僅處理一個任務. 就是JavaScript在執行時, 存在一個執行隊列, 依次執行隊列中的任務, 不能同時執行多個任務.
單線程的優勢, 也是JavaScript選擇單線程的原因是:
1, 降低處理復雜性, 簡化開發. 例如不用考慮死鎖, 競爭機制等.
2, 作為用于處理與用戶互動的腳本語言, 可以更加容易地處理狀態同步的問題(想想考慮用戶操作的不確定性).
3, JavaScript核心維護人員自身的設計與理解.
4, 越簡單越容易推廣, 快速上手.
除了優勢, 單線程有明顯的劣勢, 就是并發處理能力, 因為單線程處理下所有的任務就要排隊處理. 但是如果排在前面的任務處理很耗時, 那就導致后面的任務一直處于等待狀態. 如果前面的任務出處于滿載運行狀態還可以, 但是如果前面的任務處于IO等待狀態呢? 就會導致CPU處理資源的浪費.
思考, 前面的是AJAX任務, 后邊是其他任務. AJAX任務需要等待網絡請求響應結束, 才能處理, 此時前面的AJAX任務就處于IO等待狀態. 從而導致后面的任務也執行不了, 造成了單線程下的資源浪費. (CPU沒有辦法高速運轉, 處于空閑狀態).
在此情況下, 完全可以掛起前面的AJAX任務(掛起等待AJAX的響應結果), 先執行后面的任務. 等后面的任務處理完畢后, 再看前面的AJAX任務是否得到了IO結果, 如果有結果了, 在翻回來處理即可. 這種處理方式, 就是異步方式.
3.同步任務和異步任務單線程的JavaScript為了更好利用CPU的性能, 將執行的任務設計為: 同步任務和異步任務, 兩類.
同步任務(synchronous task), 就是需要一個個順序執行的任務, 不能跳過, 執行完前一個才能執行后一個. 我們稱之為在主模塊(主線程)執行的任務.
異步任務(asynchronous task), 指的是被掛起執行的任務, 在系統內部處于等待IO處理結果狀態, 一旦處理完畢, 記錄下來, 等待后續處理. 需要事件循環處理的任務. 上面示例中的AJAX任務就是異步任務.
你應該會想, JavaScript不是單線程么, 怎么還能異步處理呢?
是這樣的, JavaScript的單線程, 指的是在JavaScript語言(語法)層面是單線程的. 而內部的執行, 還是可以利用到處理器多線程和操作系統的任務調度的, 在后臺處理我們的異步任務. 當操作在后臺被處理完成后(例如ajax接收完畢了服務器的響應), 操作系統將結果告知給JavaScript, 并最終被JavaScript執行.
JavaScript是如何調度這些同步任務和異步任務的呢?
就涉及到了, 本文的重點: 任務隊列 和 事件循環, 執行棧.
如圖(邏輯概述圖)所示:
執行如下:
step1, 同步任務直接放入到主模塊(主線程)任務隊列執行. 異步任務掛起后臺執行, 等待IO事件完成或行為事件被觸發.
step2, 系統后臺執行異步任務, 如果某個異步任務事件發生(或者是行為事件被觸發), 則將該任務push到任務隊列中, 每個任務會對應一個回調函數進行處理. 這個步驟在后臺一直執行, 因為就不斷有事件被觸發, IO不斷完成, 任務被不斷的加入到任務隊列中.
step3, 執行任務隊列中的任務. 任務的具體執行是在執行棧中完成的. 當運行棧中一個任務的基本運行單元(稱之為Frame, 楨)全部執行完畢后, 去讀取任務隊列中的下一個任務, 繼續執行. 是一個循環的過程. 處理一個任務隊列中的任務, 稱之為一個tick.
注意, step3, 是一個循環的過程, 這就是事件循環. 循環執行任務隊列中已經發生的事件對應的任務.
再參考開始的代碼, 我們可以知道:
// A:當前時間 // 同步代碼, 直接進入任務隊列 console.log("A: " + new Date()); // B:1秒(1000毫秒)后執行的定時器 // 異步代碼, 等待到時事件發生, 才會進入任務隊列 setTimeout(function() { console.log("B: " + new Date()); }, 1000); // 循環3秒(3000毫秒), // 同步代碼, 直接進入任務隊列 var end = Date.now() + 3000; // 同步代碼, 直接進入任務隊列 while(Date.now() < end) { } // C:當前時間 // 同步代碼, 直接進入任務隊列 console.log("C: " + new Date());
也就意味著, 此時, log(A), while, log(C) 三個任務, 已經進入到了任務隊列中. 而setTimeout是異步任務(與AJAX一致)在等待事件發生(到時事件). 于此同時, JavaScript開始處理任務隊列. 隊列是先進先出, 需要依次處理. 所以, 即時當前已經到1s了, 事件發生, 也僅僅是將該任務push入任務隊列而已(并沒有立即執行回調函數). 當將setTimeout入隊列時, log(C)已經在隊列中了, 因此, setTimeout的log(B), 會在log(B)后執行. 這就是輸出了: A, C, B的原因. 如下圖(邏輯概述圖)所示:
由任務隊列可知, 輸出為: A, C, B順序.
JavaScript提供了可以操作定時器的函數, setTimeout()和setInterval. 在NodeJS中還有setImmediate().
setTimeout(), 定時執行setTimeout(callback, timer), 多久(毫秒計)后執行, 常規用法已經演示.
需要提醒大家的是, setTimetout()是延時觸發, 而不是即時觸發. 指的是, 在有機會處理計時器事件時, 優先處理最先到時的計時器程序. 而不是時間到立即處理. 因為是單線程, 需要先處理當前的任務, 例如主模塊中的任務(同步任務).
實操中還有一個setTimeout(callback, 0)的用法, 表示立即加入到任務隊列. 但是注意, 并不是在執行setTimeout的時候, 就加入隊列了, 而是當全部的同步任務入隊列后, 立即加入到任務隊列, 也就意味著同步任務之后第一個執行. 但據說這個值內部執行時有一個最小值, 4ms.
上面的代碼, 將時間改為0, 測試結果還是A, C, B. 不會因為先執行的setTimeout()而就將任務先執行.
// 異步代碼, 等待到時事件發生, 才會進入任務隊列 setTimeout(function() { console.log("B: " + new Date()); }, 0);
未發生的定時器, 可以使用clearTimeout()方法清除.
setInterval(), 循環執行.setInterval(callback, timer) 與setTimeout()相似, 不過是在callback執行完畢后, 再次設置了計時器. 不再贅述.
6.NodeJS中的事件循環NodeJS的事件循環模型比瀏覽器更為復雜些.
如下圖所示(引用自NodeJS官方文檔), 事件循環, 按照下圖的順序調用事件.
由于出現了不同的事件循環段, 例如 timer, check, 出現了額外的控制定時器方法.
邏輯含義上講, 與setTimeout(callback, 0)一致. 都是立即執行. 在NodeJS中setImmediate()存在的主要場景就是, 在異步IO調用中, 如果同時使用setImmediate()和settimeout(), 可以保證, setImmediate()先于所有的setTimeout()執行.
如下代碼: (引用自NodeJS官方文檔)
var fs = require("fs"); // 異步文件IO fs.readFile(__filename, () => { setTimeout(function timeout () { console.log("timeout"); },0); setImmediate(function immediate () { console.log("immediate"); }); });
以上代碼的執行結果, 一定是:
>node scriptsasync-node.js immediate timeout 這是因為, 根據NodeJS的事件循環處理順序, 處理完IO后, 需要處理check, 而setImmediate()就是check中的事件. 因此先處理.
但上面的代碼如果沒有在異步IO中調用, 在主模塊(主線程)中調用, 則順序不一定, 由操作系統調度決定!
process.nextTick(callback)tick, 就是一個事件循環周期. 在prcess.nextTick()中設置的異步callback會在當前事件循環周期結束, 下一個事件循環周期開始前執行.
像是一個插入的tick. 生成了一個新的周期. 說白了, 是一個插隊行為.
因此, 在時間上看, 一定先于settimeout(callback, 0)和setImmediate()執行. 通常用來處理在下一個事件周期(異步任務)前, 必須要處理好的任務. 常見的有, 處理錯誤, 回收資源, 和 重新執行存在錯誤的操作等.
測試一下執行時機:
setTimeout(function timeout () { console.log("timeout"); },0); setImmediate(function immediate () { console.log("immediate"); }); process.nextTick(function immediate () { console.log("nickTick"); });
結果為:
>node scriptsasync-node.js nextTick timeout immediate
可見, nextTick先發生.
注意, 在NodeJS中, nexttick并不是一個特殊的定時器.
注意, 由于nextTick()會插隊執行, 因此, NodeJS限制了nextTick()遞歸調用的深度. 防止IO處理饑餓.一直在處理nextTick(). 由于該原因, 遞歸時, NodeJS建議使用setImmediate()完成.
process.nextTick, 永遠先執行.
setImmediate和setTimeout, 那個先到時那個先執行. 如果同時, 則由系統調度負責.
在JavaScript中, 利用運行至完成和非阻塞IO 完成單線程下異步任務的處理. 就是先處理主模塊(主線程)上的同步任務, 再處理異步任務. 異步任務使用事件循環機制完成調度.
參考:
NodeJS文檔, https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ JavaScript 運行機制詳解:再談Event Loop, http://www.ruanyifeng.com/blog/2014/10/event-loop.html 樸靈 深入淺出Node.js http://www.infoq.com/cn/master-nodejs8.結語
以上就是本人對事件循環的理解. 一家之言, 歡迎討論拍磚!
更多內容, 可以關注, 微信公眾號, 小韓說理.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83187.html
摘要:主線程從任務隊列中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為事件循環。上面也提到,在到達指定時間時,定時器就會將相應回調函數插入任務隊列尾部。這就是定時器功能。關于定時器的重要補充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運行機制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
摘要:主線程從任務隊列中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為事件循環。上面也提到,在到達指定時間時,定時器就會將相應回調函數插入任務隊列尾部。這就是定時器功能。關于定時器的重要補充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運行機制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
摘要:主線程從任務隊列中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為事件循環。上面也提到,在到達指定時間時,定時器就會將相應回調函數插入任務隊列尾部。這就是定時器功能。關于定時器的重要補充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運行機制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...
js異步歷史 一個 JavaScript 引擎會常駐于內存中,它等待著我們把JavaScript 代碼或者函數傳遞給它執行 在 ES3 和更早的版本中,JavaScript 本身還沒有異步執行代碼的能力,引擎就把代碼直接順次執行了,異步任務都是宿主環境(瀏覽器)發起的(setTimeout、AJAX等)。 在 ES5 之后,JavaScript 引入了 Promise,這樣,不需要瀏覽器的安排,J...
閱讀 832·2023-04-26 00:13
閱讀 2823·2021-11-23 10:08
閱讀 2450·2021-09-01 10:41
閱讀 2118·2021-08-27 16:25
閱讀 4198·2021-07-30 15:14
閱讀 2365·2019-08-30 15:54
閱讀 864·2019-08-29 16:22
閱讀 2741·2019-08-26 12:13