同步、异步回调?傻傻分不清楚。
大家注意了,教大家一道口诀:
同步优先、异步靠边、回调垫底!
公式表达:同步=>异步=>回调
这口诀的用处是什么呢?至少应付面试,完全够用!
例1:(经典面试题)
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
想必大家都遇到过这样的题目吧,那么为什么会是这样的输出结果呢?
来,跟着我念:”同步优先,异步靠边,回调垫底!“
首先: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);
一眼看不出名堂,不过不慌!
先读口诀:同步优先=>异步靠边=>回调垫底
1、看同步代码:a变量是一个Promise,我们知道Promise是异步的,是指他的then()和catch()方法,Promise本身还是同步的,所以这里先执行a变量内部的Promise同步代码(同步优先)
2、Promise内部有4个console,第二个是一个setTimeout回调(回调垫底)。所以这里先输出1,3,4,回调的方法当然是丢到消息队列中排队等着。
3、接着执行resolve(true),进入then(),then是异步,下面还有同步代码没执行,所以也被丢到消息队列中排队等待(异步靠边)
4、b变量也是一个Promise,和a一样,先执行内部的同步代码,输出5,setTimeout滚如消息队列排队等待。
5、最下面同步输出7
6、同步代码执行完了,Javascript就跑到消息队列呼叫异步的代码:这里的异步只有then,输出8
7、异步也执行完了,接着就是去消息队列中依次找到回调了:这里2个回调在排队,setTimeout时间参数都是0,不做任何影响,只是跟他们在消息队列中的排队顺序有关,所以先输出a里面的2,最后输出b里面的回调6
8、最终输出结果是:1、3、4、5、7、8、2、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)