js的轮询机制

1,159 阅读7分钟

在了解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状态结束的时候,就会立即放进异步队列中去了。

解题过程:

  1. console.lo(1)是同步任务,执行打印出 1 。

  2. setTimout()是异步宏任务,但是,定时器需要等待一定的时间才会执行,所以,这个任务并不会立即放进队列中。

  3. setTimout()是异步宏任务,可以立即执行,将它的回调加入队列中moc[setTimout1CB]

  4. 变量p是一个promise对象,new promise是同步任务,所以,console.lo(4)是同步任务,执行打印出 4,console.lo(5)是同步任务,执行打印出 5

  5. p.then是异步微任务,p的状态已经变成了成功,此时可以将它的回调加入队列mic[thenCB]

  6. console.lo(8)是同步任务,执行打印出 8 。

  7. 同步任务全部执行完毕

  8. 查看微任务mic[thenCB],按照队列先进先出的原则,会先执行第一个,执行回调,then函数接收两个参数,第一个参数是调用成功的回调,第二个参数是失败的回调,从上可以看到,调用的是成功回调,没有失败,不会执行7。所以,执行resolve的回调函数,打印出 6。

  9. 微任务没有了,执行完毕

  10. 查看宏任务moc[setTimout1CB],宏任务有一个,执行回调,打印出 3。

  11. 继续往下找第二个宏任务,宏任务没有了。

  12. 1000ms后,定时器时间到达,把setTimeout回调函数放到宏任务队列中去moc[setTimout2CB]

  13. 查看宏任务队列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'

解题过程:

  1. 函数不是任务,跳过。

  2. console.log("script start")同步任务,打印出"script start"

  3. setTimout()是异步宏任务,等待0s,可以立即执行,将它的回调加入队列中moc[setTimout1CB]

  4. 执行异步函数async1(),在await之前的任务,是同步任务,打印出"async1 start"

  5. 执行await后面的函数async2(),此时的aysnc2也是一个异步函数,返回一个promise对象,由于里面没有await,所以,里面的代码也是同步任务,此时打印出"async2"

  6. 异步函数async1()中有await关键字,await后面的代码,会被阻塞,要等async外面主进程的代码执行完之后才会执行,相当于promise的then函数。我个人可以这么理解:由于aysnc2里面没有await,状态立即返回,相当于直接resolve,所以可以把await之后的任务当做一个then回调函数,放入微任务队列中:mic[async1CB]

  7. 继续往下,new Promise是同步任务,打印"promise1"

  8. then函数是异步微任务,状态已经变成了成功,此时可以将它的回调加入队列mic[async1CB,thenCB]

  9. console.log( 'script end’ )是同步任务,打印出"script end"

  10. 同步任务全部执行完毕

  11. 查看微任务队列mic[async1CB,thenCB],执行回调async1CB,也就是async1的await后面的代码,打印出"async1 end"

  12. 查看微任务队列mic[thenCB],执行回调thenCB,也就是new promise.then后面的代码,打印出"promise2"

  13. 微任务执行完毕

  14. 查看宏任务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')