在了解js的轮询之前,首先,我们来做一个题目吧:
console.log(1)
setTimeout(function(){
console.log(2)
},1000)
setTimeout(function(){
console.log(3)
},0)
var p = new Promise(function(resolve,reject){
console.log(4)
resolve()
console.log(5)
})
p.then(function(){
console.log(6)
},function(){
console.log(7)
})
console.log(8)
上述代码打印的顺序是什么?
// 1 4 5 8 6 3 2
为什么?我们来了解一下js的执行机制。
js执行机制
我们都知道,js是单线程的,通俗一点,就是一条时间线,在这条线上的每个时间点,只能做一个任务,我们可以把这条线叫做主线程。
异步和轮询
单线程就意味着,后面的任务必须得等待前面的任务完成后,才能执行,这样就会引起一个严重的问题,阻塞后面任务的执行。
如果前一个任务一直不完成,那后面的任务咋办呢?
然后就有了异步,为了不阻塞主线程,我们把需要长时间等待回应的任务作为异步事件,把异步事件放到一个队列里,主线程会先执行那些可以直接操作的任务,然后会不停的去访问队列,看看异步任务有没有完成,如果完成了,就执行异步任务的回调,如果还没有完成,那就继续如此循环。
总结:
-
所有任务都在主线程上执行,形成一个执行栈。
-
如果执行栈中的所有同步任务执行完毕,js就会读取消息队列中的异步任务,如果有可以执行的任务就把他放入执行栈中并开始执行。
-
主线程不断重复上面的第三步,这样的一个循环称为事件循环。
这就是js的轮询。
宏任务和微任务
异步队列里面,有两种任务。
宏任务:setTimiout,setInterval,setImmediate
微任务:process.nextTick,promise,mutationObserver(html5新特性)
js宏任务和微任务轮询:
-
所有任务都在主线程上执行,形成一个执行栈。
-
主线查看是否有异步任务,如果是微任务,就把任务放到微任务的消息队列里,如果是宏任务,就把宏任务放到宏任务的消息队列里。
-
同步操作执行完毕,
-
执行微任务,执行宏任务
-
主线程重复上面的第四步。
解题
所以,我们把任务分成三种,同步任务,微任务,宏任务,来分析一下上面的题。
同步任务线程
微任务队列:mic[]
宏任务队列:moc[]
console.log(1)
setTimeout(function(){
console.log(2)
},1000)
setTimeout(function(){
console.log(3)
},0)
var p = new Promise(function(resolve,reject){
console.log(4)
resolve()
console.log(5)
})
p.then(function(){
console.log(6)
},function(){
console.log(7)
})
console.log(8)
这里需要注意几点:
setTimeout
setTimout:setTimeOut并不是直接的把你的回调函数放进上述的异步队列中去,而是在定时器的时间到了之后,把回调函数放到执行异步队列中去。如果此时这个队列已经有很多任务了,那就排在他们的后面。这也就解释了为什么setTimeOut为什么不能精准的执行的问题了。
1. 主进程必须是空闲的状态,如果到时间了,主进程不空闲也不会执行你的回调函数 2. 这个回调函数需要等到插入异步队列时前面的异步函数都执行完了,才会执行
promise
new Promise是同步的任务,会被放到主进程中去立即执行。而.then()函数是异步任务会放到异步队列中去,那什么时候放到异步队列中去呢?当你的promise状态结束的时候,就会立即放进异步队列中去了。
解题过程:
-
console.lo(1)是同步任务,执行打印出 1 。 -
setTimout()是异步宏任务,但是,定时器需要等待一定的时间才会执行,所以,这个任务并不会立即放进队列中。 -
setTimout()是异步宏任务,可以立即执行,将它的回调加入队列中moc[setTimout1CB] -
变量p是一个promise对象,new promise是同步任务,所以,
console.lo(4)是同步任务,执行打印出 4,console.lo(5)是同步任务,执行打印出 5 -
p.then是异步微任务,p的状态已经变成了成功,此时可以将它的回调加入队列
mic[thenCB] -
console.lo(8)是同步任务,执行打印出 8 。 -
同步任务全部执行完毕
-
查看微任务
mic[thenCB],按照队列先进先出的原则,会先执行第一个,执行回调,then函数接收两个参数,第一个参数是调用成功的回调,第二个参数是失败的回调,从上可以看到,调用的是成功回调,没有失败,不会执行7。所以,执行resolve的回调函数,打印出 6。 -
微任务没有了,执行完毕
-
查看宏任务
moc[setTimout1CB],宏任务有一个,执行回调,打印出 3。 -
继续往下找第二个宏任务,宏任务没有了。
-
1000ms后,定时器时间到达,把setTimeout回调函数放到宏任务队列中去
moc[setTimout2CB] -
查看宏任务队列
moc[setTimout2CB],,执行回调,打印 2。
升级难度,加入async/await
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log('script end');
上述代码打印出什么?为什么?
async/await
带async关键字的函数会返回一个promise对象,如果里面没有await,执行起来等同于普通函数。
await关键字要在async关键字函数的内部,等待表达式完成。此时,await会让出线程,阻塞后续的代码,先去执行async外的同步代码。
'script start’
'async1 start’
'async2’
'promise1’
'script end’
'async1 end'
'promise2'
'settimeout'
解题过程:
-
函数不是任务,跳过。
-
console.log("script start")同步任务,打印出"script start" -
setTimout()是异步宏任务,等待0s,可以立即执行,将它的回调加入队列中moc[setTimout1CB] -
执行异步函数
async1(),在await之前的任务,是同步任务,打印出"async1 start" -
执行await后面的函数
async2(),此时的aysnc2也是一个异步函数,返回一个promise对象,由于里面没有await,所以,里面的代码也是同步任务,此时打印出"async2" -
异步函数
async1()中有await关键字,await后面的代码,会被阻塞,要等async外面主进程的代码执行完之后才会执行,相当于promise的then函数。我个人可以这么理解:由于aysnc2里面没有await,状态立即返回,相当于直接resolve,所以可以把await之后的任务当做一个then回调函数,放入微任务队列中:mic[async1CB] -
继续往下,
new Promise是同步任务,打印"promise1" -
then函数是异步微任务,状态已经变成了成功,此时可以将它的回调加入队列
mic[async1CB,thenCB] -
console.log( 'script end’ )是同步任务,打印出"script end" -
同步任务全部执行完毕
-
查看微任务队列
mic[async1CB,thenCB],执行回调async1CB,也就是async1的await后面的代码,打印出"async1 end" -
查看微任务队列
mic[thenCB],执行回调thenCB,也就是new promise.then后面的代码,打印出"promise2" -
微任务执行完毕
-
查看宏任务
moc[setTimout1CB],执行回调,打印出"setTimeout"
如果真正理解了js轮询机制,解题是比较容易滴。
有些文章,会把同步也当做是宏任务,那么,说法就是,setTimeout是下一个宏任务,promise.then是当前宏任务里面的微任务,所以,先执行当前宏任务的微任务,再去执行下一个宏任务。
如有不对的地方,请大家指正。
献上一道题:
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2-1')
setTimeout(()=>{
console.log('async2')
},0)
}
console.log('script start')
async1();
setTimeout(function(){
console.log('setTimeout')
},0)
new Promise(function(resolve){
console.log('promise1')
setTimeout(()=>{
console.log('promise1-setTimeout')
},0)
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')