JavaScript的运行机制

129 阅读5分钟

前言

本人前端小白,最近我想把学习的知识做成笔记发表出来,希望大家有错误可以指出来。

单线程

JavaScript 是一种单线程编程语言,这意味着它一次只能执行一个任务。为了处理可能需要较长时间的任务(如网络请求、文件读写等)而不阻塞主线程,JavaScript 引入了同步(Synchronous)和异步(Asynchronous)执行的概念。

(一)同步与异步

1.什么是同步(Synchronous)?

在同步模式下,代码按顺序逐行执行。当前面的代码行执行完毕后,才能执行后面的代码行。如果在执行一个函数或者语句时需要等待某些操作完成(如等待一个网络响应),那么JavaScript 引擎会阻塞该线程,直到操作完成。

例如:

console.log('开始');
console.log('中间');
console.log('结束');

2.什么是异步(Asynchronous)?

异步操作允许JavaScript 执行不需要等待某些操作完成的代码。异步操作通常涉及到以下几种方式:

  1. 回调函数(Callbacks) :这是最传统的异步处理方式,你定义一个函数,当异步操作完成时,该函数会被调用。通常有计时器(setimeout,setinterval)
console.log('开始');
setTimeout(() => {
  console.log('中间的异步操作');
}, 1000);
console.log('结束');
//结果为:开始 结束 中间的异步操作

在上面的代码中,setTimeout 是一个常见的异步JavaScript函数,它将在1000毫秒后执行回调函数。控制台会首先打印“开始”和“结束”,然后在大约1秒后打印“中间的异步操作”。

  1. Promises:这是ES6引入的一种更优雅的异步处理方式。Promise代表了一个最终可能完成也可能失败的操作,并且它提供了更强大的错误处理。
console.log('开始');
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('中间的异步操作');
  }, 1000);
}).then((message) => {
  console.log(message);
});
console.log('结束');
//结果:开始 结束 中间的异步操作
  1. async/await:这是ES2017引入的语法,允许用同步的方式写异步代码。
async function asyncFunction() {
  console.log('开始');
  const message = await new Promise((resolve) => {
    setTimeout(() => {
      resolve('中间的异步操作');
    }, 1000);
  });
  console.log(message);
  console.log('结束');
}

asyncFunction();

asyncFunction中,await关键字“暂停”了函数的执行,直到Promise解决(或者拒绝)。尽管代码看起来是同步的,但实际上它是异步执行的。

(二)process.nextTick与setimmediate方法

  1. process.nextTick:在同步代码执行之后,异步代码执行之前执行

  2. setimmediate:异步执行之后执行

所以代码执行顺序:

  1. 同步代码
  2. nextTick
  3. 异步代码
  4. setImmediate

(三)事件循环

JavaScript 的运行环境是单线程的,这意味着它一次只能执行一个任务。为了实现非阻塞的行为,JavaScript 引入了事件循环(Event Loop)机制。事件循环是JavaScript引擎处理异步任务的方式。以下是事件循环的基本工作原理:

执行栈(Call Stack)

当JavaScript代码开始执行时,它会进入执行栈(也称为调用栈)。这是一个后进先出(LIFO)的数据结构,用于存储在代码执行期间调用的所有函数。

任务队列(Task Queue)

当异步事件发生时(比如一个定时器到期,或者网络请求完成),对应的回调函数不会立即执行,而是被放入任务队列(也称为回调队列)中。任务队列是一个先进先出(FIFO)的数据结构。

事件循环(Event Loop)

事件循环不断地检查执行栈是否为空。如果执行栈为空,事件循环会从任务队列中取出第一个任务并将其放入执行栈中执行。这个过程会不断重复,这就是所谓的“事件循环”。 以下是事件循环的详细步骤:

  1. 执行同步代码:JavaScript引擎从全局代码开始,按顺序执行所有同步代码。
  2. 遇到异步任务:当遇到异步任务(如 setTimeout, setInterval, Promise, XMLHttpRequest 等)时,JavaScript引擎会将其挂起,并继续执行栈中的其他代码。
  3. 异步任务完成:异步任务完成后,它的回调函数会被放入任务队列中。
  4. 检查执行栈:事件循环不断地检查执行栈是否为空。
  5. 执行任务队列中的任务:一旦执行栈为空,事件循环会从任务队列中取出第一个回调函数,并将其放入执行栈中执行。
  6. 重复过程:事件循环重复步骤4和5,直到任务队列为空。

(四)宏任务(Macro Task)与微任务(Micro Task)

在事件循环中,任务队列实际上分为宏任务队列和微任务队列:

  • 宏任务(Macro Task):包括整体代码script,setTimeout,setInterval等。
  • 微任务(Micro Task):包括Promise的回调,process.nextTick(Node.js环境)等。 事件循环的每个循环周期称为一个tick,在每个tick结束时,JavaScript引擎会先检查微任务队列,如果有微任务,则会执行它们直到微任务队列为空,然后才会执行下一个宏任务。

示例

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

执行结果:

script start
script end
promise1
promise2
setTimeout

解释:

  • console.log('script start');console.log('script end'); 是同步代码,按顺序执行。
  • setTimeout 的回调函数被放入宏任务队列。
  • Promise 的回调函数被立即放入微任务队列。
  • 在执行栈为空后,事件循环先执行微任务队列中的 promise1promise2
  • 最后,在下一个tick中,事件循环从宏任务队列中取出 setTimeout 的回调并执行。