JS事件循环

13,383 阅读5分钟
上周写了篇关于setTimeout的文章,其实也就牵扯到了js的运行机制。所以,这周就来谈谈javascript的运行机制吧。

那就先问个问题吧😁。

为什么JavaScript是单线程的?

还是我来回答吧:

单线程意思就是说同一个时间只能做一件事。那这样的话效率不是很低?也没有啦,其实javascript的单线程特点是跟他的用途有关的。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。假如不是单线程的话,在一个线程当我们在给某个DOM节点增加内容的时候,另一个线程正在删除这个DOM节点的内容,那还得了,那不是乱套了吗。所以javascript只能是单线程。

虽然javascript是单线程,但是javascript中有同步和异步的概念,解决了js阻塞的问题。

同步和异步:

一.同步:

如果在一个函数返回的时候,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。

用代码解释一下:

console.log('Hello');

如果在函数返回时,就看到了预期的效果:在控制台打印了Hello

二.异步:

如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。

代码解释:

fs.readFile('test.txt', 'utf8', function(err, data) {
    console.log(data);
});

在上面的代码中,我们希望通过fs.readFile函数读取文件foo.txt中的内容,并打印出来。但是在fs.readFile函数返回时,我们期望的结果并不会发生,而是要等到文件全部读取完成之后。如果文件很大的话可能要很长时间。

小总结:

  1. 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。

  2. 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作。

javascript就可以进行同步任务和异步任务。把读文件这种操作,ajax请求这些需要耗时的任务放到任务队列中,我还是能够一步步的继续下面的任务。所以啊,javascript还是可以很6。那么异步任务里面只是放要进行异步操作的任务吗,里面会发生啥呢?

任务队列:

上面说过了javascript里面的任务有两种,同步任务和异步任务。

同步任务是指:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步任务指的是,不进入主线程、而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

先看个小栗子吧:

console.log("a");
setTimeout(function () {
    console.log("b");
},0);
console.log("c");

//a
//c
//b

js中代码从上往下执行,执行第一行代码的时候控制台输出a,执行到第二行代码的时候遇到了setTimeout函数,因为setTimeout函数是个异步函数,所以,浏览器会记住这个事件,添加到时间表中,之后把这个事件的回调函数入栈到任务队列中。而此时主线程程序继续往下运行,到了第五行:console.log("c"),执行这条,控制台输出c。这时候主线程空了,他会到任务队列里面去查找是否有可以执行的任务,有的话直接拿出来执行,没有的话会一直去询问,等到有可以执行的。

为了更好的说明任务队列和事件循环,先看一张图。😄

这张图片里面已经画出了js的事件循环的流程了。 流程:

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 当主线程中的执行栈为空时,检查事件队列是否为空,如果为空,则继续检查;如不为空,则执行3;
  3. 取出任务队列的首部,压入执行栈;
  4. 执行任务;
  5. 检查执行栈,如果执行栈为空,则跳回第 2 步;如不为空,则继续检查;

Event Loop:

事件循环其实就是入栈出栈的循环。上面例子中说到了setTimeout,那setInterval呢,Promise呢等等等等,有很多异步的函数。但是这些异步任务有分宏任务(macro-task)和微任务(micro-task):

macro-task包括:script setTimeout, setInterval, setImmediate, I/O, UI rendering。

micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver。

每一次Event Loop触发时:

  1. 执行完主执行线程中的任务也就是执行第一个macro-task任务,例如script任务。
  2. 取出micro-task中任务执行直到清空。
  3. 取出macro-task中一个任务执行。
  4. 取出micro-task中任务执行直到清空。
  5. 重复3和4。
其实promise的then和catch才是microtask,本身的内部代码不是。

注意:

  1. 在浏览器浏览器和node中的执行不一样。

  2. 任务队列里面是“先入先出”的。

那来个小栗子测试一下你是不是已经完全理解啦:
console.log('global')

for (var i = 1;i <= 5;i ++) {
  setTimeout(function() {
    console.log(i)
  },i*1000)
  console.log(i)
}

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
 }).then(function () {
  console.log('then1')
})

setTimeout(function () {
  console.log('timeout2')
  new Promise(function (resolve) {
    console.log('timeout2_promise')
    resolve()
  }).then(function () {
    console.log('timeout2_then')
  })
}, 1000)

控制台输出:

这个博客能够非常好的加深理解:深入理解 JavaScript 事件循环(一)— event loop

总结:

这里主要是讲了在浏览器端js事件循环。这篇文章可以帮助更好的理解node和浏览器环境下不同的事件循环:浏览器和Node不同的事件循环(Event Loop)