JavaScript 語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。為了協調事件、用戶交互、腳本、UI 渲染和網絡處理等行為,防止主線程的不阻塞,Event Loop 的方案應用而生。Event Loop 包含兩類:一類是基於 Browsing Context,一種是基於 Worker。二者的運行是獨立的,也就是說,每一個 JavaScript 運行的"線程環境"都有一個獨立的 Event Loop,每一個 Web Worker 也有一個獨立的 Event Loop。為了協調這些進行函數調用或者任務的調度,任務隊列就產生了。
一、先搞懂兩個東西:堆和棧
棧由操作系統自動分配釋放,用於存放函數的參數值、局部變量等一些基本的數據類型,其操作方式類似於數據結構中的棧
堆用於存放對象(引用數據類型),一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收。分配方式類似於鏈表。
棧使用的是一級緩存, 他們通常都是被調用時處於存儲空間中,調用完畢立即釋放。
堆則是存放在二級緩存中,生命周期由虛擬機的垃圾回收算法來決定(並不是一旦成為孤兒對象就能被回收)。所以調用這些對象的速度要相對來得低一些。
棧: 在函數調用時,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向函數的返回地址,也就是主函數中的下一條指令的地址,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容由程序員安排。
1、棧
函數中定義的局部變量按照先后定義的順序依次壓入棧中,也就是說相鄰變量的地址之間不會存在其它變量。棧的內存地址生長方向與堆相反,由高到底,所以后定義的變量地址低於先定義的變量。
2、堆
var car1 = { name: 'huruqing', money: 100000000 } var car2 = car1; car2.money = 1000; console.log(car1.money === car2.money); //true 1000
var obj1 = { a: 2 } var obj2 = { a: 2 } console.log(obj1 === obj2); // false
var arr1 = []; var arr2 = []; console.log(arr1 === arr2); // false
棧相比於堆,在程序中應用較為廣泛,最常見的是函數的調用過程由棧來實現,函數返回地址、EBP、實參和局部變量都采用棧的方式存放。雖然棧有眾多的好處,但是由於和堆相比不是那么靈活,有時候分配大量的內存空間,主要還是用堆。
二、事件循環模型
解釋:
當一個任務被執行時,js會判斷是否為同步任務,同步任務和異步任務會進入不同的執行環境,所有的同步任務都會進入到主執行棧立即執行,所有的異步任務都會會被加入到對應的事件管理模塊,當事件發生時管理模塊會將回調函數及其數據添加到回調隊列中,只有當初始化代碼執行完后(可能要一定時間), 才會遍歷讀取回調隊列中的回調函數執行。這一過程的不斷重復就是事件循環。
(簡單點理解就是當一個任務被執行時,js會判斷是否為同步任務,同步任務和異步任務會進入不同的執行環境,所有的同步任務都會進入到主執行棧,所有的異步任務都會進入到任務隊列,直到主執行棧的任務執行完畢才會執行任務隊列的異步任務,這一過程的不斷重復就是事件循環。)
舉例子:
function fn1() { console.log('fn1()') } fn1() document.getElementById('btn').onclick = function () { console.log('點擊了btn') } setTimeout(function () { console.log('定時器執行了') }, 2000) function fn2() { console.log('fn2()') } fn2()
再來看這個的輸出順序
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
這里promise.then和setTimeout都是異步的,那為什么先輸出promise.then里面的呢?
因為promise函數是同步任務會立即執行,其后的.then是異步里面的微任務,setTimeout是異步里面的宏任務,先執行完微任務再執行宏任務。
async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end')
console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') return Promise.resolve().then(()=>{ console.log('async2 end1') }) } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end')
async 函數(包含函數語句、函數表達式、Lambda表達式)會返回一個 Promise 對象,如果在函數中 return
一個直接量,async 會把這個直接量通過 Promise.resolve()
封裝成 Promise 對象。Promise 的特點——無等待,所以在沒有 await
的情況下執行 async 函數,它會立即執行,async2就是立即執行,返回一個 Promise 對象,並且,絕不會阻塞后面的語句。這和普通返回 Promise 對象的函數並無二致。但是有了await就不一樣了,實際上await是一個讓出線程的標志。await后面的函數會先執行一遍,然后就會跳出整個async函數來執行后面js棧(后面會詳述)的代碼。等本輪事件循環執行完了之后又會跳回到async函數中等待await后面表達式的返回值,如果返回值為非promise則繼續執行async函數后面的代碼,否則將返回的promise放入promise隊列(Promise的Job Queue)
三、補充微任務與宏任務
宏任務:整個js代碼塊、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 環境)
微任務:Promise、MutaionObserver、process.nextTick(Node.js 環境)
其執行順序如下圖所示:
所以,上面那個程序:
主程序(整個js代碼塊)和和settimeout都是宏任務,兩個promise是微任務
第一個宏任務(主程序)執行完,執行全部的微任務(兩個promise),再執行下一個宏任務(settimeout),所以輸出結果就是那樣的。
四、定時器引發的思考
定期器分為一次性定時器setTimeout與周期性定時器setInterval,前者是等待N秒之后執行回調一次沒了,后者是每隔N秒執行回調一次。
並不代表執行時間,而是將回調函數加入任務隊列的時間。
setTimeout(() => console.log('1'), 3000); setTimeout(() => console.log('2'), 3000);
這兩個定時器的輸出順序是什么?
你肯定會說先輸出1,在輸出2,我上面說了,時間並不代表執行時間,而是告訴事件管理模塊三秒后將第一個加入到任務隊列,再過三秒,再將第二個加入到任務隊列,等到主執行棧任務為空了在調用任務隊列,至於主執行棧什么時候執行完畢那就要看里面代碼的執行情況,所以定時器的執行時間不一定是3000。
通過這個例子可以看出。