JS 異步編程

教程簡介

作者: TigerChain
地址: https://www.jianshu.com/p/876e68fd6a1c
本文出自 TigerChain 簡書 手把手教 Vue 系列

本節大綱

img

正文

js 是一門單線程、非阻塞、異步、併發語言。

一、同步和異步

1. 同步

同步指的是任務是一個接一個的去完成,上一個任務沒有完成,下一個任務就不能開始,單線程和多線程都可以實現同步,但是單線程一定是同步的「一個線程只有執行完前面的任務,才能執行後面的」

img

2. 異步

異步是一個相對概念,多線程是異步的前提,一個線程是玩不了異步的

img

比如 Java 語言,聲明一個 Thread 看起來只有一個線程,但是調用 start() 方法卻異步執行了,請看圖

img

執行結果

img

從結果來看 java 默認是有一個主線程的「main 線程,上面的 thrad 異步就是相對於 main 而言的」,所以根本不可能一個線程就能完成異步

那麼到底 js 是如何實現異步的呢?說異步我們不得不說以下幾個角色
JavaScript Engine、Web APIs、Message queue、Event loop

接下來一一介紹,首先登場的是 JS 引擎:

二、JavaScript Engine

JS 引擎有的也稱爲 JS 虛擬機,主要是負責解析和執行 js 的,它是瀏覽器所實現的,不同的瀏覽器有不同的實現方式「採用 c/c++ 實現」,這裏以 V8 引擎爲例來說明「其它的引擎都大同小異」

來看看引擎的簡易圖

img

由圖可知,JS 引擎主要包括兩個組件就是堆和棧

堆: 用就是用來分配內存的地方

棧: 也叫 調用棧/執行棧 就是方法調用和執行的地方「js 是單線程說的就是 call stack 」

這裏順便說一下,瀏覽器有渲染引擎和 js 引擎,瀏覽器是從上向下解析 html 標籤的,當遇到 script 標籤「js 代碼」時會立即停止解析,直接執行 js 腳本,所以渲染引擎和 js 引擎是互斥的「js 操作 DOM 的會影響渲染」,這一個過程是同步的,所以加載一個耗時的 js 會導致界面卡死的根本原因就在這裏。

來看看 stack 的執行機制

先來一段代碼

img

這段代碼本身沒有什麼好說的,非常簡單的代碼,我們看看 js 引擎在執行這段代碼的時候 call stack 中的執行過程

img

call stack 由名子可以看出它是一個棧結構「那肯定遵循先進後出原則」,當一個方法調用的時候就入棧,執行完成以後就出棧

再來一段暴力代碼

img

以上代碼是一段暴力代碼,就是一個死循環,我們來看看結果

img

由結果可行,call stack 棧大小被撐爆了,其實可以想像,不停的調用 hello 方法,入棧、入棧 … 入棧,肯定最後就放不下了

棧有多大?

由上面的死循環代碼我們就可以嘗試着算出 call stack 的大小參考 2ality.com

img

當然對不同的瀏覽器結果是不一樣的,引擎實現的方式不一樣,我測試在 chorome 如下「不同瀏覽器大家可自行測試一下」

img

show-cal-call-stack-size-result

我們清楚了當調用一個方法的時候 js 引擎會把方法壓入 call stack ,當方法執行完畢以後出棧

三、Web APIs

由於 js 引擎中的 stack 同一時間只能幹一件事情「單線程」,那麼 call stack 肯定是玩不了異步,可是雖然 js 是單線程的,但瀏覽器卻是多線程的,我們知道 js 有好多 API 有些不是核心 js 語言的一部分,比如 BOM DOM AJAX setTimeOut Canvas WegGl 等 api 瀏覽器可以在調用之外執行這些 api 「另起一個或多個線程跑這些 api」

img

這些 api 就可以獨立於調用棧來執行自己的功能,但是有一個問題是如果這些 api 執行完以後該怎麼辦呢?有兩種方案

  • 我們將 web api 完成的方法直接推送到調用棧
  • 我們採取一些機制來保存這些響應,在合適的時候推送給調用棧

第 1 種方法顯然不靠譜,如果 web api 執行完以後直接把結果給調用棧可以會影響正在執行的調用棧,所以瀏覽器採用第二種方法,使用消息隊列來保存這些 web api 執行的響應以便在調用棧可以調用的時候推送給調用棧,這個保存消息的東西就是接下來我們要說的 Message Queue

四、Message Queue

Message Queue「消息隊列也叫 Callback Queue」是用來保存 Web Api 調用完成以後的所有消息的回調函數,當調用棧「call stack」爲空時「也就是調用棧中的方法執行完畢以後」Message Queue 中的回調方法「先進先出」會被添加到調用棧中去執行,但是瀏覽器是什麼方式來把調用棧和 message Queue 聯繫起來的「什麼機制把 Message Queue 中的回調方法給 call stack 當 call statck 爲空的時候」,它就是 Event Loop

img

五、Event Loop

Event Loop 是把 call stack 和 Message Queue 聯繫起來的紐帶和橋樑,Event Loop 是一個基於事件的併發模型,它時刻在監聽着消息隊列,如果有完成的消息它此刻還要關心 call stack 是否爲空,如果爲空則把 Messag Queue 中的回調結果推送給 call statck 回調方法執行

Event Loop 做兩件事情

  • 監聽 Message Queue「是否有消息」
  • 監聽 call statck 「看是否爲空,如果爲空則推送結果」

img

這樣就完成了 js 的非阻塞異步調用

六、代碼來分析異步調用過程

1. 寫一段如下代碼

img

非常簡單的一段代碼,體現了 js 的異步過程

2. 分析過程

img

上圖顯示了上述代碼的執行過程,簡單的說一下吧,上述代碼分爲十個步驟

  • 當代碼執行 console.log(‘大家好!’)的時候此方法入棧,這個沒有什麼好說的,前面說過了
  • 方法繼續向下執行遇到 setTimeout()方法,這是一個 webapi 方法然後交給 3 去執行
  • 瀏覽器單獨開一個線程去執行,然後調用棧不停止繼續向下執行
  • 調用棧執行 console.log(‘歡迎關注’) 方法
  • web api 執行 setTimeout 方法完畢,指導結果給 Message Queue ,此是 webpai 就變成空的「圖上沒有體現,希望明白」
  • 此時調用棧中執行完 console.log(‘歡迎關注’) 以後此方法出棧,棧此時變成空的,Event Loop 監聽着 Message Queue
  • Event loop 把 Message Queue 中的方法取出來,推給空的調用棧,此時 callback 入棧「調用棧」
  • 執行其中的方法體 console.log(‘TigerChain’)
  • 打印了 TigerChain 此方法出棧
  • 到此 callback 執行完畢,callback 出棧,調用棧變爲空

以上過程只是一個針對簡單代碼的一個簡單的分析,如果存在多個異步操作,則 Event Loop 不停的執行取出消息推入棧的操作直到完成

七、總結

到此我們把 js 非阻塞和異步的原理大概說了一下,相信大家應該有一個簡單的知識和了解,大概總結一下

  • js 是非阻塞異步的單線程「單線程指的就是 call stack」
  • js 實現異步的方式是基於 Event Loop 的併發模型
  • 瀏覽器的 web api 不是 js 核心的部分,但是和 call stack 不衝突執行「瀏覽器另外開線程去執行」
  • web api 的執行結果不能直接給 call stack 先要通過 Message Queu 把結果存起來,等待 Event Loop 去處理
  • Event Loop 如果發現 call statck 爲空時「此時就是推入 Message Queue 中的消息的最佳時機」取出消息隊列中的消息推入給調用棧,異步結束

我們來看一張非常形象的 Event Loop 併發模型的圖

img

圖片來源 The Main Event… Loop 建議把這張圖印在心裏、印在心、印在心裏「重要的事情說三遍」

js 的異步執行過程從上圖非常形象的展現出來,從 statck 開始順時針執行,遇到 webpai 方法讓其去調用並把結果給 Message Queue , Event Loop 查看 stack 爲空則取出 Message Queue 中的方法給 statck 搞懂此圖就徹底瞭解了 Event Loop 的併發模型了。

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