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 設備運行結果出來後,再回過頭來,把掛起的任務繼續執行下去。這就是異步操作。

於是,所有的任務可以分成兩種,一種是同步任務,一種是異步任務

現在我們來看一下異步任務的執行機制:

  1. 所有同步任務都在主線程上執行,形成一個 執行棧

  1. 主線程之外,還存在一個 消息隊列,只要異步任務有了運行結果,就在 消息隊列中放置一個事件,並通知主線程

  2. 一旦 執行棧中的所有同步任務執行完畢,系統就會讀取 消息隊列,相應的事件就結束了等待的狀態,進入主線程,開始執行

主線程會不斷的執行上面的三個步驟,只要主線程空了,就會去讀取 消息隊列,這就是 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
// 我是異步回調

setTimeoutbar函數執行結束後延時 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延時時間到),就再去執行它。

值得注意的是:

另外,異步回調是指回調函數函數在主函數外部執行,一般有兩種方式:

  1. 第一種:把異步任務添加到消息隊列尾部

  2. 第二種:把異步任務添加到微任務隊列中,這樣就可以在當前任務的末尾處執行微任務了

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Cpj2ePlujjeIrym_Ecll2w