JS 之同步操作 vs 異步操作
作者:Pingan8787
來源:SegmentFault 思否社區
1、單線程的 JavaScript
我們都知道,js 是一門單線程語言,何爲單線程?就是在同一時間,只能做一件事。
爲什麼 js 要這麼設計呢?js 的主要用途就是操作 DOM,與用戶進行操作,所以如果 js 有兩個線程,這時一個線程在某個節點上修改內容,另一個線程也在該節點上修改該內容,那 js 要以誰爲準呢?
所以 js 的單線程當然是爲了高效安全
爲了提高利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準,允許 js 腳本創建多個線程,但是子線程完全受主線程控制且不得操作 DOM。所以這個新標準並沒有改變 js 單線程的本質。
2、同步任務和異步任務
js 的單線程就意味着所有任務都需要排隊,前一個任務結束,纔會執行下一個任務。但是 IO 設備很慢,當需要讀取數據時,這時候CPU
就會停下來等待IO
操作,更要命的是即使該CPU
再忙,其它CPU
也不會幫忙,大家你看我我看你,這就特別影響用戶體驗了。
所以爲了解決阻塞式IO
帶來的不好的體驗,js 規定了,這時候主線程完全可以不管IO
設備,將其掛起處於等待中的任務,然後繼續運行後面的任務,等到 IO 設備運行結果出來後,再回過頭來,把掛起的任務繼續執行下去。這就是異步操作。
於是,所有的任務可以分成兩種,一種是同步任務,一種是異步任務
-
同步任務:在主線程上執行任務,這個前一個任務執行完之後,才能執行下一個任務;如果前一個任務沒有執行完,那麼線程會一直等待下去,直到該任務執行完纔會繼續執行
-
異步任務:任務不進入主線程,而是進入 “消息隊列”,主線程不會一直等待下去,而是繼續執行下面的任務,當只有消息隊列通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行
現在我們來看一下異步任務的執行機制:
- 所有同步任務都在主線程上執行,形成一個 執行棧
-
主線程之外,還存在一個
消息隊列
,只要異步任務有了運行結果,就在消息隊列
中放置一個事件,並通知主線程 -
一旦
執行棧
中的所有同步任務執行完畢,系統就會讀取消息隊列
,相應的事件就結束了等待的狀態,進入主線程,開始執行
主線程會不斷的執行上面的三個步驟,只要主線程空了,就會去讀取 消息隊列
,這就是 JavaScript 的執行機制。
3、消息隊列和事件循環
消息隊列就是隊列,也是遵循先進先出的原則。IO
線程每完成一項任務,就會將該任務添加到消息隊列中。
所以先進入的任務會優先被主線程讀取,只要執行棧一清空,即同步任務已執行完畢,消息隊列中的任務就會依次進入主線程。但是有一種特殊情況,那就是定時器,定時器時間沒到,是不會被添加到主線程的。
現在我們知道異步操作後消息隊列會通知主線程,可以來取事件執行了,那麼問題來了,這個通知機制是怎麼實現的呢?
答案就是事件循環
事件循環(Event Loop
):事件循環是指主線程重複從消息隊列中取消息、執行消息的過程。
而這裏的事件就是我們熟悉的回調函數,該回調函數是在註冊異步任務的時候添加的。
所以,工作線程將事件添加到消息隊列中,主線程通過事件循環去讀取事件。而實際上,主線程只會做一件事,就是從任務對列中讀取消息、執行消息,再讀取、再執行,直到消息隊列爲空。並且每次主線程只有在將當前的消息執行完畢之後,纔會去取下一個消息。
下面我們用一張圖來更好的表示這個過程:
主線程在運行的時候,會產生堆(heap)和 棧(stack),棧中的代碼會調用外部的 API,它們在 消息隊列
中加入各種事件,只要棧中的代碼執行完畢,主線程就會去讀取 消息隊列
,依次執行那些事件所對應的回調函數
4、定時器
我們先來看一下同步回調
function callback() {
console.log('我是同步回調');
}
function bar(fn) {
console.log(123);
fn();
console.log(456);
}
bar(callback);
// 123
// 我是同步回調
// 456
callback
函數作爲參數傳給了bar
函數,在bar
函數中的callback
就是回調函數,而且是同步回調。
我們再來看看異步回調的例子:
function foo() {
console.log('我是異步回調');
}
function bar(fn) {
console.log(123);
setTimeout(fn, 1000);
console.log(456);
}
bar(foo);
// 123
// 456
// 我是異步回調
setTimeout
在bar
函數執行結束後延時 1s 後再執行,這種回調函數在主函數外部執行的過程就稱爲異步回調。
顯然,setTimeout()
定時器是一個異步任務,系統會先執行執行棧中的同步任務,再回過頭來執行 消息隊列
中的事件。
即使定時器的延時時間爲 0
function foo() {
console.log('我是異步回調');
}
function bar(fn) {
console.log(123);
setTimeout(fn, 0);
console.log(456);
}
bar(foo);
// 123
// 456
// 我是異步回調
因爲setTimeout
本質就是異步任務,無論如何它都會被掛起,js 先執行同步任務後,發現消息隊列中的任務可以執行了(setTimeout
延時時間到),就再去執行它。
值得注意的是:
-
定時器事件雖然是添加到
任務隊列
中了,但是也得等它定時完成之後,纔會去指定它 -
如果此時它已經位於隊列的首位了,但是定時時間還未結束,此時,它也不會被執行,後面事件會先執行
另外,異步回調是指回調函數函數在主函數外部執行,一般有兩種方式:
-
第一種:把異步任務添加到消息隊列尾部
-
第二種:把異步任務添加到微任務隊列中,這樣就可以在當前任務的末尾處執行微任務了
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Cpj2ePlujjeIrym_Ecll2w