Event Loop里的task queue

首先 js 是单线程(当然 web worker 可以创建新的线程), js单线程任务被分为同步任务异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行(如下图)。

task queue(上图的任务队列) 分为:

  • macro-task (task/宏任务)由js外部宿主提供的 如: 渲染事件(如解析 DOM、计算布局、绘制);用户交互事件(如鼠标点击、滚动页面、放大缩小等);JavaScript 脚本执行事件;网络请求完成、文件读写完成事件。
  • micro-task (jobs/微任务)由js自己提供的,例如: process.nextTick, Promise, Object.observe(已废弃), MutationObserver( 核心就是采用了微任务机制,有效地权衡了实时性和执行效率的问题。 )

setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务

task queue的入队顺序如下面的代码

for (const macroTask of macroTaskQueue) {  
  handleMacroTask();   
  for (const microTask of microTaskQueue) {
    handleMicroTask(microTask);  
  }
}

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。
每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。

以下是案例:

setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');

以上代码根据规则 :

  • timeout1首先进入宏任务队列,
  • Promise实例第一个参数不会进入其他队列,在当前任务执行,then里面的函数进入微任务队列
  • 执行console.log(‘global1’);
  • 这时候微任务队列里有 console.log(‘then1’); ,所以执行它,然后微任务队列为null
  • 当前微任务队列是null,所以执行宏任务里的代码console.log(‘timeout1’);

所以最后的打印结果是:

  • promise1
  • promise2
  • global1
  • then1
  • timeout1

有关async 和await

我们经常使用上面两个关键字来写同步代码,这个时候要了解的是他们做了什么

async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

await 将等待 Promise 正常处理完成并返回其处理结果。 如果该值不是一个 Promise,await 会把该值转换为已正常处理的Promise,然后等待其处理结果。

示例:

async function async1() {
    console.log('async 1 start');
    await async2();
    console.log('async 1 end');
  }
  
  async function async2() {
    console.log('async 2');
  } 
  
  async1();
  new Promise(function(resolve) {
    console.log('promise1');
    resolve();
  }).then(function() {
    console.log('promise2');
  })

上面的代码可以理解为:

  • async1里的 console.log(“async 1 start”)可视为promise里面的同步代码,在当前执行栈中直接执行
  • 执行到await时,async2里的console.log(“async 2”)与上面逻辑一样,而console.log(‘async 1 end’);可视为promise().then()里执行,进入微任务队列
  • 再执行console.log(“promise1”),console.log(“promise2”)进入微任务队列
  • 此时微任务队列按照进入的先后顺序进入执行栈执行,依次打印async 1 end 和promise2

所以最后结果为:

  • async 1 start
  • async 2
  • promise1
  • async 1 end
  • promise2

ps: promise里的then的实现根据规范可以当做微任务,也可以是宏任务,以上代码都是在chrome里执行的,各个浏览器可能实现不一样

参考:

https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/shi-er-3001-shi-jian-xun-huan-ji-zhi.html

https://juejin.im/post/5c3d8956e51d4511dc72c200