異步的概念
同步是發出一次調用,一直等待結果返回,再繼續往下執行。而異步是不需要等待結果返回。實際看到的現象就是前面的代碼還沒執行完成,就跳到後面執行了。當在編程中遇到阻塞操作(比如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();
first()
被推入棧頂。console.log('Hi there!')
被推入棧頂,這行代碼執行完成後接着被彈出棧。second()
被推入棧頂。- 執行
second()
裏的代碼,console.log('Hello there!')
被推入棧頂,這行代碼執行完成後接着被彈出棧。 second()
執行完成,被彈出棧。console.log(‘The End’)
被推入棧頂,這行代碼執行完成後接着被彈出棧。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