JS高级 - 事件循环

162 阅读4分钟

JS中的线程

名称说明
进程(process)计算机已经运行的程序,是操作系统管理程序的一种方式
启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
进程是线程的容器
线程(thread)操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中
每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程

CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换

当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码, 对于用户来说是感受不到这种快速的切换的

JS常见的宿主环境为node和浏览器,而node和浏览器是多进程的

当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出

而对于每一个页签进程,又都是多线程的。一个页签进程由JS解析线程,UI渲染线程,HTTP网络请求线程等多个线程共同组成

在这些线程中,用于解析和执行JS代码的线程 有且只有一个,而这个线程又被称之为JS主线程

所以JavaScript的代码执行是在一个单独的线程中执行的,JavaScript是单线程的

这就意味着JavaScript的代码,在同一个时刻只能做一件事,如果这件事是非常耗时的,就意味着当前的线程就会被阻塞

但是一个进程中是有多个线程的,所以真正耗时的操作,实际上并不是由JavaScript线程在执行的

而是由浏览器其他线程可以来完成这个耗时的操作,比如网络请求、定时器,我们只需要在特定的时候执行应该有的回调即可

而这类会交给浏览器其它线程来执行的任务就被称之为异步任务

console.log('定时器之前的代码')

// 虽然该定时器所需要等待的时候为0s
// 但是该定时器是异步操作,会被交给浏览器别的线程来进行执行
// 以便于其不会阻塞JS主线程代码的执行
// 因此即使该定时器的等待时间为0s,其依旧会被加入到对应的事件队列中
// 等到JS主线程中的所有代码执行完毕后,浏览器才会去事件队列中读取对应的事件并进行回调
setTimeout(() => {
  // setTimeout这个函数是在主线程中执行的
  // 但是setTImeout对应的回调函数会别加入到对应的事件队列中
  console.log('定时器中的代码')
}, 0)

console.log('定时器之后的代码')
/*
  =>
    定时器之前的代码
    定时器之后的代码
    定时器中的代码
*/

这个过程会形成一个完整的闭环,这个闭环就被称之为事件循环

对应的用于存放异步任务的队列就被称之为事件队列

image.png

宏任务和微任务

事件循环中并非只维护着一个队列,事件队列由宏任务和微任务共同组成

任务说明
宏任务队列(macrotask queue)ajax、setTimeout、setInterval、DOM监听、UI Rendering等
微任务队列(microtask queue)Promise的then回调、 Mutation Observer API、queueMicrotask()等

对应队列的执行顺序:

  1. main script中的代码优先执行
  2. 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
    • 也就是宏任务执行之前,必须保证微任务队列是空的
    • 如果不为空,那么就优先执行微任务队列中的任务(回调)

示例

setTimeout(function () {
  console.log("setTimeout1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});

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

setTimeout(function () {
  console.log("setTimeout2");
});

console.log(2);

queueMicrotask(() => {
  console.log("queueMicrotask1")
});

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});

/*
  =>
    promise1
    2
    then1
    queueMicrotask1
    then3
    setTimeout1
    then2
    then4
    setTimeout2
*/
async function async1 () {
  console.log('async1 start')
  // await后边的代码会等到await后边的promise的状态变成resolve的时候,才会执行
  // 相当于 await后边的代码 是写在await后边promise(在这里就是async2)的then方法中的
  await async2();
  console.log('async1 end')
}

async function async2 () {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout')
}, 0)

async1();

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

console.log('script end')

/*
  =>
    script start
    async1 start
    async2
    promise1
    script end
    async1 end
    promise2
    setTimeout
*/
console.log('script start')

function requestData(url) {
  // 执行到这行代码的时候
  // 会执行Promise构造函数后并将Promise实例返回
  // 所以会执行定时器,但此时定时器会开始计时,所以其内部代码并不会执行
  // 而是会等到2s后在被放入宏任务事件队列
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('setTimeout')
      resolve(url)
    }, 2000)
  })
}

function getData() {
  console.log('getData start')
  requestData('https://www.example.com').then(res => {
    console.log(res)
  })
  console.log('getData end')
}

getData()

console.log('script end')

/*
  =>
    script start
    getData start
    getData end
    script end
    setTimeout
    https://www.example.com
*/