JS的執行機制的總結!

常好樂發表於2018-12-17

JS是單執行緒語言,為什麼要一開始設計成單執行緒語言呢???

JS這門語言設計之初,一 是用來做 頁面指令碼,也就是客戶端的一些簡單互動,比如:表單驗證這樣的事情,二是用來操作DOM,如果JS開兩個執行緒做事情,如果碰見兩個執行緒在操作同一個DOM,那麼,瀏覽器就會迷惑到底要相信哪一個執行緒的話。所以,為了避免未來JS發展太過於冗雜,JS從誕生以來就沿用單執行緒的底層設計在發展,並一直沿用至今。

同步和非同步和任務佇列:

正因為JS是單執行緒語言,所以同步和非同步的概念就格外重要。JS所有的執行任務分為兩種,一種是同步任務,一種是非同步任務。 可以粗略地認為:同步任務執行在 執行棧上,非同步任務放在任務佇列中。 我們所寫的按順序普通執行的執行函式都放在主執行緒執行棧中。 所有的事件回撥(例如:點選事件),setTimeout,setInterVal,Promise.then(()=>{})還有 $http()請求,都是非同步任務,放置在執行佇列中等待被執行。 所以JS的Event Loop機制如下: (1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。 (2)執行棧之外,還存在一個"任務佇列"。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。 (3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。 (4)主執行緒不斷重複上面的第三步。 然而,還沒完的是, 上面所說的任務佇列並非那麼簡單,任務佇列之中還會細化為兩種模式:MacroTasks和MicroTasks。 通常地: MacroTasks包括有:
滑鼠,鍵盤等回撥事件
setTimeout
setInterval
setImmediate
requestAnimationFrame I/O UI rendering

MicroTasks包括有:

process.nextTick

Promise.then().then().then()

MVVM框架例如vue的Object.observe等等、

JS的事件迴圈執行機制就可以看做一個 執行棧+無限個任務佇列(MacroTasks) 所構成。而MicroTasks就是附著在 主執行緒或者每一個 任務佇列(MacroTasks)末尾的 小任務佇列。每一次任務佇列中的任務執行完後,JS就會去檢測當前附著的MicroTasks裡面有沒有任務,有就執行MicroTasks裡的任務,沒有,就執行下一個大的任務佇列(MacroTasks)裡的任務。依次迴圈。直至終結。

面試經典題:

  console.log(1)
}, 0);
new Promise(function executor(resolve) {
  console.log(2);
  for( var i=0 ; i<10000 ; i++ ) {
    i == 9999 && resolve();
  }
  console.log(3);
}).then(function() {
  console.log(4);
});
console.log(5);
複製程式碼

執行順序:2,3,5,4,1 1.由於setTimeout 定時函式 定義出來,首先就被放置在下一個任務佇列(MacroTasks)中,所以即便第二個引數設定是0,也會延遲到主執行緒內所有任務執行完後再執行。 2.碰到New Promise()函式,Promise函式內部是按正常流程的主執行緒中執行的,所以首先輸出2。for迴圈依舊是在主執行緒裡執行,按正常執行,然後碰到輸出3,則再正常輸出3。 3.碰到then()方法,由於then()內部是放在任務佇列中,由於主執行緒還未執行完,所以then()裡的執行被擱置。 4.輸出5在主執行緒中,然後輸出5,當前首個任務佇列在執行棧中執行完畢。 5.主執行緒跑完了,接下來就是重點。由上面所說,Promise屬性MicroTasks,所以在當前任務佇列執行完後,進去MicroTasks佇列去看看有沒有Promise.then()之類MicroTasks,發現有,則執行輸出4。 6.最後,當前首個任務佇列執行完了,附著在任務佇列最後的MicroTasks佇列也執行完了,就可以執行下一個 任務佇列(MacroTasks),即一開始被setTimeout放置的console.log(1),即最後 輸出1。

經典例題:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}
複製程式碼

由於for迴圈內是一個 壓入任務佇列(MacroTasks)的setTimeout非同步函式,所以for迴圈內部並沒有去執行那setTimeout內部的匿名回撥函式,但確實執行了第二個引數1000*i,定義瞭然後執行到了下一個 任務佇列(MacroTasks),然後進入到setTimeout的回撥函式中去,由於此時,對於回撥的匿名函式內部來說,i已經累加到5了,所以,i=5是一個定值。所以會持續輸出五個5,由於第二個引數確實在for迴圈中執行了,所以是在第0秒,第1秒,第2秒,第3秒,第4秒期間持續輸入的五個5。

那如何改成 正常地輸出 0,1,2,3,4呢?

for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}
複製程式碼

或者: var i 改成let i

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
}
複製程式碼

通過立即執行函式或 var 改成let,來自己構成一個封閉的作用域, 原理是: 讓i 處在 執行的時候當前的作用域,而非 i處在定義的時候i當前的作用域

相關文章