JS是在瀏覽器中運行的,瀏覽器為了運行JS, 必須要編譯或解釋JS,因為JS是高級語言,計算機不認識,必須把它編譯或解釋成機器語言,其次,在運行JS的過程,瀏覽器還要創建堆棧,因為程序是在棧中執行,執行過程中的創建的對象是在堆中。瀏覽器的JS引擎,比如V8,就是做這些事的。JS引擎負責編譯或解釋JS,並創建堆棧來運行JS。
比如,執行以下代碼,
function multiply (x, y) {
return x * y } function printSquare (x) { const s = multiply(x, x) console.log(s) } printSquare(5)
程序初始化,棧為空;程序開始執行,調用printSquare(5),printSquare函數入棧並執行,它調用了multiply(x, x), multiply函數入棧並執行,執行完畢返回25,multiply函數彈棧,回到printSquare, 執行它后面的代碼,也就是console.log , console.log 也是函數,進棧,執行完,彈棧,然后回到printSquare,,執行consoe.log 后面的代碼,后面沒有代碼了,printSquare也就執行完了,彈棧,回到調用printSquare的地方,執行它后面的代碼,它后面也沒有代碼,所有程序執行完畢,棧為空。整個調用棧的情況如下圖所示,
在JS中,棧就是記錄了程序執行到了什么地方,如果調用一個函數,這個函數就放到棧中,如果從函數中返回,就把該函數彈出棧。每一次的調用,都會創建stack frame。程序執行出錯,也可以通過調用棧,追蹤到程序在什么地方出錯。
function foo() {
throw new Error('SessionStack will help you resolve crashes :)'); }
function bar() { foo(); }
function start() { bar(); }
start();
錯誤信息如下,start調用了bar,bar調用了foo,foo報錯了。
如果函數一直調用呢? 那就棧溢出了。因為棧在內存中開辟的,內存不可能無限大,內存是有限的,棧也就是有限。遞歸處理不好,容易棧溢出。
function f () {
return f() } f()
函數的調用棧如下
通過上面的例子,你會發現,只有一個棧在執行程序,這就是JS的單線程,JS引擎中只有一個調用棧,一次只能處理一件事情。調用棧並不屬於JS,它是JS引擎的一部分。
如果僅僅是運行JS,作用也不大,因為JS本身沒有輸入或輸出等與外界交互的能力,因此瀏覽器除了包含JS引擎,還提供了JS與外界交互的能力。這些能力是通過API提供的,比如document, fetch等等,把它們注入到JS的全局作用域中,在JS運行時,可以直接使用它們。這些API統稱為 web API,或外部API,因為它們也不屬於JS。運行JS並能和外部交互,這很好,但也會帶來一個問題, 比如,fetch() 向服務器請求數據,可能要很長時間,JS是單線程也就意味着,要等到它執行結束,才能執行它后面的代碼,如果一直等,那后面的代碼就不用執行了,瀏覽器也就卡死了。如果某件事情執行時間過長,怎么辦?異步處理。為了支持異步,瀏覽器提供了事件循環和事件隊列,以及向事件隊列中插入事件的功能。因此JS的運行時,也就是瀏覽器,要包含以下幾部分
假設執行如下代碼
console.log('js') setTimeout(function cb() { console.log(' awesome!') }, 5000) console.log(' is')
console.log(‘js’),函數入調用棧並執行,控制台輸出js, 函數執行完畢,彈棧,
setTimeout()執行,這是一個Web API,是瀏覽器內部實現的,調用Web API,只是告訴瀏覽器幫我們做事情,setTimeout是告訴瀏覽器5s之后執行cb函數,
告訴完瀏覽器,setTimeout也就執行完了,彈棧,此時瀏覽器設置定時器,並開始倒數計時,
console.log('is')執行,進棧,出棧,瀏覽器控制台輸出is.
5s 過后,計時器完成計時,瀏覽器把回調函數cb放到了事件隊列中
此時,事件循環發現事件隊列中有一個事件,它就會檢查調用棧是不是空,如果調用棧為空,它就會把事件拿出來,放到調有棧中。
回調函數cb執行,console.log(‘awesome’) 進棧,出棧,控制台輸出awesome,回調函數執行完了, 出棧。
整個程序執行完畢。在整個程序的執行過程中,異步的實現或異步代碼的執行,是瀏覽器幫我們安排的,瀏覽器安排異步代碼,插入到事件隊列中,事件循環則調用JS引擎,從事件隊列中取出要執行的代碼發給它。JS引擎只不過是一個按需執行的環境來執行JS代碼。從事件隊列中取出事件到調用棧中執行,也稱為一個tick.
到了ES6,增加了Promise,情況有所變化。Promise異步的處理方式和傳統的回調函數處理方式不一樣,promise中注冊的回調函數稱為Job或Micortask,所以JS從概念上定義了兩個隊列,Microtask或Job隊列和Macrotask隊列,而不再是一個隊列。Promise完成后的回調,是放到Microtask或Job隊列中,傳統回調函數放到Macrotask隊列,當然,它們不僅僅處理這些。因為有了兩個隊列,tick的定義也要改一下,從Macrotask隊列中取出事件並執行,稱為一個tick。主程序執行完畢,先檢查Micortask隊列中有沒有micortask或job(回調函數),如果有,就會執行該micortask,執行完畢后,還是檢查Micortask隊列中有沒有事microtask,直到Micortask隊列中所有microtask執行完畢,它才執行Macrotask隊列中的macrotask,從中取出一個開始執行(tick),如果在一個tick的執行過程中,有一個Promise完成了,這個Prmise注冊的回調函數(microtask),並不是插入到整個Macrotask隊列的后面,而是插入到當前tick后面的Micortask隊列中,Micotask隊列就是附在事件循環中每一個tick后面的隊列。當tick執行完畢,從它后面的Microtask隊列中取出microtask,進行執行,由於事件中還可能有promise完成,promise注冊的回調函數,又會插入到當前tick后的Microtask中,形成一個Microtask隊列,所以要等到后面的Microtask隊列中所有microtask執行完畢,再從Macrotask取出一個事件執行。
這里要注意,由於Microtask可能執行其它Microtask,Microtask隊列可以一直增加下去,如要是這樣的話,事件循環就不能從當前tick中跳出,后面的Macrotask就無法執行。為了阻止這種情況發生,瀏覽器內置了保護機制,一個tick最多執行1000個microtasks,執行完成后,執行下一個macrotask.
console.log('script start') const interval = setInterval(() => { console.log('setInterval') }, 0) setTimeout(() => { console.log('setTimeout 1') Promise.resolve() .then(() => console.log('promise 3')) .then(() => console.log('promise 4')) .then(() => { setTimeout(() => { console.log('setTimeout 2') Promise.resolve().then(() => console.log('promise 5')) .then(() => console.log('promise 6')) .then(() => clearInterval(interval)) }, 0) }) }, 0) Promise.resolve() .then(() => console.log('promise 1')) .then(() => console.log('promise 2'))
程序執行,也可以稱為第一個tick。console.log(), 進棧,執行,出棧,控制台輸出script start。setInterval進棧,告訴瀏覽器每隔0s,控制台輸出setInterval,瀏覽器設置定時器,setInterval執行完畢,出棧。setTimeout進棧,告訴瀏覽器0s后,執行一段代碼,瀏覽器設置定時器,setTimeout執行完畢,出棧. Promise.resovle執行,兩個then回調函數放入到microtasks隊列中。程序執行完畢,第一個tick執行完畢,此時要檢查當前tick后面的microtasks隊列有沒有task。有,就是Promise.resovle的兩個回調,依次執行,控制台輸出promise 1 和 promise 2。0s肯定過了,瀏覽器把setInterval和setTimeout放入到macrotask隊列中。
每二個tick,從macrotask隊列中取出settInterval 的回調函數,控制台輸出settInterval ,它沒有產生microtask,也就沒有microtasks隊列,0s過了,瀏覽器又到macrotask隊列中放入settInterval 。此時macrotask隊列中 [setTimeout, settInterval]
第三個tick,setTimeout注冊的回調函數執行,控制台輸出 setTimeout 1,Promise.resovle執行,三個then放入到microtasks,microtasks是放到當前tick后,tick執行完畢,檢查它后面的microtasks隊列,有。依次執行,控制台輸出Promise 3和 Promise 4,另外一個setTimeout放到macrotask隊列中,稱它為setTimeout2。此時,macrotask隊列[settInterval, setTimeout ]
第四個tick,從macrotask隊列中取出settInterval 的回調函數,控制台輸出settInterval ,它沒有產生microtask,也就沒有microtasks隊列,0s過了,瀏覽器又到macrotask隊列中放入settInterval 。此時macrotask隊列中 [setTimeout2, settInterval]
第五個tick,setTimeout2注冊的回調函數執行,控制台輸出 setTimeout 2,Promise.resovle執行,三個then放入到microtasks,microtasks是放到當前tick后,tick執行完畢,檢查它后面的microtasks隊列,有。依次執行,控制台輸出Promise 5和 Promise 6,同時清除掉了setInterval,此時,macrotask隊列[]。