js:任务队列 与 事件循环

1,638 阅读6分钟

任务队列

我在上一篇的文章中说到,在JavaScript单线程语言的神奇操作之下,我们的同步任务先予以执行,而我们的异步任务则会进入“任务队列”之中,而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。说白了,任务队列就是安排函数执行顺序的一个队列。

那么,在js中粗略的事件执行步骤是什么呢?

1. 首先先执行主线程,其实就是在调用调用栈里面的同步代码,系统会先予以执行

2. 等到主线程将调用栈里面的全部同步任务执行完毕后,事件循环此时开始执行

3. 主线程发现调用栈为空后,会进行事件循环来观察要执行的事件回调。这个时候会进入任务队列当中,而事件循环检测到任务队列当中有事件,就进行我们最上面说到的操作,然后就取出相关事件任务放入调用栈中,由主线程执行。

那么我们就要来说说这个所谓的事件循环,那么这是个什么东西呢?

事件循环

我们都知道,当js代码在执行时,也就是往调用栈中放进去函数,然后再执行,但是,如果遇到异步函数,异步函数就会被挂起,进入所谓的任务队列中,并在调用栈为空的时候拿出来执行 流程如下:

  step1:主线程读取JS代码,此时为同步环境,形成相应的执行上下文,也就是调用栈;

  step2:  主线程遇到异步任务,将异步任务挂起;

  step3:  将相应的异步任务推入任务队列;

  step4: 主线程之中的任务执行完毕,查询任务队列,如果队列中存在任务,则取出一个任务推入主线程处理;

  step5: 重复执行step2、3、4;称为事件循环。

 具体流程如下图:

未命名文件(7).jpg

    

 接下来我们又要来说说任务队列当中的猫腻了

宏任务与微任务

从任务队列中提取出来执行的函数分两种情况:

  1. 微任务(microtask) :如process.nextTick, promise.then, MutationObserver

  2. 宏任务(macrotask) :如script(最早执行), setTimeout, setInterval, setImmediate, I/O, UI-rendering

event-loop 五部曲:

 1. 首先执行同步代码,这属于宏任务(至少包含script)

 2. 当执行完所有同步代码,执行栈为空,查询是否有异步代码要执行

 3. 执行所有的微任务

 4. 当所有的微任务执行完毕后,有需要的话会渲染页面

 5. 开启下一次的Event-Loop,执行宏任务中的异步代码

async 与 await

那么在此之前我们有需要来聊聊async 与 await。

如果在某个函数前加了async这个关键字,则表示当前这个函数内部可以存在异步,但是不会影响函数本身,async当中,也可以利用关键字,也就是await,而加了await的代码会立即执行,且后面的代码会被阻塞,导致后面的代码去到下一次的微任务队列。

我们看个例子

function getJSON() {

  return new Promise((resolve,reject) =>{

      setTimeout( () => {

        console.log('json');

    },500)

  })

   

}

// async表示当前这个函数内部可以存在异步

async function testASync() {

 await getJSON()//加了await的代码会立即执行,且后面的代码会被阻塞

console.log('数据已经拿到了')

}

testASync()

执行结果

json

数据已经拿到了

代码分析

上述代码当中,我们定义了一个getJSON( )函数,并在其中设置了异步函数,打印‘json’,而在另外一个函数当中,我们利用了async 与 await,并在其中执行getJSON( )函数,此时代码立即执行,阻塞了后一步的同步代码console.log('数据已经拿到了'),那么此时就会先执行getJSON( )函数,打印了‘json’,之后才打印‘数据已经拿到了’。

看完这个,我们来看终极代码

例题

console.log('script start')

    async function async1() {//同步

      await async2() // await会导致后面的代码去到下一次的微任务队列

      console.log('async1 end')//同步 v8正在违反规定,将await后面的代码提上一个循环

    }

    async function async2() {

      console.log('async2 end')

    }

    async1()

    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('script end')

代码分析

我们就严格按照事件五部曲来代替js,执行以下这个代码。

  1. 首先执行同步代码,那么我们从上往下寻找同步代码。我们发现,第一行console.log('script start')就是一个同步代码,所以直接打印出结果script start;之后我们继续执行,发现了 async function async1()这个函数,但是我们却需要卸掉他的伪装,虽然它前面有关键字async,但是有整个关键字存在并不会影响函数本身,所以它还是一个同步函数,这个时候在里面有个await async2(),发现await,代码立即执行,打印出结果async2 end,将console.log('async1 end')挤到下一次的微任务队列。之后又找到了Promise( ),打印出结果Promise,最后才遇到了最后一个同步代码console.log('script end'),直接打印.

  2. 当执行完所有同步代码后,执行栈为空,查询是否有异步代码要执行,这里很明显有异步函数,但是其中又包括了宏任务与微任务,所以直接去第三步

  3. 执行所有的微任务,我们从上到下,所以发现了微任务为两个.then( )函数,所以需要打印结果promise1,``promise2

  4. 当所有的微任务执行完毕后,有需要的话会渲染页面,这里似乎没有。

  5. 最后,执行宏任务setTimeout,需要打印结果setTimeout 并开启下一次的Event-Loop,执行宏任务中的异步代码,也就是被挤到后面的那个async1 end 对于这个,需要说明一下,因为在谷歌浏览器中,浏览器将await后面的代码提升上一个循环的微任务当中,也就是下列结果的promsise1前面,所以在浏览器里面打印的是下面这份结果,这个我们需要区分开来。

执行结果(浏览器)


    //script start

   // async2 end

   //Promise

   //script end

   //async1 end 

   //promise1

   //promise2

   //setTimeout

执行结果(编译器)


    //script start

   // async2 end

   //Promise

   //script end

   //promise1

   //promise2

   //setTimeout
   
   //async1 end  这一行不同

总结

我是小白,希望能和大家一起学习js,如果存在错误,敬请指出,谢谢大家!