摘要:同步異步回調(diào)傻傻分不清楚。分割線上面主要講了同步和回調(diào)執(zhí)行順序的問題,接著我就舉一個包含同步異步回調(diào)的例子。同步優(yōu)先回調(diào)內(nèi)部有個,第二個是一個回調(diào)回調(diào)墊底。異步也,輪到回調(diào)的孩子們回調(diào),出來執(zhí)行了。
同步、異步、回調(diào)?傻傻分不清楚。
大家注意了,教大家一道口訣:
同步優(yōu)先、異步靠邊、回調(diào)墊底(讀起來不順)
用公式表達就是:
同步 => 異步 => 回調(diào)
這口訣有什么用呢?用來對付面試的。
有一道經(jīng)典的面試題:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log("i: ",i); }, 1000); } console.log(i); //輸出 5 i: 5 i: 5 i: 5 i: 5 i: 5
這道題目大家都遇到過了吧,那么為什么會輸出這個呢?記住我們的口訣 同步 => 異步 => 回調(diào)
1、for循環(huán)和循環(huán)體外部的console是同步的,所以先執(zhí)行for循環(huán),再執(zhí)行外部的console.log。(同步優(yōu)先)
2、for循環(huán)里面有一個setTimeout回調(diào),他是墊底的存在,只能最后執(zhí)行。(回調(diào)墊底)
那么,為什么我們最先輸出的是5呢?
非常好理解,for循環(huán)先執(zhí)行,但是不會給setTimeout傳參(回調(diào)墊底),等for循環(huán)執(zhí)行完,就會給setTimeout傳參,而外部的console打印出5是因為for循環(huán)執(zhí)行完成了。
知乎有大神講解過 80% 應聘者都不及格的 JS 面試題 ,就是以這個例子為開頭的。但是沒有說為什么setTimeout是輸出5個5。
這里涉及到JavaScript執(zhí)行棧和消息隊列的概念,概念的詳細解釋可以看阮老師的 JavaScript 運行機制詳解:再談Event Loop - 阮一峰的網(wǎng)絡日志,或者看 并發(fā)模型與Event Loop
《圖片來自于MDN官方》
我拿這個例子做一下講解,JavaScript單線程如何處理回調(diào)呢?JavaScript同步的代碼是在堆棧中順序執(zhí)行的,而setTimeout回調(diào)會先放到消息隊列,for循環(huán)每執(zhí)行一次,就會放一個setTimeout到消息隊列排隊等候,當同步的代碼執(zhí)行完了,再去調(diào)用消息隊列的回調(diào)方法。
在這個經(jīng)典例子中,也就是說,先執(zhí)行for循環(huán),按順序放了5個setTimeout回調(diào)到消息隊列,然后for循環(huán)結(jié)束,下面還有一個同步的console,執(zhí)行完console之后,堆棧中已經(jīng)沒有同步的代碼了,就去消息隊列找,發(fā)現(xiàn)找到了5個setTimeout,注意setTimeout是有順序的。
那么,setTimeout既然在最后才執(zhí)行,那么他輸出的i又是什么呢?答案就是5。。有人說不是廢話嗎?
現(xiàn)在告訴大家為什么setTimeout全都是5,JavaScript在把setTimeout放到消息隊列的過程中,循環(huán)的i是不會及時保存進去的,相當于你寫了一個異步的方法,但是ajax的結(jié)果還沒返回,只能等到返回之后才能傳參到異步函數(shù)中。
在這里也是一樣,for循環(huán)結(jié)束之后,因為i是用var定義的,所以var是全局變量(這里沒有函數(shù),如果有就是函數(shù)內(nèi)部的變量),這個時候的i是5,從外部的console輸出結(jié)果就可以知道。那么當執(zhí)行setTimeout的時候,由于全局變量的i已經(jīng)是5了,所以傳入setTimeout中的每個參數(shù)都是5。很多人都會以為setTimeout里面的i是for循環(huán)過程中的i,這種理解是不對的。
===========================================分割線=========================================
看了上面的解釋,你是不是有點頭暈,沒事,繼續(xù)深入講解。
我們給第一個例子加一行代碼。
for (var i = 0; i < 5; ++i) { setTimeout(function() { console.log("2: ",i); }, 1000); console.log("1: ", i); //新加一行代碼 } console.log(i); //輸出 1: 0 1: 1 1: 2 1: 3 1: 4 5 2: 5 2: 5 2: 5 2: 5 2: 5
來,大家再跟著我一起念一遍:同步 => 異步 => 回調(diào) (強化記憶)
這個例子可以很清楚的看到先執(zhí)行for循環(huán),for循環(huán)里面的console是同步的,所以先輸出,for循環(huán)結(jié)束后,執(zhí)行外部的console輸出5,最后再執(zhí)行setTimeout回調(diào) 55555。。。
=====================================分割線============================================
這么簡單,不夠帶勁是不是,那么面試官會問,怎么解決這個問題?
最簡單的當然是let語法啦。。
for (let i = 0; i < 5; ++i) { setTimeout(function() { console.log("2: ",i); }, 1000); } console.log(i); //輸出 i is not defined 2: 0 2: 1 2: 2 2: 3 2: 4
咦,有同學問,為什么外部的i報錯了呢?
又有同學問,你這個口訣在這里好像不適應???
let是ES6語法,ES5中的變量作用域是函數(shù),而let語法的作用域是當前塊,在這里就是for循環(huán)體。在這里,let本質(zhì)上就是形成了一個閉包。也就是下面這種寫法一樣的意思。如果面試官對你說用下面的這種方式,還有l(wèi)et的方式,你可以嚴肅的告訴他:這就是一個意思!這也就是為什么有人說let是語法糖。
var loop = function (_i) { setTimeout(function() { console.log("2:", _i); }, 1000); }; for (var _i = 0; _i < 5; _i++) { loop(_i); } console.log(i);
面試官總說閉包、閉包、閉包,什么是閉包?后面再講。
寫成ES5的形式,你是不是發(fā)現(xiàn)就適合我說的口訣了?而用let的時候,你發(fā)現(xiàn)看不懂?那是因為你沒有真正了解ES6的語法原理。
我們來分析一下,用了let作為變量i的定義之后,for循環(huán)每執(zhí)行一次,都會先給setTimeout傳參,準確的說是給loop傳參,loop形成了一個閉包,這樣就執(zhí)行了5個loop,每個loop傳的參數(shù)分別是0,1,2,3,4,然后loop里面的setTimeout會進入消息隊列排隊等候。當外部的console執(zhí)行完畢,因為for循環(huán)里的i變成了一個新的變量 _i ,所以在外部的console.log(i)是不存在的。
現(xiàn)在可以解釋閉包的概念了:當內(nèi)部函數(shù)以某一種方式被任何一個外部函數(shù)作用域訪問時,一個閉包就產(chǎn)生了。
我知道你又要我解釋這句話了,loop(_i)是外部函數(shù),setTimeout是內(nèi)部函數(shù),當setTimeout被loop的變量訪問的時候,就形成了一個閉包。(別說你又暈了?)
隨便舉個新的例子。
function t() { var a = 10; var b = function() { console.log(a); } b(); } t(); //輸出 10
跟我一起念口訣:同步 => 異步 => 回調(diào) (強化記憶)
先執(zhí)行函數(shù)t,然后js就進入了t內(nèi)部,定義了一個變量,然后執(zhí)行函數(shù)b,進入b內(nèi)部,然后打印a,這里都是同步的代碼,沒什么異議,那么這里怎么解釋閉包:函數(shù)t是外部函數(shù),函數(shù)b是內(nèi)部函數(shù),當函數(shù)b被函數(shù)t的變量訪問的時候,就形成了閉包。
========================================分割線==============================================
上面主要講了同步和回調(diào)執(zhí)行順序的問題,接著我就舉一個包含同步、異步、回調(diào)的例子。
let a = new Promise( function(resolve, reject) { console.log(1) setTimeout(() => console.log(2), 0) console.log(3) console.log(4) resolve(true) } ) a.then(v => { console.log(8) }) let b = new Promise( function() { console.log(5) setTimeout(() => console.log(6), 0) } ) console.log(7)
看到這個例子,千萬不要害怕?,先讀一遍口訣:同步 => 異步 => 回調(diào) (強化記憶)
1、看同步代碼:a變量是一個Promise,我們知道Promise是異步的,是指他的then()和catch()方法,Promise本身還是同步的,所以這里先執(zhí)行a變量內(nèi)部的Promise同步代碼。(同步優(yōu)先)
console.log(1) setTimeout(() => console.log(2), 0) //回調(diào) console.log(3) console.log(4)
2、Promise內(nèi)部有4個console,第二個是一個setTimeout回調(diào)(回調(diào)墊底)。所以這里先輸出1,3,4回調(diào)的方法丟到消息隊列中排隊等著。
3、接著執(zhí)行resolve(true),進入then(),then是異步,下面還有同步?jīng)]執(zhí)行完呢,所以then也滾去消息隊列排隊等候。(真可憐)(異步靠邊)
4、b變量也是一個Promise,和a一樣,執(zhí)行內(nèi)部的同步代碼,輸出5,setTimeout滾去消息隊列排隊等候。
5、最下面同步輸出7。
6、同步的代碼執(zhí)行完了,JavaScript就跑去消息隊列呼叫異步的代碼:異步,出來執(zhí)行了。這里只有一個異步then,所以輸出8。
7、異步也over,輪到回調(diào)的孩子們:回調(diào),出來執(zhí)行了。這里有2個回調(diào)在排隊,他們的時間都設置為0,所以不受時間影響,只跟排隊先后順序有關(guān)。則先輸出a里面的回調(diào)2,最后輸出b里面的回調(diào)6。
8、最終輸出結(jié)果就是:1、3、4、5、7、8、2、6。
我們還可以稍微做一點修改,把a里面Promise的 setTimeout(() => console.log(2), 0)改成 setTimeout(() => console.log(2), 2),對,時間改成了2ms,為什么不改成1試試呢?1ms的話,瀏覽器都還沒有反應過來呢。你改成大于或等于2的數(shù)字就能看到2個setTimeout的輸出順序發(fā)生了變化。所以回調(diào)函數(shù)正常情況下是在消息隊列順序執(zhí)行的,但是使用setTimeout的時候,還需要注意時間的大小也會改變它的順序。
====================================分割線==================================================
口訣不一定是萬能的,只能作為一個輔助,更重要的還是要理解JavaScript的運行機制,才能對代碼執(zhí)行順序有清晰的路線。
還有async/await等其他異步的方案,不管是哪種異步,基本都適用這個口訣,對于新手來說,可以快速讀懂面試官出的js筆試題目。以后再也不用害怕做筆試題啦。
特殊情況下不適應口訣的也很正常,JavaScript博大精深,不是一句話就能概括出來的。
最后,在跟著我念一遍口訣:同步 => 異步 => 回調(diào)
如果文章對你有幫助,請點擊一下推薦。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/82315.html
摘要:同步異步回調(diào)傻傻分不清楚。分割線上面主要講了同步和回調(diào)執(zhí)行順序的問題,接著我就舉一個包含同步異步回調(diào)的例子。同步優(yōu)先回調(diào)內(nèi)部有個,第二個是一個回調(diào)回調(diào)墊底。異步也,輪到回調(diào)的孩子們回調(diào),出來執(zhí)行了。 同步、異步、回調(diào)?傻傻分不清楚。 大家注意了,教大家一道口訣: 同步優(yōu)先、異步靠邊、回調(diào)墊底(讀起來不順) 用公式表達就是: 同步 => 異步 => 回調(diào) 這口訣有什么用呢?用來對付面試的...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。異步編程入門的全稱是前端經(jīng)典面試題從輸入到頁面加載發(fā)生了什么這是一篇開發(fā)的科普類文章,涉及到優(yōu)化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結(jié)思考,循序漸進的理解 TypeScript。 網(wǎng)絡基礎知識之 HTTP 協(xié)議 詳細介紹 HTT...
摘要:編寫異步代碼可能是一種不同的體驗,尤其是對異步控制流而言。回調(diào)函數(shù)的準則在編寫異步代碼時,要記住的第一個規(guī)則是在定義回調(diào)時不要濫用閉包。為回調(diào)創(chuàng)建命名函數(shù),避免使用閉包,并將中間結(jié)果作為參數(shù)傳遞。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關(guān)注我的專欄,之后的博文將在專...
閱讀 964·2023-04-25 23:54
閱讀 3046·2021-11-08 13:21
閱讀 3773·2021-09-27 13:35
閱讀 3391·2021-07-26 23:41
閱讀 1054·2019-08-30 15:52
閱讀 3439·2019-08-30 11:27
閱讀 2097·2019-08-29 18:37
閱讀 537·2019-08-29 17:24