Javascript事件環該如何理解?(圖文)

來源:互聯網
上載者:User
本篇文章給大家帶來的內容是關於Javascript事件環該如何理解?(圖文),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。

棧和隊列

在電腦記憶體中存取資料,基本的資料結構分為棧和隊列。

棧(Stack)是一種後進先出的資料結構,注意,有時候也管棧叫做“堆棧”,但是“堆”又是另一種複雜的資料結構,它和棧完全是兩碼事。棧的特點是操作只在一端進行,一般來說,棧的操作只有兩種:進棧和出棧。第一個進棧的資料總是最後一個才出來。

隊列(Queue)和棧類似,但它是先進先出的資料結構,插入資料的操作從隊列的一端進行,而刪除的操作在另一端。

通俗的比喻棧就像是一個立好的桶,先放入棧的資料會放在桶底,出棧時會在桶口一一將資料取出,所以最先放入棧的資料總是最後一個才能取出。而隊列就像是一個水管,最先放入隊列的資料會第一個從隊列的另一端流出,這是它們最大的區別。

在javascript中,函數的執行就一個典型的入棧與出棧的過程:

function fun1() {    function fun2() {        function fun3() {            console.log('do it');        }        fun3();    }    fun2();}fun1();

在程式執行時,首先將fun1,fun2,fun3依次入棧,而在調用函數時,是先將fun3調用(出棧),再是fun2和fun1,試想一下,如果fun1先出棧,那麼函數fun2和fun3必將丟失。

單線程和非同步

在javascript這門語言中程式是單線程的,只有一個主線程,這是為什嗎?因為不難想像,最初javascript的設計是跑在瀏覽器中的指令碼語言,如果設計成多線程,兩個線程同時修改DOM那以誰的為準呢?所以javascript為單線程,在一個線程中代碼會一句一句向下走,直到程式跑完,若中間有較為費時的操作,那也只能等著。

單線程的設計使得語言的執行效率很差,為了利用多核心CPU的效能,javascript語言支援非同步代碼,當有較為費時的操作時,可將任務寫為非同步執行,當一個非同步任務還沒有執行完時,主線程會將非同步任務掛起,繼續執行後面的同步代碼,之後再回過頭來看,如果有非同步任務運行完了再執行它。

這種執行代碼的方式其實很符合我們生活中的很多情境,比如小明同學下班回家了,他很渴,想燒水泡茶,如果是同步的執行方式那就是燒水,在水沒開時小明像個傻子似的等著,等水開了再泡茶;若是非同步執行,小明先開始燒水,然後就去幹點別的事,比如看會電視、聽聽音樂,等水燒開了再去泡茶。明顯第二種非同步方式效率更高。

常見的非同步作業都有哪些?有很多,我們可以羅列幾個常見的:

  • Ajax

  • DOM的事件操作

  • setTimeout

  • Promise的then方法

  • Node的讀取檔案

我們先來看一段代碼:

//樣本1console.log(1);setTimeout(function () {    console.log(2);}, 1000);console.log(3);

這段代碼非常簡單,把它們放在瀏覽器中執行結果如下:

132

因為setTimeout函數延時了1000毫秒執行,因此先輸出1和3,而2是過了1000毫秒之後再輸出,這很合邏輯。

我們稍稍改動一下代碼,將setTimeout的延時時間改為0:

//樣本2console.log(1);setTimeout(function () {    console.log(2);}, 0); //0毫秒,不延時console.log(3);

運行結果:

132

為什麼延時了0毫秒還是最後輸出的2?先別急,我們再來看一段代碼:

//樣本3console.log(1);setTimeout(function () {    console.log(2);}, 0);Promise.resolve().then(function(){    console.log(3);});console.log(4);

運行結果:

1432

以上三段代碼,如果你能正確的寫出結果,並且能說明白為什麼這樣輸出,說明你對javascript的事件環理解的很清楚,如果講不出來,我們就一起聊聊這裡面發生了什麼,其實很有意思。

javascript是怎麼執行的?

一開始先簡單聊了聊基本的資料結構,它和我們現在說的事件環有什麼關係嗎?當然有,首先要明確的一點是,javascript代碼的執行全都在棧裡,不論是同步代碼還是非同步代碼,這個一定要清楚。

而代碼我們大體上分為了同步代碼和非同步代碼,其實非同步代碼還可以再分為兩類:宏任務微任務

先別管什麼是宏任務和微任務,往往這種高大上的術語不利於我們理解,我們先這麼認為:宏,即是宏觀的、大的;微即微觀的、小的。

javascript是解釋型語言,它的執行過程是這樣的:

  1. 從上到下依次解釋每一條js語句

  2. 若是同步任務,則壓入一個棧(主線程);如果是非同步任務,就放到一個任務隊列裡

  3. 開始執行棧裡的同步任務,直到將棧裡的所有任務都走完,此時棧清空了

  4. 回過頭看非同步隊列裡如果有非同步任務完成了,就產生一個事件並註冊回調,壓入棧中

  5. 再返回第3步,直到非同步隊列都清空,程式運行結束

語言描述的費勁,不如看圖:

通過以上的步驟可以看到,不論是同步還是非同步,只要是執行的時候都是要在棧裡執行的,而一遍又一遍的回頭檢查非同步隊列,這種執行方式 就是所謂的“事件環”。

明白了javascript的執行原理,我們就不難理解之前的第二段代碼,為什麼setTimeout為0時會最後執行,因為setTimeout是非同步代碼,必須要等所有的同步代碼都執行完,才會執行非同步隊列。即使setTimeout執行得再快,它也不可能在同步代碼之前執行。

瀏覽器中的事件環

聊了這麼多,我們好像還沒有說宏任務和微任務的話題呢,上面說了,非同步任務又分為微任務和宏任務,那它們又是一個怎樣的執行機制呢?

注意!微任務和宏任務的執行方式在瀏覽器和Node中有差異,有差異!重要的事我們多說幾遍,以下我們討論的是在瀏覽器的環境裡。

在瀏覽器的執行環境中,總是先執行小的、微任務,再執行大的、宏任務,回過頭再看看第三段代碼,為什麼Promise的then方法在setTimeout之前執行?其根本原理就是因為Promise的then方法是一個微任務,而setTimeout是一個宏任務。

接下來我們借用阮一峰老師的一張圖來說明:

其實,以上這張圖示我們可以再將它細化一點,這個圖上的非同步隊列只畫了一個,也就是說沒有區分微任務隊列和宏任務隊列。我們可以腦補一下,在此圖上多加一個微任務隊列,當javascript執行時再多加一個判斷,如果是微任務就加到微任務隊列裡,宏任務就加到宏任務隊列裡,在清空隊列時,瀏覽器總會優先清空“微任務”。這樣就把瀏覽器的事件環撤底說全了。

最後來一個大考,以下代碼的運行結果是什麼:

<script type="text/javascript">    setTimeout(function () {        console.log(1);        Promise.resolve().then(function () {            console.log(2);        });    });    setTimeout(function () {        console.log(3);    });    Promise.resolve().then(function () {        console.log(4);    });    console.log(5);</script>

將此代碼拷到chrome中跑一下,結果是:

54123

不妨我們試著分析一下為什麼是這個結果,首先輸出5,因為console.log(5)是同步代碼,這沒什麼可說的。

之後將前兩個setTimeout和最後一個Promise放入非同步隊列,注意它們的區分,此時執行完了同步代碼之後發現微任務和宏任務隊列中都有代碼,按瀏覽器的事件環機制,優先執行微任務,此時輸出4。

然後執行宏任務隊列裡的第一個setTimeout,輸出1。

此時,setTimeout中又有一個Promise,放入微任務隊列。

再次清空微任務隊列,輸出2。

最後宏任務隊列裡還有最後一個setTimeout,輸出3。

Node中的事件環

而Node中的事件環又和瀏覽器有些許的不同,在node.js的官方文檔中有專門的描述,其中文檔中有一張圖,詳細的說明了它的事件環機制,我們把它拿出來:

可以看到,node.js中的事件環機制分為了6個階段,其中最重要的3個階段我在上面做了註明:

  • timer階段,指的就是setTimeout等宏任務

  • poll輪詢階段,如讀取檔案等宏任務

  • check階段,setImmediate宏任務

圖中每一個階段都代表了一個宏任務隊列,在Node事件環中,微任務的運行時機是在每一個“宏任務隊列”清空之後,在進入下一個宏任務隊列之間執行。這是和瀏覽器的最大區別。

還是用代碼說話吧,有一道經典的Node.js事件環面試題:

const fs = require('fs');fs.readFile('./1.txt', (err, data) => {    setTimeout(() => {        console.log('timeout');    });    setImmediate(() => {        console.log('immediate');    });    Promise.resolve().then(() => {        console.log('Promise');    });});

運行結果:

Promiseimmediatetimeout

代碼並不複雜,首先使用fs模組讀取了一個檔案,在回調的內部有兩個宏任務和一個微任務,微任務總是優於宏任務執行的,因此先輸出Promise。

但是之後的區別為什麼先輸出immdiate?原因就在於fs讀取檔案的宏任務在中的第4個輪詢階段,當第4個階段清空隊列之後,就該進入第5個check階段,也就是setImmediate這個宏任務所在的階段,而不會跳回第1個階段,因此先輸出immedate。

尾巴

最後總結一下,分析完瀏覽器和Node的事件環發現它們並不簡單,但只要記住了它們之間的區別就可以分析出結果。

瀏覽器事件環是運行完一個宏任務馬上清空微任務隊列
Node事件環是清空完一個階段的宏任務隊列之後再清空微任務隊列

最後,總結一下常見的宏任務和微任務:

宏任務 微任務
setTimeout Promise的then方法
setInterval process.nextTick
setImmediate MutationObserver
MessageChannel
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.