同步、异步、回调?傻傻分不清楚。html
你们注意了,教你们一道口诀:面试
同步优先、异步靠边、回调垫底(读起来不顺)ajax
用公式表达就是:浏览器
同步 => 异步 => 回调网络
这口诀有什么用呢?用来对付面试的。闭包
有一道经典的面试题:并发
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。(同步优先)async
二、for循环里面有一个setTimeout回调,他是垫底的存在,只能最后执行。(回调垫底)函数
那么,为何咱们最早输出的是5呢?
很是好理解,for循环先执行,可是不会给setTimeout传参(回调垫底),等for循环执行完,就会给setTimeout传参,而外部的console打印出5是由于for循环执行完成了。
知乎有大神讲解过 80% 应聘者都不及格的 JS 面试题 ,就是以这个例子为开头的。可是没有说为何setTimeout是输出5个5。
这里涉及到JavaScript执行栈和消息队列的概念,概念的详细解释能够看阮老师的 JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志,或者看 并发模型与Event Loop
《图片来自于MDN官方》
我拿这个例子作一下讲解,JavaScript单线程如何处理回调呢?JavaScript同步的代码是在堆栈中顺序执行的,而setTimeout回调会先放到消息队列,for循环每执行一次,就会放一个setTimeout到消息队列排队等候,当同步的代码执行完了,再去调用消息队列的回调方法。
在这个经典例子中,也就是说,先执行for循环,按顺序放了5个setTimeout回调到消息队列,而后for循环结束,下面还有一个同步的console,执行完console以后,堆栈中已经没有同步的代码了,就去消息队列找,发现找到了5个setTimeout,注意setTimeout是有顺序的。
那么,setTimeout既然在最后才执行,那么他输出的i又是什么呢?答案就是5。。有人说不是废话吗?
如今告诉你们为何setTimeout全都是5,JavaScript在把setTimeout放到消息队列的过程当中,循环的i是不会及时保存进去的,至关于你写了一个异步的方法,可是ajax的结果还没返回,只能等到返回以后才能传参到异步函数中。
在这里也是同样,for循环结束以后,由于i是用var定义的,因此var是全局变量(这里没有函数,若是有就是函数内部的变量),这个时候的i是5,从外部的console输出结果就能够知道。那么当执行setTimeout的时候,因为全局变量的i已是5了,因此传入setTimeout中的每一个参数都是5。不少人都会觉得setTimeout里面的i是for循环过程当中的i,这种理解是不对的。
===========================================分割线=========================================
看了上面的解释,你是否是有点头晕,没事,继续深刻讲解。
咱们给第一个例子加一行代码。
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循环,for循环里面的console是同步的,因此先输出,for循环结束后,执行外部的console输出5,最后再执行setTimeout回调 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中的变量做用域是函数,而let语法的做用域是当前块,在这里就是for循环体。在这里,let本质上就是造成了一个闭包。也就是下面这种写法同样的意思。若是面试官对你说用下面的这种方式,还有let的方式,你能够严肃的告诉他:这就是一个意思!这也就是为何有人说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的形式,你是否是发现就适合我说的口诀了?而用let的时候,你发现看不懂?那是由于你没有真正了解ES6的语法原理。
咱们来分析一下,用了let做为变量i的定义以后,for循环每执行一次,都会先给setTimeout传参,准确的说是给loop传参,loop造成了一个闭包,这样就执行了5个loop,每一个loop传的参数分别是0,1,2,3,4,而后loop里面的setTimeout会进入消息队列排队等候。当外部的console执行完毕,由于for循环里的i变成了一个新的变量 _i ,因此在外部的console.log(i)是不存在的。
如今能够解释闭包的概念了:当内部函数以某一种方式被任何一个外部函数做用域访问时,一个闭包就产生了。
我知道你又要我解释这句话了,loop(_i)是外部函数,setTimeout是内部函数,当setTimeout被loop的变量访问的时候,就造成了一个闭包。(别说你又晕了?)
随便举个新的例子。
function t() { var a = 10; var b = function() { console.log(a); } b(); } t(); //输出 10
跟我一块儿念口诀:同步 => 异步 => 回调 (强化记忆)
先执行函数t,而后js就进入了t内部,定义了一个变量,而后执行函数b,进入b内部,而后打印a,这里都是同步的代码,没什么异议,那么这里怎么解释闭包:函数t是外部函数,函数b是内部函数,当函数b被函数t的变量访问的时候,就造成了闭包。
========================================分割线==============================================
上面主要讲了同步和回调执行顺序的问题,接着我就举一个包含同步、异步、回调的例子。
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)
看到这个例子,千万不要惧怕?,先读一遍口诀:同步 => 异步 => 回调 (强化记忆)
一、看同步代码:a变量是一个Promise,咱们知道Promise是异步的,是指他的then()和catch()方法,Promise自己仍是同步的,因此这里先执行a变量内部的Promise同步代码。(同步优先)
console.log(1) setTimeout(() => console.log(2), 0) //回调 console.log(3) console.log(4)
二、Promise内部有4个console,第二个是一个setTimeout回调(回调垫底)。因此这里先输出1,3,4回调的方法丢到消息队列中排队等着。
三、接着执行resolve(true),进入then(),then是异步,下面还有同步没执行完呢,因此then也滚去消息队列排队等候。(真可怜)(异步靠边)
四、b变量也是一个Promise,和a同样,执行内部的同步代码,输出5,setTimeout滚去消息队列排队等候。
五、最下面同步输出7。
六、同步的代码执行完了,JavaScript就跑去消息队列呼叫异步的代码:异步,出来执行了。这里只有一个异步then,因此输出8。
七、异步也over,轮到回调的孩子们:回调,出来执行了。这里有2个回调在排队,他们的时间都设置为0,因此不受时间影响,只跟排队前后顺序有关。则先输出a里面的回调2,最后输出b里面的回调6。
八、最终输出结果就是:一、三、四、五、七、八、二、6。
咱们还能够稍微作一点修改,把a里面Promise的 setTimeout(() => console.log(2), 0)改为 setTimeout(() => console.log(2), 2),对,时间改为了2ms,为何不改为1试试呢?1ms的话,浏览器都尚未反应过来呢。你改为大于或等于2的数字就能看到2个setTimeout的输出顺序发生了变化。因此回调函数正常状况下是在消息队列顺序执行的,可是使用setTimeout的时候,还须要注意时间的大小也会改变它的顺序。
====================================分割线==================================================
口诀不必定是万能的,只能做为一个辅助,更重要的仍是要理解JavaScript的运行机制,才能对代码执行顺序有清晰的路线。
还有async/await等其余异步的方案,不论是哪一种异步,基本都适用这个口诀,对于新手来讲,能够快速读懂面试官出的js笔试题目。之后不再用惧怕作笔试题啦。
特殊状况下不适应口诀的也很正常,JavaScript博大精深,不是一句话就能归纳出来的。
最后,在跟着我念一遍口诀:同步 => 异步 => 回调
若是文章对你有帮助,请点击一下推荐。