laizimo/zimo-article

JS事件循环

laizimo opened this issue · 1 comments

涉及到多个异步事件执行的时候,大家会不会产生思考!执行顺序,还和原来一样么?还是有什么固定的规则,让我们去判断!今天我们就来了解一下Event Loop的概念。

在聊起这个概念之前,我们先来听一段JavaScript语言的自述。

浏览器为什么选择了我

众所周知,JavaScript是一门单线程的语言。为什么浏览器会去选择它呢!原因就是——简单。在浏览器端,复杂的UI环境会限制多线程语言的开发。例如:

一个线程在操作一个DOM元素的时候,另一个线程需要去删除这个DOM元素。这种情况下,我们就需要进行状态的同步。可怕的是,浏览器往往不止去操作一个DOM元素!所以,为了避免开发中处理这种复杂的情况,单线程语言不失为一种好的解决方案。

但是,单线程也会有它的缺陷——同步阻塞。如图所示:

image

CPU在进行一个I/O操作的时候,需要去请求数据,期间需要等待数据返回之后,才能够继续执行下面的任务。这个等待期,就阻塞了其他任务的执行。因此,JavaScript在执行过程中,将任务分成了同步任务异步任务,来解决类似的情况。

同步/异步

每个线程都有一个执行栈,会根据先进后出的顺序来执行线程中的任务,所有的同步任务,都会被放到这个执行栈中,我们可以来看一段代码:

function fun1(){
  return 'hello hip-hop';
}

function fun2(){
  return fun1();
}

function fun3(){
  console.log(fun2());
}

fun3();   //'hello hip-hop'

它的执行顺序如下:

image

或者我们可以通过浏览器后台的报错来看整个执行顺序,如下:

function fun1(){
  throw new Error('hello hip-hop');
}

function fun2(){
  return fun1();
}

function fun3(){
  console.log(fun2());
}

fun3();

浏览器后台的报错提示,如图:

image

在此基础上,我们如果加入异步任务,会发生什么样的情况呢,如下:

console.log('first');
setTimeout(() => {
  console.log('second');
}, 500);
console.log('three');

我们依然通过画图的形式,来直观地感受一下执行栈的顺序,如图:

image

从图中,我们可以清晰地看到setTimeout执行完成之后,就出栈了!那么,后来的console.log('second')是如何入栈的呢?

其实,在主线程之外,还存在一个任务队列。异步任务,都会被放到任务队列中。只有当指定事件触发之后,异步任务才会被放到主线程中执行。

任务队列中,是一个事件队列。拿setTimeout举例来说:

  • 当主线程执行到setTimeout的时候,会创建一个定时器;
  • 一旦定时器的到达时间,就会将回调函数放到任务队列中;
  • 当主线程任务执行完成之后,就会去循环任务队列,执行回调函数;

事件循环

上述流程,规范为一张图如下:

这里有个循环,是一个死循环,无论哪种情况都是闭环,这个就是事件循环。事件循环不断地在检测队列是否存在已触发的任务,如果有的话,就放到主线程中执行(注:这个过程往往在主线程执行完之后进行)。

这幅图里面,我们看到了有浏览器的点击事件、ajax请求、Promise等这里。但是不同之处在于,它们的任务性质存在不同。

自从,ES6出现之后,Promise逐渐被开发者热议。这里我们来讨论一下它这方面的特殊性。

首先,我们来看一段代码:

setTimeout(() => {
  console.log(1);
}, 0);

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

console.log(4);      // 4   2    3    1

你会不会有所疑问,为啥不是4 1 2 3的顺序呢?

其实,这个执行顺序和任务队列有关系!任务队列中存在两种队列类型:宏任务微任务。宏任务可以有多个队列,微任务只能有一个!同时,宏任务是一个一个出队的,而微任务是一队一队出队的

在执行事件循环的过程中:

  • 主线程会先遍历一遍微任务队列,然后将队列中的函数抽离出来执行。
  • 执行完成之后,再执行一个宏任务,在循环一遍微任务队列,再执行一个宏任务
  • 直至队列都循环完毕。

了解清楚这个后,我们再回头看,心中亦如明镜。setTimeout是宏任务,Promise是微任务。具体分类如下:

  • 宏任务:setTimeout,setInterval,JavaScript(整段代码),I/O操作,UI渲染等
  • 微任务:Promise,process.nextTick(NodeJS)等

总结

本文我们回顾了:

  • JS的线程机制,同步和异步操作
  • 事件循环的过程
  • 任务队列的不同

希望对于你来说有所收获,感谢阅读。

欢迎您扫一扫上面的微信公众号,订阅我的博客!