JS的事件循环 Event Loop

158 阅读6分钟

JS的事件循环 Event Loop

javascript是单线程,当遇到异步任务的时候,它会将异步任务放到事件队列中去,先去执行后面的代码,当同步任务执行完成以后,再回来处理事件队列中的回调函数。

如果一个异步任务中又嵌套着另一个异步任务,又会重复上述的过程,我们将这个循环往复的过程称之为事件循环(event loop)。

那么这篇要学的知识点有这些

  1. 浏览器的线程和js的单线程
  2. js的执行栈
  3. 事件队列的异步任务:宏任务,微任务
  4. 事件循环的具体过程

浏览器的线程

浏览器是多线程的,这区别于js是单线程的,浏览器在运行的时候,会有很多线程,其中就包括JS的执行线程,但是浏览器的多线程中,GUI渲染线程和JS执行线程是互斥的。当JS执行的时候渲染线程就会挂起。至于为什么会挂起,原因就是js可以修改dom节点,如果继续渲染,可能会渲染出来没被修改前的dom节点,或者产生一些意想不到的错误。

浏览器线程主要有以下几种

  1. GPU进程
  2. 网络进程
  3. 插件进程
  4. 浏览器主进程
  5. 渲染进程
    • GUI渲染线程
    • js执行线程
    • 事件触发线程
    • 定时器线程
    • 异步请求线程

浏览器线程.jpg

JS的执行栈

js执行线程执行任务的时候,会创建js执行栈,同步任务直接推入执行栈中执行,异步任务放入事件队列中,并注册回调函数。当执行栈空闲的的时候,js会读取事件队列中的函数到执行栈中执行。

执行栈遵循先进后出,后进先出原则。执行栈中涉及到函数的执行上下文。这里简单说明一下,与本次的事件循环关系不大。

  • js的所有代码都是排队执行的
  • 一开始浏览器执行全局代码的时候,会创建全局执行上下文,压入栈中。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部,当前函数执行完成后,当前执行上下文出栈,并等待垃圾回收。
  • 浏览器js执行引擎总是访问栈顶的执行上下文

备注:这里的执行上下文,可以简单的理解为变量的作用域

事件队列中的异步任务划分

异步任务分为宏任务和微任务

  • 宏任务: script, setTimeout,ajax请求,I/O操作,等等
  • 微任务: Promsie.then,MutationObserver,process.nextTick()等等

整个js代码被当做一个宏任务,也就是上面分类的script,也是一轮循环的起点,一轮循环的终点是所有微队列里面的微任务全部执行完毕。下一轮的起点又是一个宏任务被推入执行栈。

事件循环的具体过程

上面说了,js执行的时候会创建执行栈,同时也会有一个事件队列,队列里面分宏队列和微队列(我是这样理解的)。js的代码都在在执行栈中执行的,异步任务会放到事件队列中。那么具体执行流程如下:

  1. 整个script代码被当做一个宏任务,放到执行栈中执行,当遇到宏任务的时候,直接放到宏队列当中,遇到微任务的时候,会放到微队列当中。
  2. 当执行栈中的代码执行完毕,js首先会将微任务队列的任务推到执行栈中执行,由于微任务中可能又有宏任务和微任务,所以继续将宏任务和微任务推入不同的队列。
  3. 当所以微队列的微任务都被执行完了以后,会将第一个推入宏队列的任务,取出执行。(此时就是一个新的事件循环的开始,事件循环的结束,就是所以微任务被执行完毕)

代码示例

示例一

console.log('开始');

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

let p = new Promise((resolve, reject) => {
  console.log(2);
  resolve(3);
});

p.then((value) => {
  console.log(value);
});

console.log(4);


// 输出结果;

开始
2
4
3
1

解析:

  1. 首先一整个代码被当做一个宏任务推入执行栈中执行
  2. 当遇到console.log('开始'); 打印开始
  3. setTimeout是个宏任务里面的函数被推入宏队列
  4. 执行到Promise时,Promise里面传入的是一个立即执行函数,所以它会立马执行,打印出2,并且将promise的状态改成了成功。
  5. p.then是个微任务,被放入微队列
  6. 执行console.log(4);打印出4
  7. 第一个宏任务执行完毕,执行微队列里面的微任务,所以执行p.then里面的函数,打印出来3,
  8. 微队列执行完毕,开始下一轮循环,将宏队列里面的第一个推入执行栈执行,也就是执行setTimeout里面的函数console.log(1);打印出来1。整个代码执行完毕。

备注:这里Promise立即执行函数和回调,我已经在上一篇说过了,不会的看这个链接的文章。 juejin.cn/post/709530…

示例二

console.log('开始');

setTimeout(() => {
  console.log(1);

  let p1 = new Promise((resolve) => {
    console.log(2);
    resolve(3);
  });

  p1.then((value) => {
    console.log(value);
  });
});

let p2 = new Promise((resolve, reject) => {
  console.log(4);
});

p2.then(() => {
  console.log(5);
  setTimeout(() => {
    console.log(6);
  })
});

console.log(7);

// 输出结果:
开始
4
7
1
2
3

解析:

  1. 执行到console.log('开始'); 打印开始
  2. setTimeout为宏任务,直接放入宏队列
  3. p2的new Promise立即执行函数会被执行,打印出来4,但是它没有改变状态,所以p2.then不会被放入微队列。
  4. 执行console.log(7);打印7
  5. 微队列无任务,直接进入下一轮循环,执行setTimeout里面的函数,打印1
  6. p1的new Promise立即执行函数执行,打印出来2,并且改变状态,p1.then里面的函数放入微队列,执行栈空闲,取微队列里面任务,打印出来3.结束。

下面的实例自己去研究吧,我就不一一解释了,有问题评论区

示例三

console.log('1');

setTimeout(function() {
  console.log('2');
  new Promise(function(resolve) {
    console.log('3');
    resolve();
  }).then(function() {
    console.log('4')
  })
  setTimeout(function() {
    console.log('5');
    new Promise(function(resolve) {
      console.log('6');
      resolve();
    }).then(function() {
      console.log('7')
    })
  })
  console.log('14');
})

new Promise(function(resolve) {
  console.log('8');
  resolve();
}).then(function() {
  console.log('9')
})

setTimeout(function() {
  console.log('10');
  new Promise(function(resolve) {
    console.log('11');
    resolve();
  }).then(function() {
    console.log('12')
  })
})
console.log('13')

// 输出结果

// 1 8 13 9 2 3 14 4 10 11 12 5 6 7

示例四

console.log('1');

setTimeout(function() {
  console.log('2');
  process.nextTick(function() {
    console.log('3');
  })
  new Promise(function(resolve) {
    console.log('4');
    resolve();
  }).then(function() {
    console.log('5')
  })
})
process.nextTick(function() {
  console.log('6');
})
new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
})

setTimeout(function() {
  console.log('9');
  process.nextTick(function() {
    console.log('10');
  })
  new Promise(function(resolve) {
    console.log('11');
    resolve();
  }).then(function() {
    console.log('12')
  })
})
console.log('13')

// 输出结果
// 1 7 13 6 8 2 4 3 5 9 11 10 12

写在最后

通过这篇文章的学习,应该是完全能应对,面试题中的事件循环了吧,其实很简单,只要理解一次循环的过程,和能区分宏任务和微任务,就好了,剩下的都是同步代码的执行,都很简单。只要习题能做出来,问题应该不大。

嗯,写到这里,我发现用process.nextTick是微任务,哈哈哈,我上一章在promise里面用setTimeout去模仿微任务,真的太蠢拉。我要去在评论区给自己纠正一下。