同步、異步回調?傻傻分不清楚。前端
你們注意了,教你們一道口訣:面試
同步優先、異步靠邊、回調墊底!ajax
公式表達:同步=>異步=>回調瀏覽器
這口訣的用處是什麼呢?至少應付面試,徹底夠用!安全
例1:(經典面試題)babel
for(var i=0; i<5; i++){閉包
setTimeout(function(){併發
console.log('i:',i);異步
},1000);async
}
console.log(i);
此處先看結果:
5
i:5
i:5
i:5
i:5
i:5
想必你們都遇到過這樣的題目吧,那麼爲何會是這樣的輸出結果呢?
來,跟着我念:」同步優先,異步靠邊,回調墊底!「
首先:for循環及循環外部的console是同步的,因此先執行for循環再執行外部的console.log-->同步優先
再來看:同步代碼應該是順序執行,爲何先輸出的是」5「呢?
緣由:for循環是先執行,可是setTimeout的回調函數是不能接收到參數的(回調墊底),等for循環執行完,就會執行外部的console.log了,
因此先打印的會是外部console-->5
繼續:外部console執行完以後爲何會是輸出了5個」i:5「呢?
這裏就涉及到JavaScript的執行棧和消息隊列的概念了,
概念的詳細解釋能夠看下阮老師的JavaScript運行機制詳解:再談Event Loop-阮一峯的網路日誌,或者看併發模型與Event Loop。
我拿這個例子作一下講解,JavaScript單線程如何處理回調呢?JS同步的代碼是在堆棧中順序執行的,而setTimeout回調會先被放到消息隊列,
for循環每執行一次就會放一個setTimeout到消息隊列排隊等候,同步代碼執行完了,再去順序執行消息隊列上的回調方法。
這個例子中,也就是說先執行for循環,按順序放置了5個setTimeout回調到消息隊列,而後for循環結束後,再執行其餘的同步代碼也就是外部的console
,至此,堆棧中已經沒有同步的代碼了,就去消息隊列中訊息好,發現了5個setTimeout(也是根據以前放置的順序而執行的)
到這裏:已經知道了爲何setTimeout是最後執行的了
那麼:爲何是5個5呢?
JavaScript在把setTimeout放到消息隊列的過程當中,循環的i是不會及時保存進去的,至關於你謝了一個異步方法,可是ajax的結果都還沒能返回,只能等到返回以後才能傳參到異步函數中,也就是同步代碼都還沒執行完,i是不會被傳入到回調函數的。
在這裏,由於i是用var定義的,因此是全局變量(由於此處沒有其餘的函數,若是有其餘的函數,那i就是此函數內部變量),當for循環執行完畢,i值爲5,從外部的console也可看出,那麼當同步函數執行完畢,回調接收到的i也就是5了(不少人都會覺得setTimeout裏面的i會是for循環過程當中的i值,這種理解是不正確的)
例2:咱們在例1中加上一行代碼。
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
牢記口訣:同步=>異步=>回調(強化記憶)
這個例子的補充,能夠很清楚的看到先執行for循環,循環裏面的console是同步的,因此先輸出,結束後再執行外部的console,最後再執行setTimeout回調函數!
是否是so easy?
那麼面試官若是再問,如何解決這個問題?
很簡單,固然是ES6中的最新特性!let!!!
例3:
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
咦~爲何外部的console.log(i)會報錯呢?
你這個口訣是否是哪裏不對勁呢?
let是ES6的語法(ECMAScript 6,JavaScript最新規範,主流瀏覽器已基本支持該規範,並持續向該規範靠攏,其中,IE比較特殊想必你們都知道的!
在PC端開發的時候,要注意IE9如下的兼容,移動端開發時,能夠比較放心了!
目前實際項目中,安全的作法是運用babel工具將ES6解釋爲ES5)
ES5中的變量做用域是函數,而let語法的做用域是當前塊,這裏就是for循環體了。
咱們來分析一下,用了let做爲變量i的定義以後,做用與代碼塊中,此處也就是指for循環,for循環每執行一次,都會先給循環內的setTimeout傳參數i,每次傳入的參數依次是0,1,2,3,4,每接受一次參數而後循環內的setTimeout被放到消息隊列(帶入了傳入的參數i,與以前var定義的i是不一樣的),for循環執行完畢,i不在做用在當前塊以外的代碼中,因此外部的同步代碼console輸出的i爲定義!當同步代碼執行完畢,再依次取出消息隊列中帶有不一樣參數的回調函數,全部輸出的結果是如上所示!
在這裏let本質上就是造成了一個閉包。也就是下面例4這種寫法同樣的意思,若是面試官說用下面例4的方式,你能夠正兒八經的告訴他:這就是一個意思!
這也就是爲何有人說let是語法糖!
例:4:
var loop = function(_i){
setTimeout(function(){
console.log('2:',_i);
},1000);
}
for(var _i=0; i<5; _i++){
loop(i)
}
console.log(_i);
//輸出
5
2:0
2:1
2:2
2:3
2:4
或許這或讓面試官聯想到閉包問題,什麼是閉包呢?耐心往下看,後面講。
回到ES5,你是否是就發現適合個人口訣了?同步優先=>異步靠邊=>回調墊底!
而用let的時候。你看不懂?你須要真正的瞭解ES6的語法原理!
注意!
閉包概念:當內部函數以某一種方式被任何一個外部函數做用域訪問時,一個閉包就產生了!
也就是說loop(_1)是外部函數,setTimeout是內部函數,當setTimeout被loop的變量訪問的時候,就造成了一個閉包!
例5:
function test(){
var a=10;
var b = function(){
console.log(a);
}
b();
}
test();//輸出10
口訣繼續:同步優先=>異步靠邊=>回調墊底
先執行函數test,而後JS進入test函數內部,定義了一個變量,而後執行函數b,進入函數b內部,打印a,這裏都是同步代碼,那麼這裏怎麼解釋閉包?
解釋:函數test是外部函數,函數b是內部函數,當函數b被外部函數test的變量訪問的時候,就造成了閉包。
迴歸正題!
上面主要講了同步、異步、回調的執行順序問題,接着我就舉一個簡單的同步、異步、回調的例子
例6:
let a=new Promise(
function(){
console.log(1);
setTimeout(()=>consoel.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);
一眼看不出名堂,不過不慌!
先讀口訣:同步優先=>異步靠邊=>回調墊底
一、看同步代碼:a變量是一個Promise,咱們知道Promise是異步的,是指他的then()和catch()方法,Promise自己仍是同步的,因此這裏先執行a變量內部的Promise同步代碼(同步優先)
二、Promise內部有4個console,第二個是一個setTimeout回調(回調墊底)。因此這裏先輸出1,3,4,回調的方法固然是丟到消息隊列中排隊等着。
三、接着執行resolve(true),進入then(),then是異步,下面還有同步代碼沒執行,因此也被丟到消息隊列中排隊等待(異步靠邊)
四、b變量也是一個Promise,和a同樣,先執行內部的同步代碼,輸出5,setTimeout滾如消息隊列排隊等待。
五、最下面同步輸出7
六、同步代碼執行完了,Javascript就跑到消息隊列呼叫異步的代碼:這裏的異步只有then,輸出8
七、異步也執行完了,接着就是去消息隊列中依次找到回調了:這裏2個回調在排隊,setTimeout時間參數都是0,不作任何影響,只是跟他們在消息隊列中的排隊順序有關,因此先輸出a裏面的2,最後輸出b裏面的回調6
八、最終輸出結果是:一、三、四、五、七、八、二、6
PS:若是想變得有趣一點的話,咱們能夠稍微作一點點修改,把a裏面Promise的setTimeout的時間蠶食0改爲2,也就是2ms後執行,爲何不是1ms,1ms的話,瀏覽器都尚未反應過來呢。改爲>=2的數字才能看到2個setTimeout的輸出順序發生了變化。因此回調函數正常狀況下是在消息隊列中順序執行的,可是使用setTimeout的時候,還須要注意時間大小也會改變它的順序(感受上是改變了順序,其實先讀檢索的消息隊列上的回調仍是a,不過由於時間參數的緣由,被滯後2ms執行了)。
口訣不必定是萬能的,不過做爲一種輔助,更重要的仍是要理解JavaScript的運行機制,才能對代碼的執行順序有清晰的路線。
還有async/await等其餘異步的方案,不論是那種異步,基本都試用於這個口訣,對於新手來講,能夠快速讀懂面試官出的js筆試題目,作出快速準確而且也是面試官但願獲得的答案,之後不再用怕作到相似的筆試題目啦!
PS:特殊狀況下不適應口訣也很正常!JavaScript博大精深,不是一句話就能歸納出來的,隨着ES6的推廣與開拓,JavaScript必定會有更爲快速的發展!可是萬變不離其宗,掌握JavaScript的底層機制始終異常重要!
最後:再念一次口訣!同步優先=>異步靠邊=>回調墊底!
(原文自前端教程=>hyy1115)