摘要:只要指定過回調函數,這些事件發生時就會進入任務隊列,等待主線程讀取。三主線程從任務隊列中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為事件循環。
一、任務隊列 同步任務與異步任務的由來
單線程就意味著,所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等著。
如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等著結果出來,再往下執行。
JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處于等待中的任務,先運行排在后面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。
于是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。
同步任務與異步任務的定義同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;
異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。
異步執行的運行機制具體來說,異步執行的運行機制如下。(同步執行也是如此,因為它可以被視為沒有異步任務的異步執行。)
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重復上面的第三步。
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重復。
舉個例子:
console.log("1"); setTimeout(()=>{ console.log("2"); },0); console.log("3"); // 1 // 3 // 2
運行結果是:1、3、2
setTimeout里的函數并沒有立即執行,而是延遲一段時間,符合特定的條件才開始執行,這就是異步執行操作。
console.log("1") //是同步任務,放入主線程, setTimeout() //是異步任務,被放入事件列表Event table中,0秒后被推入任務隊列task queue里, console.log("3") //是同步任務,放入主線程 //當1、3任務先執行完后,主線程去task queue(事件隊列)里查看是否有可執行的函數,執行setTimeout里的函數。二、事件和回調函數
"任務隊列"是一個事件的隊列(也可以理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主線程讀取"任務隊列",就是讀取里面有哪些事件。
"任務隊列"中的事件,除了IO設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。
所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。
"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。但是,由于存在后文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。
三、Event Loop主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為Event Loop(事件循環)。
主線程運行的時候,產生堆(heap)和棧(stack),
heap(堆):是用戶主動請求而劃分出來的內存區域,比如你new Object(),就是將一個對象存入堆中,可以理解為heap存對象。
stack(棧):是由于函數運行而臨時占用的內存區域,函數都存放在棧里。
棧中的代碼調用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done)。
(當滿足觸發條件后才加入隊列,如ajax請求完畢)
而當棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。如此循環
【注意,總是要等待棧中的代碼執行完畢后才會去讀取事件隊列中的事件】
四、宏任務和微任務JS中分為兩種任務類型:宏任務macro task和微任務micro task,宏任務與微任務的定義
在ECMAScript中,micro task稱為jobs,macro task可稱為task
1)宏任務(macro task),可以理解為每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執行棧中執行)
每一個task會從頭到尾將這個任務執行完畢,不會執行其它
瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束后,在下一個 task 執行開始前,對頁面進行重新渲染
(task->渲染->task->...)
2)微任務(micro task),可以理解為在當前 task 執行結束后立即執行的任務
也就是說,在當前task任務后,下一個task之前,在渲染之前
所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染
也就是說,在某一個macro task執行完后,就會將在它執行期間產生的所有micro task都執行完畢(在渲染前)
1)宏任務(macro task):
主代碼塊,setTimeout,setInterval,I/O、UI交互事件、postMessage、MessageChannel、setImmediate(node.js 環境)等(可以看到,事件隊列中的每一個事件都是一個宏任務)
2)微任務(micro task):
Promise.then、MutaionObserver、MessageChannel、process.nextTick(node.js 環境)等
__補充:在node環境下,process.nextTick的優先級高于Promise,也就是可以簡單理解為:在宏任務結束后會先執行微任務隊列中的nextTickQueue部分,然后才會執行微任務中的Promise部分。
再根據線程來理解下:
宏任務(macro task)中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護
微任務(micro task)中的所有微任務都是添加到微任務隊列(Job Queues)中,等待當前宏任務執行完畢后執行,而這個隊列由JS引擎線程維護
所以,總結下運行機制:
執行一個宏任務(棧中沒有就從事件隊列中獲取)
執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染
渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
舉個例子:
setTimeout(()=>{ console.log("定時器開始執行"); }) new Promise(function(resolve){ console.log("準備執行for循環了"); for(var i=0;i<100;i++){ i==22&&resolve(); } }).then(()=>console.log("執行then函數")); console.log("代碼執行完畢"); //首先執行script下的宏任務,遇到setTimeout,將其放到宏任務的【隊列】里 //遇到 new Promise直接執行,打印"準備執行for循環" //遇到then方法,是微任務,將其放到微任務的【隊列里】 //打印 "代碼執行完畢" //本輪宏任務執行完畢,查看本輪的微任務,發現有一個then方法里的函數, 打印"執行then函數" //到此,本輪的event loop 全部完成。 //下一輪的循環里,先執行一個宏任務,發現宏任務的【隊列】里有一個 setTimeout里的函數,執行打印"定時器開始執行"
所以最后的執行順序就是:【準備執行for循環-->代碼執行完畢-->執行then函數-->定時器開始執行】
如果你覺得這篇文章對你有所幫助,那就順便點個贊吧,點點關注不迷路~
黑芝麻哇,白芝麻發,黑芝麻白芝麻哇發哈!
前端哇發哈
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103040.html
摘要:令人困惑的是,文檔中稱,指定的回調函數,總是排在前面。另外,由于指定的回調函數是在本次事件循環觸發,而指定的是在下次事件循環觸發,所以很顯然,前者總是比后者發生得早,而且執行效率也高因為不用檢查任務隊列。 一、定時器 除了放置異步任務的事件,任務隊列還可以放置定時事件,即指定某些代碼在多少時間之后執行。這叫做定時器(timer)功能,也就是定時執行的代碼。 定時器功能主要由setTim...
摘要:腳本執行,事件處理等。引擎線程,也稱為內核,負責處理腳本程序,例如引擎。事件觸發線程,用來控制事件循環可以理解為,引擎線程自己都忙不過來,需要瀏覽器另開線程協助。異步請求線程,也就是發出請求后,接收響應檢測狀態變更等都是這個線程管理的。 一、進程與線程 現代操作系統比如Mac OS X,UNIX,Linux,Windows等,都是支持多任務的操作系統。 什么叫多任務呢?簡單地說,就是操...
摘要:由此可知閉包是函數的執行環境以及執行環境中的函數組合而構成的。此時產生了閉包。二閉包的作用閉包的特點是讀取函數內部局部變量,并將局部變量保存在內存,延長其生命周期。三閉包的問題使用閉包會將局部變量保持在內存中,所以會占用大量內存,影響性能。 一、什么是閉包 1.閉包的定義 閉包是一種特殊的對象。它由兩部分構成:函數,以及創建該函數的環境(包含自由變量)。環境由閉包創建時在作用域中的任何...
摘要:在中,通過棧的存取方式來管理執行上下文,我們可稱其為執行棧,或函數調用棧。因為執行中最先進入全局環境,所以處于棧底的永遠是全局環境的執行上下文。 一、什么是執行上下文? 執行上下文(Execution Context): 函數執行前進行的準備工作(也稱執行上下文環境) JavaScript在執行一個代碼段之前,即解析(預處理)階段,會先進行一些準備工作,例如掃描JS中var定義的變量、...
摘要:全局作用域局部作用域局部作用域全局作用域局部作用域塊語句沒有塊級作用域塊級聲明包括和,以及和循環,和函數不同,它們不會創建新的作用域。局部作用域只在該函數調用執行期間存在。 一、什么是作用域? 作用域是你的代碼在運行時,各個變量、函數和對象的可訪問性。(可產生作用的區域) 二、JavaScript中的作用域 在 JavaScript 中有兩種作用域 全局作用域 局部作用域 當變量定...
閱讀 2083·2021-11-15 17:57
閱讀 747·2021-11-11 16:54
閱讀 2595·2021-09-27 13:58
閱讀 4083·2021-09-06 15:00
閱讀 955·2021-09-04 16:45
閱讀 3511·2019-08-30 15:56
閱讀 1789·2019-08-30 15:53
閱讀 1620·2019-08-30 14:12