摘要:是怎么執(zhí)行的一開始先簡單聊了聊基本的數(shù)據(jù)結(jié)構(gòu),它和我們現(xiàn)在說的事件環(huán)有什么關(guān)系么當(dāng)然有,首先要明確的一點(diǎn)是,代碼的執(zhí)行全都在棧里,不論是同步代碼還是異步代碼,這個(gè)一定要清楚。
棧和隊(duì)列
在計(jì)算機(jī)內(nèi)存中存取數(shù)據(jù),基本的數(shù)據(jù)結(jié)構(gòu)分為棧和隊(duì)列。
棧(Stack)是一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),注意,有時(shí)候也管棧叫做“堆?!?,但是“堆”又是另一種復(fù)雜的數(shù)據(jù)結(jié)構(gòu),它和棧完全是兩碼事。棧的特點(diǎn)是操作只在一端進(jìn)行,一般來說,棧的操作只有兩種:進(jìn)棧和出棧。第一個(gè)進(jìn)棧的數(shù)據(jù)總是最后一個(gè)才出來。
隊(duì)列(Queue)和棧類似,但它是先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),插入數(shù)據(jù)的操作從隊(duì)列的一端進(jìn)行,而刪除的操作在另一端。
通俗的比喻棧就像是一個(gè)立好的桶,先放入棧的數(shù)據(jù)會(huì)放在桶底,出棧時(shí)會(huì)在桶口一一將數(shù)據(jù)取出,所以最先放入棧的數(shù)據(jù)總是最后一個(gè)才能取出。而隊(duì)列就像是一個(gè)水管,最先放入隊(duì)列的數(shù)據(jù)會(huì)第一個(gè)從隊(duì)列的另一端流出,這是它們最大的區(qū)別。
在javascript中,函數(shù)的執(zhí)行就一個(gè)典型的入棧與出棧的過程:
function fun1() { function fun2() { function fun3() { console.log("do it"); } fun3(); } fun2(); } fun1();
在程序執(zhí)行時(shí),首先將fun1,fun2,fun3依次入棧,而在調(diào)用函數(shù)時(shí),是先將fun3調(diào)用(出棧),再是fun2和fun1,試想一下,如果fun1先出棧,那么函數(shù)fun2和fun3必將丟失。
單線程和異步在javascript這門語言中程序是單線程的,只有一個(gè)主線程,這是為什么?因?yàn)椴浑y想像,最初javascript的設(shè)計(jì)是跑在瀏覽器中的腳本語言,如果設(shè)計(jì)成多線程,兩個(gè)線程同時(shí)修改DOM那以誰的為準(zhǔn)呢?所以javascript為單線程,在一個(gè)線程中代碼會(huì)一句一句向下走,直到程序跑完,若中間有較為費(fèi)時(shí)的操作,那也只能等著。
單線程的設(shè)計(jì)使得語言的執(zhí)行效率很差,為了利用多核心CPU的性能,javascript語言支持異步代碼,當(dāng)有較為費(fèi)時(shí)的操作時(shí),可將任務(wù)寫為異步執(zhí)行,當(dāng)一個(gè)異步任務(wù)還沒有執(zhí)行完時(shí),主線程會(huì)將異步任務(wù)掛起,繼續(xù)執(zhí)行后面的同步代碼,之后再回過頭來看,如果有異步任務(wù)運(yùn)行完了再執(zhí)行它。
這種執(zhí)行代碼的方式其實(shí)很符合我們生活中的很多場景,比如小明同學(xué)下班回家了,他很渴,想燒水泡茶,如果是同步的執(zhí)行方式那就是燒水,在水沒開時(shí)小明像個(gè)傻子似的等著,等水開了再泡茶;若是異步執(zhí)行,小明先開始燒水,然后就去干點(diǎn)別的事,比如看會(huì)電視、聽聽音樂,等水燒開了再去泡茶。明顯第二種異步方式效率更高。
常見的異步操作都有哪些?有很多,我們可以羅列幾個(gè)常見的:
Ajax
DOM的事件操作
setTimeout
Promise的then方法
Node的讀取文件
我們先來看一段代碼:
//示例1 console.log(1); setTimeout(function () { console.log(2); }, 1000); console.log(3);
這段代碼非常簡單,把它們放在瀏覽器中執(zhí)行結(jié)果如下:
1 3 2
因?yàn)閟etTimeout函數(shù)延時(shí)了1000毫秒執(zhí)行,因此先輸出1和3,而2是過了1000毫秒之后再輸出,這很合邏輯。
我們稍稍改動(dòng)一下代碼,將setTimeout的延時(shí)時(shí)間改為0:
//示例2 console.log(1); setTimeout(function () { console.log(2); }, 0); //0毫秒,不延時(shí) console.log(3);
運(yùn)行結(jié)果:
1 3 2
為什么延時(shí)了0毫秒還是最后輸出的2?先別急,我們再來看一段代碼:
//示例3 console.log(1); setTimeout(function () { console.log(2); }, 0); Promise.resolve().then(function(){ console.log(3); }); console.log(4);
運(yùn)行結(jié)果:
1 4 3 2
以上三段代碼,如果你能正確的寫出結(jié)果,并且能說明白為什么這樣輸出,說明你對javascript的事件環(huán)理解的很清楚,如果講不出來,我們就一起聊聊這里面發(fā)生了什么,其實(shí)很有意思。
javascript是怎么執(zhí)行的?一開始先簡單聊了聊基本的數(shù)據(jù)結(jié)構(gòu),它和我們現(xiàn)在說的事件環(huán)有什么關(guān)系么?當(dāng)然有,首先要明確的一點(diǎn)是,javascript代碼的執(zhí)行全都在棧里,不論是同步代碼還是異步代碼,這個(gè)一定要清楚。
而代碼我們大體上分為了同步代碼和異步代碼,其實(shí)異步代碼還可以再分為兩類:宏任務(wù)和微任務(wù)。
先別管什么是宏任務(wù)和微任務(wù),往往這種高大上的術(shù)語不利于我們理解,我們先這么認(rèn)為:宏,即是宏觀的、大的;微即微觀的、小的。
javascript是解釋型語言,它的執(zhí)行過程是這樣的:
從上到下依次解釋每一條js語句
若是同步任務(wù),則壓入一個(gè)棧(主線程);如果是異步任務(wù),就放到一個(gè)任務(wù)隊(duì)列里
開始執(zhí)行棧里的同步任務(wù),直到將棧里的所有任務(wù)都走完,此時(shí)棧清空了
回過頭看異步隊(duì)列里如果有異步任務(wù)完成了,就生成一個(gè)事件并注冊回調(diào),壓入棧中
再返回第3步,直到異步隊(duì)列都清空,程序運(yùn)行結(jié)束
語言描述的費(fèi)勁,不如看圖:
通過以上的步驟可以看到,不論是同步還是異步,只要是執(zhí)行的時(shí)候都是要在棧里執(zhí)行的,而一遍又一遍的回頭檢查異步隊(duì)列,這種執(zhí)行方式 就是所謂的“事件環(huán)”。
明白了javascript的執(zhí)行原理,我們就不難理解之前的第二段代碼,為什么setTimeout為0時(shí)會(huì)最后執(zhí)行,因?yàn)閟etTimeout是異步代碼,必須要等所有的同步代碼都執(zhí)行完,才會(huì)執(zhí)行異步隊(duì)列。即使setTimeout執(zhí)行得再快,它也不可能在同步代碼之前執(zhí)行。
瀏覽器中的事件環(huán)聊了這么多,我們好像還沒有說宏任務(wù)和微任務(wù)的話題呢,上面說了,異步任務(wù)又分為微任務(wù)和宏任務(wù),那它們又是一個(gè)怎樣的執(zhí)行機(jī)制呢?
注意!微任務(wù)和宏任務(wù)的執(zhí)行方式在瀏覽器和Node中有差異,有差異!重要的事我們多說幾遍,以下我們討論的是在瀏覽器的環(huán)境里。
在瀏覽器的執(zhí)行環(huán)境中,總是先執(zhí)行小的、微任務(wù),再執(zhí)行大的、宏任務(wù),回過頭再看看第三段代碼,為什么Promise的then方法在setTimeout之前執(zhí)行?其根本原理就是因?yàn)镻romise的then方法是一個(gè)微任務(wù),而setTimeout是一個(gè)宏任務(wù)。
接下來我們借用阮一峰老師的一張圖來說明:
其實(shí),以上這張圖示我們可以再將它細(xì)化一點(diǎn),這個(gè)圖上的異步隊(duì)列只畫了一個(gè),也就是說沒有區(qū)分微任務(wù)隊(duì)列和宏任務(wù)隊(duì)列。我們可以腦補(bǔ)一下,在此圖上多加一個(gè)微任務(wù)隊(duì)列,當(dāng)javascript執(zhí)行時(shí)再多加一個(gè)判斷,如果是微任務(wù)就加到微任務(wù)隊(duì)列里,宏任務(wù)就加到宏任務(wù)隊(duì)列里,在清空隊(duì)列時(shí),瀏覽器總會(huì)優(yōu)先清空“微任務(wù)”。這樣就把瀏覽器的事件環(huán)撤底說全了。
最后來一個(gè)大考,以下代碼的運(yùn)行結(jié)果是什么:
將此代碼拷到chrome中跑一下,結(jié)果是:
5 4 1 2 3
不妨我們試著分析一下為什么是這個(gè)結(jié)果,首先輸出5,因?yàn)?b>console.log(5)是同步代碼,這沒什么可說的。
之后將前兩個(gè)setTimeout和最后一個(gè)Promise放入異步隊(duì)列,注意它們的區(qū)分,此時(shí)執(zhí)行完了同步代碼之后發(fā)現(xiàn)微任務(wù)和宏任務(wù)隊(duì)列中都有代碼,按瀏覽器的事件環(huán)機(jī)制,優(yōu)先執(zhí)行微任務(wù),此時(shí)輸出4。
然后執(zhí)行宏任務(wù)隊(duì)列里的第一個(gè)setTimeout,輸出1。
此時(shí),setTimeout中又有一個(gè)Promise,放入微任務(wù)隊(duì)列。
再次清空微任務(wù)隊(duì)列,輸出2。
最后宏任務(wù)隊(duì)列里還有最后一個(gè)setTimeout,輸出3。
Node中的事件環(huán)而Node中的事件環(huán)又和瀏覽器有些許的不同,在node.js的官方文檔中有專門的描述,其中文檔中有一張圖,詳細(xì)的說明了它的事件環(huán)機(jī)制,我們把它拿出來:
可以看到,node.js中的事件環(huán)機(jī)制分為了6個(gè)階段,其中最重要的3個(gè)階段我在上面做了注明:
timer階段,指的就是setTimeout等宏任務(wù)
poll輪詢階段,如讀取文件等宏任務(wù)
check階段,setImmediate宏任務(wù)
圖中每一個(gè)階段都代表了一個(gè)宏任務(wù)隊(duì)列,在Node事件環(huán)中,微任務(wù)的運(yùn)行時(shí)機(jī)是在每一個(gè)“宏任務(wù)隊(duì)列”清空之后,在進(jìn)入下一個(gè)宏任務(wù)隊(duì)列之間執(zhí)行。這是和瀏覽器的最大區(qū)別。
還是用代碼說話吧,有一道經(jīng)典的Node.js事件環(huán)面試題:
const fs = require("fs"); fs.readFile("./1.txt", (err, data) => { setTimeout(() => { console.log("timeout"); }); setImmediate(() => { console.log("immediate"); }); Promise.resolve().then(() => { console.log("Promise"); }); });
運(yùn)行結(jié)果:
Promise immediate timeout
代碼并不復(fù)雜,首先使用fs模塊讀取了一個(gè)文件,在回調(diào)的內(nèi)部有兩個(gè)宏任務(wù)和一個(gè)微任務(wù),微任務(wù)總是優(yōu)于宏任務(wù)執(zhí)行的,因此先輸出Promise。
但是之后的區(qū)別為什么先輸出immdiate?原因就在于fs讀取文件的宏任務(wù)在上圖中的第4個(gè)輪詢階段,當(dāng)?shù)?個(gè)階段清空隊(duì)列之后,就該進(jìn)入第5個(gè)check階段,也就是setImmediate這個(gè)宏任務(wù)所在的階段,而不會(huì)跳回第1個(gè)階段,因此先輸出immedate。
尾巴最后總結(jié)一下,分析完瀏覽器和Node的事件環(huán)發(fā)現(xiàn)它們并不簡單,但只要記住了它們之間的區(qū)別就可以分析出結(jié)果。
瀏覽器事件環(huán)是運(yùn)行完一個(gè)宏任務(wù)馬上清空微任務(wù)隊(duì)列。
Node事件環(huán)是清空完一個(gè)階段的宏任務(wù)隊(duì)列之后再清空微任務(wù)隊(duì)列。
最后,總結(jié)一下常見的宏任務(wù)和微任務(wù):
宏任務(wù) | 微任務(wù) |
---|---|
setTimeout | Promise的then方法 |
setInterval | process.nextTick |
setImmediate | MutationObserver |
MessageChannel |
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/97544.html
摘要:沒有任何意外,王小二的公司用來開發(fā)公司的主打產(chǎn)品。臃腫的著手開干吧小二打開熟悉的,找到提交訂單模塊的。要不再去請教下哥的煩惱小二找到哥,詳細(xì)的描述了他的問題。 流行的MVC架構(gòu)模式 如今的Web開發(fā),各種框架風(fēng)起云涌,勢如破竹。 從國民第一的ThinkPhp到稱霸全球的Laravel,這些框架有一個(gè)共同特征,都采用了MVC的架構(gòu)模式。 showImg(https://segmentfa...
摘要:在函數(shù)式編程中數(shù)據(jù)在由純函數(shù)組成的管道中傳遞。函數(shù)式編程中函子是實(shí)現(xiàn)了函數(shù)的容器下文中將函子視為范疇,模型可表示如下但是在函數(shù)式編程中要避免使用這種面向?qū)ο蟮木幊谭绞饺《畬ν獗┞读艘粋€(gè)的接口也稱為。 showImg(https://segmentfault.com/img/remote/1460000018101204); 該系列會(huì)有 3 篇文章,分別介紹什么是函數(shù)式編程、剖析函數(shù)...
摘要:任務(wù)描述使用的來發(fā)布多個(gè)目錄使用的忽略所有結(jié)尾的文件。任務(wù)描述使用的配置項(xiàng)在項(xiàng)目發(fā)布后重啟進(jìn)程忽略當(dāng)次構(gòu)建過程并提交一次使用的配置項(xiàng)設(shè)置每個(gè)遠(yuǎn)程命令超時(shí)時(shí)間為秒。下一期地址使用發(fā)布前端項(xiàng)目安全篇官方交流群 本系列文章共分為基礎(chǔ)篇,安全篇,拓展篇。 前言 曾幾何時(shí),我相信部分Web Developer(包括我)使用的項(xiàng)目發(fā)布方式比較傳統(tǒng)(使用xftp或者sublime text的插件sf...
摘要:有鎖的地方就會(huì)有鎖競爭,并且也是一個(gè)耗時(shí)的過程,所以同一個(gè)如果并發(fā)出現(xiàn)在日志堆棧中勢必會(huì)導(dǎo)致一部分線程會(huì),這對于線上系統(tǒng)中簡直就是災(zāi)難改進(jìn)在最近的版本中并沒有發(fā)現(xiàn)對的類做特殊處理,并且正如官方說的所以只要的版本大于,這個(gè)配置默認(rèn)都是關(guān)閉的 起因 偶然一次路過同事電腦,看著黑底藍(lán)色滿屏的堆棧信息,過去笑著拍了拍他的肩膀說道「小哥,又在寫B(tài)UG呢」湊過去仔細(xì)看了一眼異常堆棧詳情,「虎軀一震...
摘要:移動(dòng)組的壕星在灌水群里發(fā)了條消息,這家伙又準(zhǔn)備顯擺他的腎了。什么是小夏弱弱的問。配備身份證書,防止身份被冒充。那這么說,豈不是將來所有網(wǎng)站都會(huì)用小夏接著道,又加了個(gè)羞澀的表情。 中秋前,本牛仔正在苦思新任務(wù)的對策,突然桌面右下角的小企鵝一陣跳動(dòng):剛收到消息, 第三方下載的XCode被植入后門,N多app信息疑似泄密。還是你們用安卓的好啊,唉。 移動(dòng)組的壕星在灌水群里發(fā)了條消息,這家伙又...
閱讀 1605·2021-09-23 11:21
閱讀 2365·2021-09-07 10:13
閱讀 847·2021-09-02 10:19
閱讀 1143·2019-08-30 15:44
閱讀 1734·2019-08-30 13:18
閱讀 1921·2019-08-30 11:15
閱讀 1117·2019-08-29 17:17
閱讀 2026·2019-08-29 15:31