Js非同步機制的實現

WindrunnerMax發表於2020-04-16

Js非同步機制

JavaScript是一門單執行緒語言,所謂單執行緒,就是指一次只能完成一件任務,如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。這種模式的好處是實現起來比較簡單,執行環境相對單純,壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應也就是假死狀態,往往就是因為某一段Javascript程式碼長時間執行比如死迴圈,導致整個頁面卡在這個地方,其他任務無法執行。

執行機制

為了解決上述問題,Javascript將任務的執行模式分為兩種:同步Synchronous與非同步Asynchronous,同步或非同步,表明著是否需要將整個流程按順序地完成,阻塞或非阻塞,意味著你呼叫的函式會不會立刻告訴你結果

同步

同步模式就是同步阻塞,後一個任務等待前一個任務結束,然後再執行,程式的執行順序與任務的排列順序是一致的、同步的。

var i = 100;
while(--i) { console.log(i); }
console.log("while 執行完畢我才能執行");

非同步

非同步執行就是非阻塞模式執行,每一個任務有一個或多個回撥函式callback,前一個任務結束後,不是執行後一個任務,而是執行回撥函式,後一個任務則是不等前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的、非同步的。瀏覽器對於每個Tab只分配了一個Js執行緒,主要任務是與使用者互動以及操作DOM等,而這也就決定它只能為單執行緒,否則會帶來很複雜的同步問題,例如假定JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器無法確定以哪個執行緒的操作為準。

setTimeout(() => console.log("我後執行"), 0);
// 注意:W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms,此外這與瀏覽器設定、主執行緒以及任務佇列也有關係,執行時間可能大於4ms,例如老版本的瀏覽器都將最短間隔設為10毫秒。另外,對於那些DOM的變動尤其是涉及頁面重新渲染的部分,通常不會立即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果要好於setTimeout()。
console.log("我先執行");

非同步機制

首先來看一個例子,與上文一樣來測試一個非同步執行的操作

setTimeout(() => console.log("我在很長時間之後才執行"), 0);
var i = 3000000000;
while(--i) { }
console.log("迴圈執行完畢"); 

本地測試,設定的setTimeout回撥函式大約在30s之後才執行,遠遠大於4ms,我在主執行緒設定了一個非常大的迴圈來阻塞Js主執行緒,注意我並沒有設定一個死迴圈,假如我在此處設定死迴圈來阻塞主執行緒,那麼設定的setTimeout回撥函式將永遠不會執行,此外由於渲染執行緒與JS引擎執行緒是互斥的,Js執行緒在處理任務時渲染執行緒會被掛起,整個頁面都將被阻塞,無法重新整理甚至無法關閉,只能通過使用工作管理員結束Tab程式的方式關閉頁面。
Js實現非同步是通過一個執行棧與一個任務佇列來完成非同步操作的,所有同步任務都是在主執行緒上執行的,形成執行棧,任務佇列中存放各種事件回撥(也可以稱作訊息),當執行棧中的任務處理完成後,主執行緒就開始讀取任務佇列中的任務並執行,不斷往復迴圈。
例如上例中的setTimeout完成後的事件回撥就存在任務佇列中,這裡需要說明的是瀏覽器定時計數器並不是由JavaScript引擎計數的,因為JavaScript引擎是單執行緒的,如果執行緒處於阻塞狀態就會影響記計時的準確,計數是由瀏覽器執行緒進行計數的,當計數完畢,就將事件回撥加入任務佇列,同樣HTTP請求在瀏覽器中也存在單獨的執行緒,也是執行完畢後將事件回撥置入任務佇列。通過這個流程,就能夠解釋為什麼上例中setTimeout的回撥一直無法執行,是由於主執行緒也就是執行棧中的程式碼沒有完成,不會去讀取任務佇列中的事件回撥來執行,即使這個事件回撥早已在任務佇列中。

Event Loop

主執行緒從任務佇列中讀取事件,這個過程是迴圈不斷的,所以整個的這種執行機制又稱為Event LoopEvent Loop是一個執行模型,在不同的地方有不同的實現,瀏覽器和NodeJS基於不同的技術實現了各自的Event Loop。瀏覽器的Event Loop是在HTML5的規範中明確定義,NodeJSEvent Loop是基於libuv實現的。
在瀏覽器中的Event Loop由執行棧Execution Stack、後臺執行緒Background Threads、巨集佇列Macrotask Queue、微佇列Microtask Queue組成。

  • 執行棧就是在主執行緒執行同步任務的資料結構,函式呼叫形成了一個由若干幀組成的棧。
  • 後臺執行緒就是瀏覽器實現對於setTimeoutsetIntervalXMLHttpRequest等等的執行執行緒。
  • 巨集佇列,一些非同步任務的回撥會依次進入巨集佇列,等待後續被呼叫,包括setTimeoutsetIntervalsetImmediate(Node)requestAnimationFrameUI renderingI/O等操作
  • 微佇列,另一些非同步任務的回撥會依次進入微佇列,等待後續呼叫,包括Promiseprocess.nextTick(Node)Object.observeMutationObserver等操作

Js執行時,進行如下流程

  1. 首先將執行棧中程式碼同步執行,將這些程式碼中非同步任務加入後臺執行緒中
  2. 執行棧中的同步程式碼執行完畢後,執行棧清空,並開始掃描微佇列
  3. 取出微佇列隊首任務,放入執行棧中執行,此時微佇列是進行了出隊操作
  4. 當執行棧執行完成後,繼續出隊微佇列任務並執行,直到微佇列任務全部執行完畢
  5. 最後一個微佇列任務出隊並進入執行棧後微佇列中任務為空,當執行棧任務完成後,開始掃面微佇列為空,繼續掃描巨集佇列任務,巨集佇列出隊,放入執行棧中執行,執行完畢後繼續掃描微佇列為空則掃描巨集佇列,出隊執行
  6. 不斷往復...

例項

// Step 1
console.log(1);

// Step 2
setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
}, 0);

// Step 3
new Promise((resolve, reject) => {
  console.log(4);
  resolve();
}).then(() => {
  console.log(5);
})

// Step 4
setTimeout(() => {
  console.log(6);
}, 0);

// Step 5
console.log(7);

// Step N
// ...

// Result
/*
  1
  4
  7
  5
  2
  3
  6
*/
Step 1
// 執行棧 console
// 微佇列 []
// 巨集佇列 []
console.log(1); // 1
Step 2
// 執行棧 setTimeout
// 微佇列 []
// 巨集佇列 [setTimeout1]
setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
}, 0);
Step 3
// 執行棧 Promise
// 微佇列 [then1]
// 巨集佇列 [setTimeout1]
new Promise((resolve, reject) => {
  console.log(4); // 4 // Promise是個函式物件,此處是同步執行的 // 執行棧 Promise console
  resolve();
}).then(() => {
  console.log(5);
})
Step 4
// 執行棧 setTimeout
// 微佇列 [then1]
// 巨集佇列 [setTimeout1 setTimeout2]
setTimeout(() => {
  console.log(6);
}, 0);
Step 5
// 執行棧 console
// 微佇列 [then1]
// 巨集佇列 [setTimeout1 setTimeout2]
console.log(7); // 7
Step 6
// 執行棧 then1
// 微佇列 []
// 巨集佇列 [setTimeout1 setTimeout2]
console.log(5); // 5
Step 7
// 執行棧 setTimeout1
// 微佇列 [then2]
// 巨集佇列 [setTimeout2]
console.log(2); // 2
Promise.resolve().then(() => {
    console.log(3);
});
Step 8
// 執行棧 then2
// 微佇列 []
// 巨集佇列 [setTimeout2]
console.log(3); // 3
Step 9
// 執行棧 setTimeout2
// 微佇列 []
// 巨集佇列 []
console.log(6); // 6

參考

https://www.jianshu.com/p/1a35857c78e5
https://segmentfault.com/a/1190000016278115
https://segmentfault.com/a/1190000012925872
https://www.cnblogs.com/sunidol/p/11301808.html
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop

相關文章