Javascript異步回調原理

異步的概念

同步是發出一次調用,一直等待結果返回,再繼續往下執行。而異步是不需要等待結果返回。實際看到的現象就是前面的代碼還沒執行完成,就跳到後面執行了。當在編程中遇到阻塞操作(比如IO操作)時很有用處,因爲io阻塞期間,線程處於阻塞狀態,CPU不幹活,多線程異步可以減少程序的響應時間。比如同步的遠程調用阻塞的A,B兩個服務,A響應10ms,B響應10ms,總耗時=A耗時+B耗時=20ms。如果改爲異步調用,則爲MAX(A耗時,B耗時)=10ms。

js中異步,回調是很常見也是必須這樣做的。比如:

axios.get('/user?ID=12345')
  .then(function (response) {
    console.log("1");
  })
 console.log("2");
//肯定先輸出2,再輸出1

從後端程序員的視角來看,要實現異步編程的效果必然是使用多線程,線程是可被獨立調度的指令序列,只有開啓另外一條獨立的線程,最終結果纔可能是不按照順序的。比如:

new Thread(() -> System.out.println("耗時操作")).start();//啓動一個新的線程
System.out.println(2);//主線程
//先輸出2,再輸出“耗時操作”

這兩行代碼可能是這樣執行的:當前代碼在主線程中執行,遇到thread的start(),調用底層c/c++庫,調用系統調用創建新的線程,新的線程此時處於可運行狀態,當分配到CPU時間片後就會執行。主線程繼續往下執行,輸出“2”。新線程被調度到分配CPU時間片,執行代碼輸出“耗時操作”,(忽略中間的阻塞,中斷過程)。所以異步的本質原因是多線程,多個獨立可被調度的指令序列,單線程只能是順序執行,存在異步難道不是很奇怪的事情嗎?

理解異步js

關鍵概念:

  • call stack
  • event loop
  • message queue/task queue

理解調用棧

任何編程語言的方法調用都是通過來實現的,js是單線程的所以只有一個call stack。用以下例子來演示代碼是怎麼在js引擎中執行的:

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();
  1. first()被推入棧頂。
  2. console.log('Hi there!')被推入棧頂,這行代碼執行完成後接着被彈出棧。
  3. second()被推入棧頂。
  4. 執行second()裏的代碼,console.log('Hello there!')被推入棧頂,這行代碼執行完成後接着被彈出棧。
  5. second()執行完成,被彈出棧。
  6. console.log(‘The End’)被推入棧頂,這行代碼執行完成後接着被彈出棧。
  7. first()執行完成,被彈出棧。

理解事件循環

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
networkRequest();
console.log('The End');

這裏使用了 setTimeout 方法來模擬網絡請求。setTimeout 屬於web APIs。event loop,web APIs ,message queue/task queue都不是 js引擎的一部分,這些功能是由js的運行時環境(瀏覽器或者nodejs)提供。

上述代碼,首先networkRequest()入棧,接着setTimeout()入棧,由於setTimeout是一個web APIs,由運行時環境啓動一個定時器,這裏是2秒,定時器過期後會執行回調函數,“啓動定時器”後setTimeout()就相當於執行完成了,彈棧,接着networkRequest()彈棧。

這裏異步的關鍵在於,定時器裏的回調方法不入棧,而是進入一個message queue,然後由event loop處理。

Event Loop的任務是檢查call stack是否爲空,如果爲空,它去檢查message queue是否存在等待執行的callback方法。

在這裏例子中,當console.log('The End');被執行完成後,棧爲空,此時Event Loop會去message queue查找回調方法,也就是執行console.log('Async Code');

DOM事件

message queue也包含dom事件的回調方法,比如:

document.querySelector('.btn').addEventListener('click',(event) => {
  console.log('Button Clicked');
});

web API環境中的事件監聽器等待事件發生,然後將回調方法放在message queue 等待執行。然後 event loop檢查call stack是否爲空,如果爲空,回調方法被執行。

ES6 Job Queue/ Micro-Task queue

ES6的Promises引入了job queue/micro-task queue的概念,它們與 message queue 的區別在於job queue的優先級高於message queue 。這意味着promise的回調總是優先於上面提到的dom事件及setTimeout的回調先執行的。

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
    resolve('Promise resolved');
  }).then(res => console.log(res))
    .catch(err => console.log(err));
console.log('Script End');

輸出:

Script start
Script End
Promise resolved
setTimeout

參考

  1. Understanding Asynchronous JavaScript
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章