js - promise与异步编程(1):事件循环

113 阅读4分钟

前言

  众所周知,JavaScript是一门单线程语言。单线程语言的代码是同步执行的,这就意味着会面临阻塞的问题。为了实现异步代码,JavaScript采用了一种特殊的执行机制以模拟“多线程”。并且在ES6中还添加了一个专门用于异步的新特性:Promise(期约)。   在介绍Promise之前,有必要先介绍一下JavaScript的运行机制:事件循环

1、同步任务与异步任务

  JS的执行机制指的就是事件循环(Event Loop),每一个JavaScript的"线程环境"都会有一个独立的Event Loop。在介绍事件循环之前,先看看下面这个例子:

setTimeout(console.log,0,1);
console.log(2);
// 2
// 1

  由输出可知,setTimeout里面的函数是后执行的,或者说是经过一定时间后执行的。这类代码就是异步代码也称为异步任务。在JS中可以将任务分为同步任务和异步任务。

  按照这种分类方法,JS的执行机制可以理解为:

  1. 先判断任务是同步任务还是异步任务,同步任务就推入主线程(执行栈)直接进行执行,异步任务就在Event Table进行注册;
  2. 满足异步任务的执行条件以后,就将异步任务推入事件队列(Event Queue)等待执行;
  3. 主线程空闲时,每隔一段时间就会检查事件队列中是否有异步任务等待执行,有就将任务推入主线程依次执行。 以上三步就是事件循环

2、宏任务与微任务

  同步任务和异步任务的分类方式其实并不严谨,JS的执行机制按照的其实是宏任务与微任务的分类方式。再来看看下面这个例子:

setTimeout(console.log,0,1)
//Promise是后面将要介绍的ES6特性,不了解的话,可以简单的先将then()里面的代码理解为异步任务。
Promise.resovle().then(()=>{
    console.log(2);
})
console.log(3);

// 3 
// 2 
// 1

  上述代码的执行情况,与我们之前所说的同步、异步任务执行过程,有所不同。按照之前所说的,setTimeoutPromise.then里面的代码都是异步任务,应该是依次推入Event Loop等待执行,输出应该是 312,但是得到却是 321。

  原因就是,JS代码的分类方式是更准确的宏任务与微任务:

  • macro-task(宏任务):包括整体代码scriptsetTimeoutsetInterval,UI交互事件等。
  • micro-task(微任务)Promise.thenprocess.nextTick等。   不同类型的任务会进入不同的Event queue。实际的执行过程是:先执行一次宏任务,宏任务结束后检查微任务队列(Microtask Queue),依次执行微任务,再检查事件队列,推入执行栈执行下一次宏任务。

  执行下一次宏任务前,浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,会对页面进行重新渲染。

### 1.3、 示例代码
setTimeout(console.log,0,1);

Promise.resolve().then(()=>console.log(2));

setTimeout(()=>{
    console.log(3);
    Promise.resolve().then(()=>console.log(4));
},0);

console.log(5);

setTimeout(console.log,0,6);
// 5
// 2
// 1
// 3
// 4
// 6

执行顺序:

  1. 执行整体代码script, 期间产生了一个Promise.then微任务,注册了三个setTimeout异步回调,输出 5;
  2. 执行栈空闲,执行微任务队列(直至为空),输出 2;
  3. 检查事件队列,因为三个setTimeout回调都为0秒后触发,此时三个setTimeout回调都被推入了事件队列当中,将首个回调推入主线程(执行栈);
  4. 执行回调,没有产生微任务,输出 1;
  5. 检查事件队列,还剩两个setTimeout回调,将首个回调推入主线程(执行栈);
  6. 执行回调,期间产生了一个Promise.then微任务,输出 3;
  7. 执行栈空闲,执行微任务队列(直至为空),输出 4;
  8. 检查事件队列,还剩一个setTimeout回调,将首个回调推入主线程(执行栈);
  9. 执行回调,没有产生微任务,输出 6; 注:从事件队列取出回调也属于宏任务,回调进入调用栈以后,主线程就不再空闲,进入执行状态,回调完成就是一次宏任务完成。

总结

  本文作为介绍Promise的预热,简单介绍了JavaScript的运行机制:事件循环。

  简而言之,事件循环的过程就是三步:宏任务执行结束->微任务队列执行结束->从消息队列获取下一个宏任务。下一节就会介绍Promise的意义和使用方法。