JS事件循环(event loop)

事件循环概述

事件循环是用来实现异步特性的。

事件循环中的几个概念:

主线程:

  • 理解为同步任务的先进先出,一旦调用,同步任务就执行。

执行栈:

  • 先入后出的数据结构,一个任务来到栈底就立即执行,然后释放。

任务队列:

  • 包括宏任务队列和微任务队列,当执行栈空的时候,就会从任务队列中,取任务来执行。

宏任务和微任务:

  • 异步任务,通常在主线程执行完同步任务完成之后,才会来执行宏任务和微任务。微任务的优先级要比宏任务高。

常见的宏任务:

  • script(全局任务)、setTimeout、setInterval、setImmediate、I/O、UI Rendering。

常见的微任务:

  • Process.nextTick、Promise、Object.observer、MutationObserver、queueMicrotask(将函数添加到微任务队列)

注意:new Promise的过程属于同步任务,resolve或者reject之后才算微任务。


执行过程

image

主线程首先执行完同步任务,然后会去任务队列中执行宏任务,如果在执行宏任务的过程中发现有微任务,这时候微任务比宏任务先执行。全部执行完成之后等待主线程调用,调用完成之后再去任务队列中查看是否还有异步任务有待执行,循环往复。


事件循环详解

在实际开发中,往往是同步代码和异步代码都有。在js执行时,还是从第一行代码开始执行,遇到函数就将其添加到栈中,然后执行同步操作;如果遇到异步函数,则根据其类型,宏任务就添加到宏任务队列,微任务添加到微任务队列。直到同步代码执行完毕,则开始执行异步操作。

异步操作后于同步操作,异步操作内部也是分先后顺序的。总的来说:

  • 微任务先于宏任务执行
  • 微任务与微任务之间根据先后顺序执行,宏任务与宏任务之间根据延迟时间顺序执行
  • 微任务在下一轮DOM渲染前执行,宏任务在下一轮DOM渲染之后执行
  • 每个任务的执行都是一次出栈操作,直到栈被清空

微任务在下一轮DOM渲染前执行,宏任务在之后执行

let div = document.createElement('div');
div.innerHTML = 'hello world';
document.body.appendChild(div);
let list = document.getElementsByTagName('div');
console.log('同步任务 length ---', list.length);
console.log('start');
setTimeout(() => {
  console.log('setTimeout length ---', list.length);
  alert('宏任务 setTimeout 阻塞'); // 使用alert阻塞js执行
}, 1000);
Promise.resolve().then(() => {
  console.log('promise then length ---', list.length);
  alert('微任务 promise then 阻塞');
});
console.log('end');

// 运行结果:
// 同步任务 length --- 1
// start
// end
// promise then length --- 1
// alert('微任务 promise then 阻塞'),此时的页面是白屏,因此处于DOM渲染前
// setTimeout length --- 1
// alert('宏任务 setTimeout 阻塞'),此时页面出现了hello world,因此处于DOM渲染后

微任务中创建宏任务

new Promise((resolve) => {
  console.log('promise 1');
  setTimeout(() => {
    console.log('setTimeout 1');
  }, 500);
  resolve();
}).then(() => {
  console.log('promise then');
  setTimeout(() => {
    console.log('setTimeout 2');
  }, 0);
});
new Promise((resolve) => {
  console.log('promise 2');
  resolve();
});

// 运行结果:
// promise 1
// promise 2
// promise then
// setTimeout2
// setTimeout 1

解析:

js执行代码,遇到两个Promise,则分别添加到微任务队列,同步代码执行完毕。

在微任务队列中根据先进先出,第一个Promise先执行,遇到setTimeout,则添加到宏任务队列,resolve()返回执行结果并执行then,事件循环将其继续添加到微任务队列;第一个Promise执行完毕,执行第二个Promise。

继续执行微任务队列,直到清空队列。遇到setTimeout,并将其添加到宏任务队列。

宏任务队列现在有两个任务待执行,由于第二个setTimeout的延迟事件更小,则优先执行第二个;如果相等,则按照顺序执行。

继续执行宏任务队列,直到清空队列。


宏任务中创建微任务

setTimeout(() => {
  console.log('setTimeout 1');
  new Promise((resolve) => {
    console.log('promise 1');
    resolve();
  }).then(() => {
    console.log('promise then');
  })
}, 500);
setTimeout(() => {
  console.log('setTimeout 2');
  new Promise((resolve) => {
    console.log('promise 2');
    resolve();
  })
}, 0);

// 运行结果:
// setTimeout2
// promise2
// setTimeout1
// promise1
// promise then

解析:

js执行代码,遇到两个setTimeout,将其添加到宏任务队列,同步代码执行完毕。

先检查微任务队列中是否有待处理的,刚开始肯定没有,因此直接执行宏任务队列中的任务。第二个为零延迟,需要优先执行。遇到Promise,将其添加到微任务队列。第一个宏任务执行完毕。

在执行第二个宏任务时,微任务队列中已经存在待处理的,因此需要先执行微任务。

微任务执行完毕,并且延迟时间到期,第一个setTimeout开始执行。遇到Promise,将其添加到微任务队列中。

执行微任务队列中的Promise,执行完毕后遇到then,则将其继续添加到微任务队列。

直到所有微任务执行完毕。


宏任务中创建宏任务

setTimeout(() => {
  console.log('setTimeout 1');
  setTimeout(() => {
    console.log('setTimeout 2');
  }, 500);
  setTimeout(() => {
    console.log('setTimeout 3');
  }, 500);
  setTimeout(() => {
    console.log('setTimeout 4');
  }, 100);
}, 0);

// 运行结果
// setTimeout1
// setTimeout4
// setTimeout2
// setTimeout3

无需解析。


微任务中创建微任务

new Promise((resolve) => {
  console.log('promise 1');
  new Promise((resolve) => {
    console.log('promise 2');
    resolve();
  });
  new Promise((resolve) => {
    console.log('promise 3');
    resolve();
  })
  resolve();
})

// 运行结果:
// promise1
// promise2
// promise3

无需解析。

posted @ 2022-09-05 09:25  笔下洛璃  阅读(1367)  评论(0编辑  收藏  举报