首頁  >  文章  >  web前端  >  Javascript事件環該如何理解? (圖文)

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

不言
不言原創
2018-09-08 17:46:141881瀏覽

本篇文章帶給大家的內容是關於Javascript事件環該如何理解? (圖文),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

堆疊和佇列

在電腦記憶體中存取數據,基本的資料結構分為堆疊和佇列。

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

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

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

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

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

在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的讀取檔

##我們先來看一段程式碼:

//示例1
console.log(1);
setTimeout(function () {
    console.log(2);
}, 1000);
console.log(3);
這段程式碼非常簡單,把它們放在瀏覽器中執行結果如下:

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

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

//示例2
console.log(1);
setTimeout(function () {
    console.log(2);
}, 0); //0毫秒,不延时
console.log(3);
運行結果:

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

//示例3
console.log(1);
setTimeout(function () {
    console.log(2);
}, 0);
Promise.resolve().then(function(){
    console.log(3);
});
console.log(4);
運行結果:

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

javascript是怎麼執行的?

一開始先簡單聊了一下基本的資料結構,它和我們現在說的事件環有什麼關係?當然有,首先要明確的一點是,

javascript程式碼的執行全都在堆疊裡,不論是同步程式碼還是非同步程式碼,這個一定要清楚。

而程式碼我們大體上分為了同步程式碼和非同步程式碼,其實非同步程式碼還可以再分為兩類:

巨集任務微任務

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

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

  1. 从上到下依次解释每一条js语句

  2. 若是同步任务,则压入一个栈(主线程);如果是异步任务,就放到一个任务队列里

  3. 开始执行栈里的同步任务,直到将栈里的所有任务都走完,此时栈清空了

  4. 回过头看异步队列里如果有异步任务完成了,就生成一个事件并注册回调,压入栈中

  5. 再返回第3步,直到异步队列都清空,程序运行结束

语言描述的费劲,不如看图:

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

通过以上的步骤可以看到,不论是同步还是异步,只要是执行的时候都是要在栈里执行的,而一遍又一遍的回头检查异步队列,这种执行方式 就是所谓的“事件环”。

明白了javascript的执行原理,我们就不难理解之前的第二段代码,为什么setTimeout为0时会最后执行,因为setTimeout是异步代码,必须要等所有的同步代码都执行完,才会执行异步队列。即使setTimeout执行得再快,它也不可能在同步代码之前执行。

浏览器中的事件环

聊了这么多,我们好像还没有说宏任务和微任务的话题呢,上面说了,异步任务又分为微任务和宏任务,那它们又是一个怎样的执行机制呢?

注意!微任务和宏任务的执行方式在浏览器和Node中有差异,有差异!重要的事我们多说几遍,以下我们讨论的是在浏览器的环境里。

在浏览器的执行环境中,总是先执行小的、微任务,再执行大的、宏任务,回过头再看看第三段代码,为什么Promise的then方法在setTimeout之前执行?其根本原理就是因为Promise的then方法是一个微任务,而setTimeout是一个宏任务。

接下来我们借用阮一峰老师的一张图来说明:

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

其实,以上这张图示我们可以再将它细化一点,这个图上的异步队列只画了一个,也就是说没有区分微任务队列和宏任务队列。我们可以脑补一下,在此图上多加一个微任务队列,当javascript执行时再多加一个判断,如果是微任务就加到微任务队列里,宏任务就加到宏任务队列里,在清空队列时,浏览器总会优先清空“微任务”。这样就把浏览器的事件环撤底说全了。

最后来一个大考,以下代码的运行结果是什么:

<script>
    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中跑一下,结果是:

5
4
1
2
3

不妨我们试着分析一下为什么是这个结果,首先输出5,因为console.log(5)是同步代码,这没什么可说的。

之后将前两个setTimeout和最后一个Promise放入异步队列,注意它们的区分,此时执行完了同步代码之后发现微任务和宏任务队列中都有代码,按浏览器的事件环机制,优先执行微任务,此时输出4。

然后执行宏任务队列里的第一个setTimeout,输出1。

此时,setTimeout中又有一个Promise,放入微任务队列。

再次清空微任务队列,输出2。

最后宏任务队列里还有最后一个setTimeout,输出3。

Node中的事件环

而Node中的事件环又和浏览器有些许的不同,在node.js的官方文档中有专门的描述,其中文档中有一张图,详细的说明了它的事件环机制,我们把它拿出来:

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

可以看到,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');
    });
});

运行结果:

Promise
immediate
timeout

代码并不复杂,首先使用fs模块读取了一个文件,在回调的内部有两个宏任务和一个微任务,微任务总是优于宏任务执行的,因此先输出Promise。

但是之后的区别为什么先输出immdiate?原因就在于fs读取文件的宏任务在上图中的第4个轮询阶段,当第4个阶段清空队列之后,就该进入第5个check阶段,也就是setImmediate这个宏任务所在的阶段,而不会跳回第1个阶段,因此先输出immedate。

尾巴

最后总结一下,分析完浏览器和Node的事件环发现它们并不简单,但只要记住了它们之间的区别就可以分析出结果。

浏览器事件环是运行完一个宏任务马上清空微任务队列
Node事件环是清空完一个阶段的宏任务队列之后再清空微任务队列

最后,总结一下常见的宏任务和微任务:

宏任务 微任务
setTimeout Promise的then方法
setInterval process.nextTick
setImmediate MutationObserver
MessageChannel

相关推荐:

细说JavaScript事件循环机制-第二讲

javascript实现线程以及事件循环详解

以上是Javascript事件環該如何理解? (圖文)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn