单线程JavaScript为何如此高效

575次阅读  |  发布于9月以前

什么是js执行机制

JavaScript 的执行机制指的是 JavaScript 代码在运行时的工作方式和顺序。它涉及以下几个关键概念:

  1. 单线程:JavaScript 是一门单线程的编程语言,意味着它只有一个主线程用于执行代码。这意味着 JavaScript 中的代码是按顺序执行的,一次只能执行一个任务。
  2. 任务队列:JavaScript 通过任务队列来管理要执行的任务。任务队列中存放着各种类型的任务,包括同步任务和异步任务。
  3. 事件循环:JavaScript 的事件循环是一个持续运行的过程,它负责监视任务队列并选择下一个要执行的任务。事件循环不断地从任务队列中获取任务并将其交给主线程执行。
  4. 同步任务和异步任务:同步任务是按照顺序在主线程上执行的任务,执行一个任务时会阻塞后续任务的执行。异步任务是在主线程上注册并在将来某个时间点执行的任务,执行异步任务时不会阻塞后续任务的执行。
  5. 微任务和宏任务:JavaScript 中的任务可以分为微任务和宏任务。微任务是在当前任务执行完毕后立即执行的任务,它们使用微任务队列进行管理。而宏任务是在事件循环的下一轮中执行的任务,它们使用任务队列进行管理。

JavaScript 的执行顺序:

  1. 执行同步任务,将函数调用和变量分配到调用栈中按顺序执行。
  2. 遇到异步任务,如定时器、事件监听等,将其注册到任务队列中,并继续执行后续的同步代码。
  3. 当同步代码执行完毕后,主线程会检查微任务队列,依次执行队列中的微任务。
  4. 执行完微任务后,主线程会从任务队列中取出一个宏任务执行。
  5. 循环执行步骤 3 和步骤 4,直至任务队列和微任务队列都为空。

需要注意的是,JavaScript 中的异步任务通常是通过回调函数、Promise、async/await 等机制来处理。通过合理使用异步任务和任务队列,可以实现非阻塞的代码执行,提高代码的性能和响应能力。JavaScript 的执行机制主要涉及以下几个概念:调用栈、事件循环和任务队列。文字有点单调,看看下面的图理解理解 让我们通过一个例子来解释这些概念。假设我们有以下代码:

console.log("Script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

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

console.log("Script end");

这段代码的执行机制如下:

  1. 首先,开始执行代码,遇到第一行 console.log("Script start"),它会立即打印 "Script start"。
  2. 接下来,遇到 setTimeout,它是一个异步函数,会被放入任务队列中,并设置一个定时器。由于定时器时间为 0,所以不会立即执行。
  3. 然后,遇到 Promise.resolve().then(),它会创建一个 Promise 对象,并将 .then() 中的回调函数放入微任务队列中。
  4. 继续执行下一行,打印 "Script end"。
  5. 此时,主线程上的同步代码执行完毕,开始执行微任务队列中的任务。首先执行 Promise 的回调函数,打印 "Promise"。
  6. 接着,主线程开始执行任务队列中的任务。由于定时器时间到达,setTimeout 的回调函数被放入任务队列中。
  7. 最后,主线程执行任务队列中的任务,打印 "setTimeout"。

综上所述,JavaScript 的执行机制遵循以下步骤:

  1. 执行同步代码,将函数调用和变量分配到调用栈中按顺序执行。
  2. 遇到异步操作,如定时器、事件监听等,将其注册到任务队列中,并继续执行后续的同步代码。
  3. 同步代码执行完毕后,主线程会检查微任务队列,依次执行队列中的微任务(Promise 回调函数)。
  4. 执行完微任务后,主线程会从任务队列中取出任务执行,执行完一个任务后再检查微任务队列,如此循环,直至任务队列为空。

需要注意的是,微任务优先级高于任务队列中的任务,所以在执行任务队列中的任务之前,会先执行完所有的微任务。

现学现用,再看一个例子:

async function async1() {
      console.log("async1 start");
      await async2();
      console.log("async1 end");
    }
    async function async2() {
        console.log("async2");
    }
    console.log("js start");
    setTimeout(function () {
      console.log("timeout");
    }, 0);
    async1();
    new Promise(function (resolve) {
        console.log("promise");
        resolve();
    }).then(function () {
        console.log("then");
    });
    console.log("js end");

这段代码的打印顺序如下:

  1. "js start":立即打印,表示 JavaScript 代码的开始执行。
  2. "async1 start":由于 async1 函数被调用,所以会打印 "async1 start"。
  3. "async2":async1 函数中调用了 async2 函数,因此会打印 "async2"。
  4. "promise":new Promise 的回调函数立即执行,所以会打印 "promise"。
  5. "js end":立即打印,表示 JavaScript 代码的执行结束。
  6. "async1 end":由于 async2 函数是一个异步函数,await async2() 表达式会等待 async2 函数执行完毕,然后继续执行下面的代码,所以会打印 "async1 end"。
  7. "then":Promisethen 方法是异步执行的,所以会在下一个事件循环中执行,因此会打印 "then"。
  8. "timeout":由于 setTimeout 的延迟时间为 0,所以会在下一个事件循环中执行,因此会打印 "timeout"。代码的执行顺序是按照同步代码的顺序执行,异步代码则根据事件循环的机制来执行。async/await 会暂停同步代码的执行,并等待异步操作完成后再继续执行后续的代码。

总结

JS 代码的执行顺序主要为:

  1. 同步代码 同步代码(sync code)直接进入执行栈执行。执行顺序按代码书写顺序。
  2. 异步任务回调 异步任务(如 setTimeout)进入任务队列。
  3. 事件循环 事件循环周期性地从任务队列取出任务,推入执行栈执行。当执行栈为空时,才会取出队列中的任务。
  4. 执行栈先进后出 执行栈采用先进后出的方式执行函数。在函数执行完毕后才会执行上层函数。这保证了函数的正确嵌套调用。
  5. 微任务优先级高于宏任务
    • 宏任务(macrotask):出于任务队列的任务。比如 setTimeout、setInterval。
    • 微任务(microtask):比如 Promise .then、MutationObserver 。微任务的优先级高于宏任务。所以整个执行顺序可以描述为:
  6. 同步代码按顺序进入执行栈执行
  7. 异步宏任务进入任务队列
  8. 当执行栈清空时,执行微任务
  9. 接着执行宏任务
  10. 循环往复

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8