十分钟带你了解javascript里的面试“常客”——Event-Loop,async-await
事件循环的执行顺序经常是为很多人所困扰的一件事;无论是同步,异步还是说宏任务和微任务在执行的时候都是有着各自的规则,他们遵循着规则所以我们才需要去了解这些规则,让这些规则成为我们手中的“利器”,而不是成为困扰我们的问题,不过在讲事件循环之前,咱们首先先浅浅地谈一下两个知识点:同步异步以及promise方法。
同步与异步
讲到执行,那我们就要先讲到javascript里面经常提到的同步与异步啦。那么什么是同步?什么又是异步呢?
同步与异步的定义
我们传统意义上的同步是指两个或两个以上的事物随时间变化的量在变化过程中保持一定的相对关系,那么这些事物就算是同步,但是显然,我们在javascript里面所讲的同步就不是这个概念了,javascript里面,一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去,这样就称为同步,同步代码会按照一定的顺序去执行;
异步就是进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,异步在一定程度上能够大大提高代码执行的效率,但是这样讲应该还是比较晦涩难懂,那不妨往下看看异步是如何实现的。
如何实现异步?
实现异步的方法很多,常用的有以下几种:
1、使用setTimeout方法;
2、使用setImmediate 方法;
3、使用requestAnimationFrame方法;
接下来我就简单带着大家用setTimeout方法去实现异步:
function a(){
console.log('aaa');
}
function b(){
setTimeout(() => {
console.log('bbb');
},1000);
}
function c(){
console.log('ccc');
}
a();
b();
c();
正常来说,当a()函数被调用结束后,应该是b()函数被调用,而b函数里应该是先执行完定时器setTimeout函数里面的console.log('bbb')再去调用最后一个函数c();然而,让我们去看看控制台输出;
可是可以看到我们console.log打印出来的顺序是aaa->ccc->bbb;这说明什么?是b()函数在c()函数后面执行吗?错!不是,是当执行到setTimeout定时器函数时,定时器延时1s才开始执行里面的内容,所以浏览器默认往下继续执行,实际上定时器确实是已经执行过了,只不过延时了,那么c()函数就相当于变相的提前了,这就是异步;
如何解决异步?
如果我们就要c函数在b函数后面执行,那么有去除异步的办法吗?答案是有的;在E6之前我们是通过回调的方式去调用函数,来控制函数执行的先后顺序;
function a(){
console.log('aaa');
}
function b(cb){
setTimeout(() => {
console.log('bbb');
cb();
},1000);
}
function c(){
console.log('ccc');
}
a();
b(c); //我们不直接调用c函数,我们通过给b函数传参,将它放在b()函数里面调用这种回调的方式去除异步的效果;
在此,我们在原代码的基础上去做修改,把c函数放在b函数内进行调用,从而回调,让c函数达到在b函数的后面执行的效果。
回调地狱
但是值得注意的是,回调函数嵌套过多,代码逻辑过于复杂就容易造成回调地狱;那么什么是回调地狱呢?假如说我们存在很多个函数,而c函数放在b函数里执行,d函数在c函数里面执行,e函数在d函数里面执行,以此类推,一直这样回调,那么回调函数就会形成一个回调链,而执行上下文的内存无法得到释放,一旦到了一个极限,就会形成回调地狱。
所以我不推荐使用回调去达到去除异步的效果,接下来我会带大家去看另外一个方法:promise方法。
Promise方法
promise方法是ES6之后推出的一个新方法,它是异步编程的一种解决方案,是一个对象,可以获取异步操作的消息,大大改善了异步编程的困难,避免了回调地狱,比传统的解决方案回调函数和事件更合理和更强大。
假设咱们写两个函数,分别代表着一个人相亲和结婚;其中相亲花了两天,而结婚花了一天:
function xq(){
setTimeout(()=>{
console.log('周公子相亲了!');
},2000)
}
function marry(){
setTimeout(()=>{
console.log('周公子结婚了!');
},1000)
}
xq();
marry();
可这么一段函数执行下来却不太如我们所愿,变成了先结婚再相亲,而原因则是两个函数发生了异步;
可这样先结婚还去相亲可不就是渣男行为嘛,咱们这里就用上新方法promise方法去解决异步问题,让它按照我们想要先相亲再结婚的方向去执行。
function xq(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log('周公子相亲了!');
resolve('ok')
},2000)
})
}
function marry(){
setTimeout(()=>{
console.log('周公子结婚了!');
},1000)
}
xq().then(marry)
promise函数是一个对象,它自带两个参数,一个resolve,一个reject;当函数读取成功了,就会返回resolve,失败了就会返回reject;这里咱们只是用到了resolve,然后在调用后面.then()就可以很好的去解决异步这个问题了
then方法的返回结果是新的Promise实例
,对象状态由回调函数的执行结果决定。then方法后面还可以再调用另一个then方法,形成链条。采用链式的then
,可以指定一组按照次序调用
的回调函数。
Promise函数的其它方法
Promise.prototype.catch();
Promise.prototype.finally();
Promise.resolve();
Promise.reject();
Promise.all();
Promise.race();
......
(因为这里我们主要讲的不是Promise方法,咱们就不过多讲promise的其它子方法了,就这样一笔带过啦,不过大家感兴趣可以去看看这位大佬的这篇讲解Promise的文章——javascript中的糖衣语法--Promise对象,写得十分不错且细腻哦,十分推荐!)。
那么接下来咱们才算是讲到正题了——Event-Loop;
Event—Loop
在谈事件循环之前,我们先来看这么一段代码吧
console.log('start');
setTimeout(function(){
console.log('setTimeout');
},0)
new Promise(resolve =>{
console.log('Promise');
resolve();
})
.then(function(){
console.log('promise1');
})
.then(function(){
console.log('promise2');
})
console.log('end');
这里你们能知道这段代码的打印顺序应该是怎么样的吗?
这里我先把它的执行顺序告诉大家,至于为什么,当大家看完后面的内容再回来看相信就能知道了;
start -> Promise -> end -> promise1 -> promise2 -> setTimeout
首先我们要了解javascript是一个单线程的脚本语言,也就是说我们在执行代码的过程中不会出现同时进行两个进程(执行两段代码块)。
可大家还是会好奇,可这也跟事件循环没有关系啊?别急,在了解事件循环之前,我们还得知道一些小知识;
什么是进程?
狭义上,就是正在运行的程序的实例。广义上,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。描述的是CPU在运行指令及加载和保存上下文所需要的时间。
什么是线程?
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位。指运行中的程序的调度单位。
什么是执行栈?
V8(谷歌浏览器引擎)内部维护出来的一个用来存放函数的执行上下文环境的一个栈结构,遵循着先进后出的规则。
什么是宏任务?
(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
宏任务队列包括哪些?
· script
· setTimeOut
· setInterval
· setImmediate
(node环境下是,而浏览器环境下不是)
· I/O
· UI-rendering
· requestAnimationFrame
(在浏览器环境是,而node环境不是)
什么是微任务?
微任务可以理解是在当前 task 执行结束后立即执行的任务。
微任务队列包括哪些?
· process.nextTick
· promise.then
· MutationObserver
了解完这些小知识之后,我们就可以开始正式开始我们的内容——Event-Loop了。
什么是Event-Loop?
JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。 也就是等待宿主环境分配宏观任务,反复等待 - 执行即为Event-Loop(事件循环)。
事件循环的执行顺序
第一步:找出同步代码,优先执行,这属于宏任务。
第二步:当执行完所有的同步代码后,执行栈为空,检查是否有异步代码要执行。(注意只是检查,并非此刻检查出来就执行,而是看它是第几层次的异步代码在第几次循环执行,引擎在找到异步代码的时候会将其挂起,即装起来放在一边)
第三步:执行微任务。
第四步:执行完微任务后,有必要的情况下会渲染页面。
第五步:开启下一轮Event-Loop,执行宏任务中的代码。(这就算是下次事件循环的开始第一步,上一次的异步,在此刻也将变为这一次执行的同步)
讲完这些我们再回头看上面的代码看看,
第一步找出同步代码:
console.log('start');
new Promise(resolve =>{
console.log('Promise');
resolve();
})
console.log('end');
将其执行完;于是优先打印出了start;Promise;end;
第二步就在执行完所有同步代码后,检查是否有异步代码,
我们很快就能找到定时器所在异步代码(即便定时器定时为0,那也是属于异步代码)
setTimeout(function(){
console.log('setTimeout');
},0)
将其挂起,放到一边先不要管。
第三步则是找到微任务并将其执行:
.then(function(){
console.log('promise1');
})
.then(function(){
console.log('promise2');
})
那么继上一次打印结果之后再次打印出了:promise1;promise2;
第四步,执行完微任务后,有必要将会渲染页面,但是这里我们是纯原生JS,
并没有需要渲染的DOM结构,可以跳过这个环节,同时,这一轮Event-Loop结束。
第五步,开启下一轮Event-Loop,我们刚才挂起的异步代码已然成为了这一轮的第一个步骤中的同步代码
那么它将会成为这一轮Event-Loop的第一步首先执行。
setTimeout(function(){
console.log('setTimeout');
},0)
那么我们将会继续打印出setTimeout;
本来我们在执行完这个操作应该继续循环Event-Loop操作,但是我们知道所有代码都已经执行完,后续也没有可执行代码,也就不用执行下去了
start -> Promise -> end -> promise1 -> promise2 -> setTimeout这样的执行顺序就是这样打印出来的
async-await
async-await是继ES6的promise之后在ES7里面新出的一个对象方法,它的是基于promise之上开发出来的方法,往深处追究也可以说它就是promise方法的封装使用,其使用方法比promise要更加简便,优雅。
async-await方法的使用
这是一段简单的异步代码:
function gg(){
setTimeout(()=>{
console.log('开始');
},1000)
}
function foo(){
console.log('结束');
}
gg();
foo();
它的打印结果为:
如果在之前,我们想要处理这样的异步任务可能需要用到promise方法做如下操作:
function gg(){
return new Promise(resolve=>{
setTimeout(()=>{
console.log('开始');
resolve('ok');
},1000)
})
}
function foo(){
console.log('结束');
}
gg().then(foo)
打印出来的结果也就能如我们所想:
但是,如果我们的代码量很大的话,那么再用Promise方法显然是不够优雅的,它需要在多个函数里面返回Promise,且还要多个.then()连接,观赏性和代码量都是一个令人困扰的问题。
那么,我们的主角——async-await也就理所当然的该登场了,它的使用方法很简单,如下:
function gg(){
return new Promise(resolve=>{
setTimeout(()=>{
console.log('开始');
resolve('ok');
},1000)
})
}
async function foo(){
await gg();
console.log('结束');
}
foo();
用法很简单,我们来仔细讲讲其中原理
async
async是一个加在函数前的修饰符,被async定义的函数会默认返回一个Promise对象resolve的值。
async function test(){
return 'hello async'
}
const result = test();
console.log(result);
通过打印结果也是验证了这一点,被async定义的函数会默认返回一个Promise对象值。
因此对async函数可以直接then,返回值就是then方法传入的函数,即:
async function test(){
console.log('开始');
}
test().then(val=>{
console.log('end');
})
await
await 也是一个修饰符,只能放在async定义的函数内,字面上可以理解为等待。
await 修饰的如果是Promise对象,则可以获取Promise中返回的内容(resolve或reject的参数),且取到值后语句才会往下执行,即在await语句得到Promise返回值之前它会去堵塞后续代码的执行(能够解决异步带来的影响);
await 修饰的如果不是Promise对象,则把这个非promise的东西当做await表达式的结果,即当做await不存在即可(不能解决异步带来的影响)。
这是当await修饰的是Promise对象的情况:
function gg(){
return new Promise(resolve=>{
setTimeout(()=>{
console.log('开始');
resolve('ok');
},1000)
})
}
async function foo(){
await gg();
console.log('结束');
}
foo();
这是当await修饰非Promise对象的情况:
function gg(){
setTimeout(()=>{
console.log('开始');
},1000)
}
async function foo(){
await gg();
console.log('结束');
}
foo();
可以看到,当gg()返回的是Promise对象的时候,那么异步带来的影响就被消除了。
当gg()返回的是非Promise对象的时候,那么跟await不存在时打印的顺序也是一样的,没办法消除异步带来的影响。
async-await如何解读成为Promise形式?
async的解读
要想了解如何去解读async,我们首先要掌握它的定义——被async定义的函数会默认返回一个Promise对象resolve的值。
如果有这么一个函数:
async function a(){
console.log('123');
}
那么我们可以通过async的定义去将它解读成另一番模样;
在有关于async的定义里有说过,被async定义的函数会默认返回一个Promise对象resolve的值,那么这个默认返回又是怎么样的呢?
//这里我们将async解读;
function a(){
return Promise.resolve().then(()=>{ //async会默认在它的函数体内生成一个隐式的
console.log('123'); //return Promise.resolve().then(()=>{})
}) //而这个promise会将函数体的代码包裹在返回值里面执行
}
await的解读
相对于async来说,await要更为复杂,注意的事项也要更加繁杂。
第一:await不能够单独出现,其函数前面一定要有async
第二:当await存在于async所在的函数体内,await会干两件事:
1.将写在await后面的代码放到async创建的那个promise里面去执行
2.将写在await下面的代码放到前一个创建的promise的.then里面去执行
第三:await返回的也是promise对象,它只是把await下面的代码放到await返回的promise的.then里面去执行
用这么几段文字来说还是比较难懂的,咱们废话不多说,直接上代码带大家解读一遍大家一定会了解的
function getContent(){
console.log('getContent');
return 1;
}
async function getAsyncContent(){
console.log('getAsyncContent');
return 1;
}
function getPromise(){
return new Promise((resolve,reject)=>{
console.log('getPromise');
resolve(1);
})
}
async function text(){
let a = 2;
let c = 1;
await getContent();
let d = 3;
await getPromise();
let e = 4;
await getAsyncContent();
return 2;
}
test();
接下来,咱们来一起解读以下代码
咱们先将getContent()、getAsyncContent()、getPromise()三个函数体看一下有没有需要解读的
//async
function getAsyncContent(){
return Promise.then(resolve=>{
console.log('getAsyncContent'); //这里我们只需要将getAsyncContent()里的async依照上面讲的规则解读成为Promise的形式,另外两个函数都是普通函数,无需解读
return 1;
})
}
当调用了text()函数之后,重点await来了,同样的,咱们在解读await之前,也需要先把async解读一下;
//async
function text(){
//将async解读后,为了方便大家了解后面await的解读,我们这里可以看做暂时多了一个Promise的返回值在这
return Promise.then(resolve=>{
})
let a = 2;
let c = 1;
await getContent();
let d = 3;
await getPromise();
let e = 4;
await getAsyncContent();
return 2;
}
之后,我们会将let a = 2;以及let c =1;放到之前async所形成的Promise里面,因为它们在第一个await之前
return Promise.then(resolve=>{
let a = 2;
let c = 1;
})
然后就到了咱们的第一个await,第一个await后面跟着一个getContent();所以我们会将写在await后面的getContent放到async创建的那个promise里面去执行,并且成为一个返回值。
return Promise.then(resolve=>{
let a = 2;
let c = 1;
return getContent()
})
再之后有一个let d = 3;则相当于在第一个Promise.then()的后面再加一个.then(),将其放在里面:
return Promise.then(resolve=>{
let a = 2;
let c = 1;
return getContent()
}).then(resolve=>{
let d = 3;
})
而后,出现第二个await,则我们将其后面的getPromise();放到前一个.then()之中
return Promise.then(resolve=>{
let a = 2;
let c = 1;
return getContent()
}).then(resolve=>{
let d = 3;
return getPromise();
})
剩下的以此类推最后test()里面的代码就成了以下的样子:
function text(){
return Promise.then(resolve=>{
let a = 2;
let c = 1;
return getContent()
}).then(resolve=>{
let d = 3;
return getPromise();
}).then(resolve=>{
let e = 4;
return getAsyncContent();
}).then()(resolve=>{
return 2;
})
}
那么,async-await难吗?难!可相对于Promise方法来说,它更加贴切这个它为我们所提供的作用,即解决异步错乱。
各位看官都看到这啦,不妨给小尘点个赞叭^_^
转载自:https://juejin.cn/post/7122064402068553764