JavaScript单线程以及事件循环

为什么 JavaScript 是单线程?

JavaScript 的单线程特点主要是由于其主要用途是与用户进行交互和操作 DOM。如果 JavaScript 是多线程的话,会引入复杂的问题。例如,一个线程在 DOM 上新增内容,而另一个线程在删除这个新增的内容,这时候浏览器无法确定以哪个线程为主,因此决定了 JavaScript 只能是单线程。

为了利用多核 CPU 的计算能力,HTML5 提出了 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,并且不能操作 DOM。因此,这个新标准并没有改变 JavaScript 单线程的本质。

然而,在使用 Web Worker 时需要注意,如果 Worker 线程和主线程之间的通信时间大于不使用 Worker 线程的时间,那么使用 Worker 线程就得不偿失。

任务队列

单线程意味着所有任务需要排队,前一个任务单元完成后才会执行下一个任务单元。如果前一个任务单元耗时很长,后面的任务都需要等待前面任务单元完成才能执行(类似于食堂排队)。

任务队列中的事件包括 IO 设备的事件以及用户产生的事件,例如点击事件和滚动事件。这些事件发生时会进入"任务队列",等待主线程读取。

有些操作是非常耗时的,例如 IO 设备操作和 Ajax 等异步操作,不能等待结果出来再继续执行。因此,任务被分为两种:同步任务和异步任务。

同步任务

同步任务是在主线程上排队执行的任务,只有前一个任务单元执行完毕,才会执行下一个任务单元。

异步任务

异步任务不会进入主线程,而是被放入事件表格中。当放入事件表格中的异步任务完成某个事情或达成某种条件时,才会将这个异步任务推入事件队列(Event Queue),此时异步任务才能在执行栈中空闲时被读取。

JavaScript 执行机制:Event Loop

主线程从任务队列中读取事件,这个过程是不间断的,因此这种运行机制被称为事件循环(Event Loop)。

JavaScript 的同步与异步

单线程意味着所有任务需要排队,前一个任务结束才会执行后一个任务。简单来说,就是按顺序一个一个执行。如果前一个任务耗时很长,后一个任务就必须一直等待。如果排队是因为计算量大导致 CPU 忙不过来,那还可以理解。但是很多时候 CPU 是闲着的,因为 IO 设备(输入输出设

备)很慢(例如通过网络进行的 Ajax 操作),不得不等待结果出来后才能继续执行。

JavaScript 语言的设计者意识到,此时主线程完全可以不管 IO 设备,而是挂起处于等待中的任务,先运行排在后面的任务。等到 IO 设备返回结果后,再回过头来继续执行被挂起的任务。因此,所有任务可以分为两种:同步任务(synchronous)和异步任务(asynchronous)。同步任务指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是不进入主线程而是进入"任务队列"(task queue)的任务,只有当"任务队列"通知主线程某个异步任务可以执行时,该任务才会进入主线程执行。

异步任务又分为宏任务(Macro Task)和微任务(Micro Task)。

Macro Task

包括 setTimeoutsetInterval、事件绑定、网络请求、IO 读写等。

Micro Task

  • Promise(包括 async/await):Promise 并不是完全的同步,在 Promise 中是同步任务,执行 resolve 或 reject 回调时属于异步操作,会先将 then/catch 等放入微任务队列。当主栈执行完后,才会再去调用 resolve/reject 方法执行。
  • process.nextTick(Node.js 中实现的 API,将当前任务放到主栈的最后执行,在主栈执行完后,先执行 nextTick,再到等待队列中找)
  • MutationObserver(创建并返回一个新的 MutationObserver,它会在指定的 DOM 发生变化时被调用)

执行顺序为:同步任务 -> 微任务 -> 宏任务。

以下是一个示例代码的执行过程:

console.log("同步任务1");

function asyncFunc(mac) {
  console.log("同步任务2");
  if (mac) {
    console.log(mac);
  }
  return new Promise((resolve, reject) => {
    console.log("Promise 中的同步任务");
    resolve("Promise 中回调的异步微任务");
  });
}

setTimeout(() => {
  console.log("异步任务中的宏任务");
  setTimeout(() => {
    console.log("定时器中的定时器(宏任务)");
  }, 0);
  asyncFunc("定时器传递的任务").then(res => {
    console.log('定时器中的:', res);
  });
}, 0);

asyncFunc().then(res => {
  console.log(res);
});

console.log("同步任务3");
console.log("同步任务1");

function asyncFunc(mac) {
  console.log("同步任务2");
  if (mac) {
    console.log(mac);
  }
  return new Promise((resolve, reject) => {
    console.log("Promise 中的同步任务");
    resolve("Promise 中回调的异步微任务");
  });
}

setTimeout(() => {
  console.log("异步任务中的宏任务");
  setTimeout(() => {
    console.log("定时器中的定时器(宏任务)");
  }, 0);
  asyncFunc("定时器传递的任务").then(res => {
    console.log('定时器中的:', res);
  });
}, 0);

asyncFunc().then(res => {
  console.log(res);
});

console.log("同步任务3");

首先输出 “同步任务1”,然后遇到 setTimeout 异步宏任务,先放入宏任务队列中挂起,继续执行下一个同步任务 asyncFunc()。

执行 asyncFunc 函数,输出其中的 “同步任务2”,然后返回 Promise,输出 “Promise 中的同步任务”,遇到 resolve() 回调函数,.then 函数属于异步微任务,先放入微任务队

列中挂起。

继续执行,输出 “同步任务3”,此时同步任务执行完毕,执行栈为空。先执行微任务队列中的 .then 函数块,输出 “Promise 中回调的异步微任务”,此时微任务队列为空。

然后检查宏任务模块时间是否到了,如果到了就执行宏任务队列,先输出 “异步任务中的宏任务”,然后遇到内部的 setTimeout,先放入宏任务队列中,继续执行。

调用 asyncFunc 函数,执行其中的同步任务,输出 “同步任务2”,然后输出 “定时器传递的任务”,返回 Promise,执行 Promise 中的同步代码,输出 “Promise 中的同步任务”。

遇到 .then,先放入微任务队列。此时执行栈又为空。先执行微任务队列中的 .then 函数块,输出 “定时器中的:Promise 中回调的异步微任务”,此时微任务队列为空。执行宏任务队列,输出 “定时器中的定时器(宏任务)”。

程序执行完毕。

总结

这篇文章讨论了JavaScript为什么是单线程的,并解释了JavaScript的执行机制和任务队列的概念。以下是对文章的总结:

  • JavaScript是单线程的主要原因是其主要用途是与用户进行交互和操作DOM,如果引入多线程可能导致复杂的问题,例如在不同线程上同时对DOM进行新增和删除操作,浏览器无法确定以哪个线程为主,因此决定了JavaScript必须是单线程的。
  • HTML5提出了Web Worker标准来利用多核CPU的计算能力,允许JavaScript创建多个线程,但是这些子线程完全受主线程控制,且不能操作DOM,因此并没有改变JavaScript单线程的本质。
  • 引入任务队列的概念来管理JavaScript中的同步任务和异步任务。同步任务是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务。异步任务是不进入主线程,而是进入任务队列的任务,只有任务队列通知主线程某个异步任务可以执行时,该任务才会进入主线程执行。
  • 异步任务分为宏任务和微任务。宏任务包括setTimeout、setInterval、事件绑定、网络请求、IO读写等。微任务包括Promise(包括async/await)、process.nextTick、MutationObserver等。
  • JavaScript的执行机制是通过事件循环(Event Loop)来实现的。主线程从任务队列中读取事件,这个过程是不间断的,所以称为事件循环。
  • JavaScript中的同步任务会先执行,然后执行微任务,最后执行宏任务。

文章通过示例代码展示了JavaScript代码的执行过程,从同步任务到微任务和宏任务的执行顺序,帮助读者更好地理解JavaScript的执行机制。

总的来说,这篇文章清楚地解释了为什么JavaScript是单线程的,并介绍了JavaScript的执行机制和任务队列的概念,通过示例代码帮助读者理解JavaScript代码的执行过程。