摘要:上代碼代碼可以看出,不僅函數比指定的回調函數先執行,而且函數也比先執行。這是因為后一個事件進入的時候,事件環可能處于不同的階段導致結果的不確定。這是因為因為執行完后,程序設定了和,因此階段不會被阻塞進而進入階段先執行,后進入階段執行。
JavaScript(簡稱JS)是前端的首要研究語言,要想真正理解JavaScript就繞不開他的運行機制--Event Loop(事件環)
JS是一門單線程的語言,異步操作是實際應用中的重要的一部分,關于異步操作參考我的另一篇文章js異步發展歷史與Promise原理分析 這里不再贅述。
堆、棧、隊列 堆(heap)堆(heap)是指程序運行時申請的動態內存,在JS運行時用來存放對象。
棧(stack)棧(stack)遵循的原則是“先進后出”,JS種的基本數據類型與指向對象的地址存放在棧內存中,此外還有一塊棧內存用來執行JS主線程--執行棧(execution context stack),此文章中的棧只考慮執行棧。
隊列(queue)隊列(queue)遵循的原則是“先進先出”,JS中除了主線程之外還存在一個“任務隊列”(其實有兩個,后面再詳細說明)。
Event LoopJS的單線程也就是說所有的任務都需要按照一定的規則順序排隊執行,這個規則就是我們要說明的Event Loop事件環。Event Loop在不同的運行環境下有著不同的方式。
瀏覽器環境下的Event Loop先上圖(轉自Philip Roberts的演講《Help, I"m stuck in an event-loop》)
當主線程運行的時候,JS會產生堆和棧(執行棧)
主線程中調用的webaip所產生的異步操作(dom事件、ajax回調、定時器等)只要產生結果,就把這個回調塞進“任務隊列”中等待執行。
當主線程中的同步任務執行完畢,系統就會依次讀取“任務隊列”中的任務,將任務放進執行棧中執行。
執行任務時可能還會產生新的異步操作,會產生新的循環,整個過程是循環不斷的。
從事件環中不難看出當我們調用setTimeout并設定一個確定的時間,而這個任務的實際執行時間可能會由于主線程中的任務沒有執行完而大于我們設定的時間,導致定時器不準確,也是連續調用setTimeout與調用setInterval會產生不同效果的原因(此處就不再展開,有時間我會多帶帶寫一篇文章)。
接下來上代碼:
console.log(1); console.log(2); setTimeout(function(){ console.log(3) setTimeout(function(){ console.log(6); }) },0) setTimeout(function(){ console.log(4); setTimeout(function(){ console.log(7); }) },0) console.log(5)
代碼中的setTimeout的時間給得0,相當于4ms,也有可能大于4ms(不重要)。我們要注意的是代碼輸出的順序。我們把任務以其輸出的數字命名。
先執行的一定是同步代碼,先輸出1,2,5,而3任務,4任務這時會依次進入“任務隊列中”。同步代碼執行完畢,隊列中的3會進入執行棧執行,4到了隊列的最前端,3執行完后,內部的setTimeout將6的任務放入隊列尾部。開始執行4任務……
最終我們得到的輸出為1,2,5,3,4,6,7。
宏任務與微任務任務隊列中的所有任務都是會乖乖排隊的嗎?答案是否定的,任務也是有區別的,總是有任務會有一些特權(比如插隊),就是任務中的vip--微任務(micro-task),那些沒有特權的--宏任務(macro-task)。
我們看一段代碼:
console.log(1); setTimeout(function(){ console.log(2); Promise.resolve(1).then(function(){ console.log("promise") }) }) setTimeout(function(){ console.log(3); })
按照“隊列理論”,結果應該為1,2,3,promise。可是實際結果事與愿違輸出的是1,2,promise,3。
明明是3先進入的隊列 ,為什么promise會排在前面輸出?這是因為promise有特權是微任務,當主線程任務執行完畢微任務會排在宏任務前面先去執行,不管是不是后來的。
換句話說,就是任務隊列實際上有兩個,一個是宏任務隊列,一個是微任務隊列,當主線程執行完畢,如果微任務隊列中有微任務,則會先進入執行棧,當微任務隊列沒有任務時,才會執行宏任務的隊列。
微任務包括: 原生Promise(有些實現的promise將then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver, MessageChannel;
宏任務包括:setTimeout, setInterval, setImmediate, I/O;
Node環境下的Event Loop┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
node中的時間循環與瀏覽器的不太一樣,如圖:
timers 階段: 這個階段執行setTimeout(callback) and setInterval(callback)預定的callback;
I/O callbacks 階段: 執行除了close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks之外的callbacks;
idle, prepare 階段: 僅node內部使用;
poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這里;
check 階段: 執行setImmediate() 設定的callbacks;
close callbacks 階段: 比如socket.on(‘close’, callback)的callback會在這個階段執行。
每一個階段都有一個裝有callbacks的fifo queue(隊列),當event loop運行到一個指定階段時,
node將執行該階段的fifo queue(隊列),當隊列callback執行完或者執行callbacks數量超過該階段的上限時,
event loop會轉入下一下階段。
process.nextTick方法不在上面的事件環中,我們可以把它理解為微任務,它的執行時機是當前"執行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")之前----觸發回調函數。也就是說,它指定的任務總是發生在所有異步任務之前。setImmediate方法則是在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務總是在下一次Event Loop時執行。上代碼:
process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log("TIMEOUT FIRED"); }, 0) // 1 // 2 // TIMEOUT FIRED
代碼可以看出,不僅函數A比setTimeout指定的回調函數timeout先執行,而且函數B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否嵌套),將全部在當前"執行棧"執行。
setTimeout 和 setImmediate二者非常相似,但是二者區別取決于他們什么時候被調用.
setImmediate 設計在poll階段完成時執行,即check階段;
setTimeout 設計在poll階段為空閑時,且設定時間到達后執行;但其在timer階段執行
其二者的調用順序取決于當前event loop的上下文,如果他們在異步i/o callback之外調用,其執行先后順序是不確定的。
setTimeout(function timeout () { console.log("timeout"); },0); setImmediate(function immediate () { console.log("immediate"); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
這是因為后一個事件進入的時候,事件環可能處于不同的階段導致結果的不確定。當我們給了事件環確定的上下文,事件的先后就能確定了。
var fs = require("fs") fs.readFile(__filename, () => { setTimeout(() => { console.log("timeout") }, 0) setImmediate(() => { console.log("immediate") }) })
$ node timeout_vs_immediate.js immediate timeout
這是因為因為fs.readFile callback執行完后,程序設定了timer 和 setImmediate,因此poll階段不會被阻塞進而進入check階段先執行setImmediate,后進入timer階段執行setTimeout。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93496.html
摘要:主線程在任務隊列中讀取事件,這個過程是循環不斷地,所以這種運行機制叫做事件循環是在執行棧同步代碼結束之后,下一次任務隊列執行之前。 單線程 javascript為什么是單線程語言,原因在于如果是多線程,當一個線程對DOM節點做添加內容操作的時候,另一個線程要刪除這個DOM節點,這個時候,瀏覽器應該怎么選擇,這就造成了混亂,為了解決這類問題,在一開始的時候,javascript就采用單線...
摘要:中線程運行機制詳解對于我們都知道,他是個單線程語言,但是準確來說它是擁有一個執行程序主線程,和消息隊列輔線程,以及各個真正處理異步操作的工作線程。 JavaScript中線程運行機制詳解 對于JavaScript我們都知道,他是個單線程語言,但是準確來說它是擁有一個執行程序主線程,和消息隊列輔線程(Event Loop),以及各個真正處理異步操作的工作線程。當主線程執行JS程序的時候,...
摘要:機制詳解與中實踐應用歸納于筆者的現代開發語法基礎與實踐技巧系列文章。事件循環機制詳解與實踐應用是典型的單線程單并發語言,即表示在同一時間片內其只能執行單個任務或者部分代碼片。 JavaScript Event Loop 機制詳解與 Vue.js 中實踐應用歸納于筆者的現代 JavaScript 開發:語法基礎與實踐技巧系列文章。本文依次介紹了函數調用棧、MacroTask 與 Micr...
摘要:曾經的理解首先,是單線程語言,也就意味著同一個時間只能做一件事,那么為什么不是多線程呢這樣還能提高效率啊假定同時有兩個線程,一個線程在某個節點上編輯了內容,而另一個線程刪除了這個節點,這時瀏覽器就很懵逼了,到底以執行哪個操作呢所以,設計者把 Event Loop曾經的理解 首先,JS是單線程語言,也就意味著同一個時間只能做一件事,那么 為什么JavaScript不是多線程呢?這樣還能提...
摘要:主線程要明確的一點是,主線程跟執行棧是不同概念,主線程規定現在執行執行棧中的哪個事件。主線程循環即主線程會不停的從執行棧中讀取事件,會執行完所有棧中的同步代碼。以上參考資料詳解中的事件循環機制中的事件循環運行機制詳解再談 showImg(https://segmentfault.com/img/remote/1460000015317437?w=1920&h=1080); 前言 大家都...
閱讀 3329·2021-11-22 12:04
閱讀 2713·2019-08-29 13:49
閱讀 485·2019-08-26 13:45
閱讀 2246·2019-08-26 11:56
閱讀 1002·2019-08-26 11:43
閱讀 596·2019-08-26 10:45
閱讀 1271·2019-08-23 16:48
閱讀 2161·2019-08-23 16:07