JavaScript——事件循环机制(Event Loop)浅析

lxf2023-05-13 00:52:43

JavaScript的事件循环机制是一个非常重要的概念,它决定了JavaScript在浏览器和Node.js中如何处理异步任务和事件。在本文中,我们将浅析JavaScript事件循环机制的工作原理,并通过代码例子来解释它。前置预备知识是async_awaitPormise,可以去看一下这两篇文章。从回调地狱到异步之王:小白初次感受JS Promise的魅力!和异步编程的利器:JavaScript async/await初学者指南。

一、JavaScript是单线程语言

JavaScript语言是单线程的,也就是说,同一时间只能做一件事,不存在执行一行代码的同时又在执行另外一行代码。

1.JavaScript为何不设计成多线程语言?

JS作为浏览器的脚本语言,在浏览器中,主要是实现用户与浏览器的交互以及操作DOM,而这些操作必须在单一线程上执行,以保证页面安全性和稳定性。假设JavaScript 是多线程的,那么多个线程可能同时访问和修改同一个 DOM 元素,这可能导致一些不可预料的问题,例如多个线程同时修改 DOM 元素的样式或属性,导致页面出现奇怪的显示问题或 JavaScript 执行出错。此外,如果多个线程同时访问和修改同一个变量或对象,也可能导致数据不一致的问题。

2.单线程的好处

单线程的好处是代码简单、易于调试,也可以避免一些多线程编程中常见的并发问题,例如死锁、竞态条件等。单线程还可以降低浏览器的内存占用CPU 开销(比如节省上下文切换的时间)。虽然 JavaScript 是单线程的,但是它可以通过事件循环机制来处理异步任务,使得程序可以同时执行多个任务,而不会阻塞主线程。因此,即使 JavaScript 是单线程的,也可以处理大部分的异步编程需求。

二、进程和线程和渲染线程

1.进程

进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位,进程可以拥有资源,是系统中拥有资源的一个基本单位,是能拥有资源和独立运行的最小单位。每个进程都有自己独立的内存空间和执行环境,可以同时运行多个进程,每个进程之间相互独立,互不影响。

2.线程

线程(thread)是比进程更小的基本单位,是调度和分派的基本单位,线程本身并不拥有系统资源,仅有一点必不可少的、能保证独立运行的资源,是cpu调度的最小单位。

每个进程可以包含一个或多个线程。线程共享进程的内存空间和资源,但是每个线程有自己的栈空间和程序计数器。

3.渲染线程

在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。 所以,浏览器是一个多进程的。 在浏览器中,渲染线程是浏览器中负责解析 HTML、CSS 和 JavaScript 并将其显示在屏幕上的线程。渲染线程通常是单独的进程,与浏览器的其他进程(例如浏览器内核、网络进程、GPU 进程等)相互独立。

三、宏任务和微任务

在JavaScript中,异步代码可以分为宏任务和微任务,它们的执行顺序不同。

宏任务(macro-task)是指在主线程上执行的任务,包括以下几种:

  • 1.script整体代码
  • 2.setTimeout()
  • 3.setInterval()
  • 4.I/O 操作(例如文件读写、HTTP请求等)
  • 5.UI 渲染
  • 6.setImmediate()(仅在 Node.js 环境下)

微任务(micro-task)是指在当前宏任务执行结束后立即执行的任务,包括以下几种:

  • 1.Promise 的 then() 、catch()、finally()方法
  • 2.async/await (实际就是Promise)
  • 3.process.nextTick()(仅在 Node.js 环境下)
  • 4.MutationObserver(仅在浏览器环境下)

微任务的执行优先级高于宏任务,即微任务中的所有任务都会在下一个宏任务之前执行完毕。当一个宏任务执行完毕后,会先执行微任务队列中的所有任务,直到微任务队列为空,然后再从宏任务队列中取出一个任务执行,以此类推。

在当前的微任务没有执行完成时,是不会执行下一个宏任务的。例如,如果我们在一个微任务中使用 setTimeout() 函数添加一个宏任务,那么这个新的宏任务会在当前微任务执行完毕后立即执行,而不是等待当前宏任务执行完毕再执行。

四、任务队列

任务队列(task queue)通常分为两种类型:宏任务队列和微任务队列。在 JavaScript 中,宏任务和微任务会被分别添加到不同的队列中,然后按照先进先出的顺序执行。即新任务会被添加到队列的末尾,而任务的执行顺序是按照添加的顺序依次执行,只有异步代码才会进入任务队列。

在执行一个宏任务时,如果它中途产生了微任务,那么这些微任务会被添加到微任务队列中,等待当前宏任务执行完成后依次执行。当所有微任务都执行完成后,才会从宏任务队列中取出下一个任务执行。

例如,以下代码中先执行console.log('1'),然后将 setTimeout() 的回调函数添加到宏任务队列中。此时执行 Promise.resolve().then() 的代码时,会将其添加到微任务队列中。等到当前宏任务执行完成后,先执行微任务队列中的 Promise 的 then() 方法,然后再执行下一个宏任务中的 setTimeout() 回调函数。

console.log('1');
setTimeout(function() {
  console.log('2');
}, 0);
Promise.resolve().then(function() {
  console.log('3');
});

五、调用栈

调用栈(call stack)也叫执行栈,是 JavaScript 运行时用于存储函数调用的数据结构(栈先进后出),它记录了当前执行的上下文(context)和函数调用链

当 JavaScript 引擎执行一个函数时,它会将函数的调用信息添加到调用栈的顶部,并在执行完该函数后从调用栈中弹出该函数的信息。如果该函数调用了其他函数,那么这些函数也会依次被添加到调用栈中,并在执行完后弹出。在函数执行期间,调用栈会保持不断增长和收缩的状态。

调用栈的作用是跟踪函数的执行过程,并确保每个函数按照正确的顺序被调用和执行。所有函数想要执行就必须经过执行栈,因为执行栈可以理清词法环境、变量环境、词法作用域等。

function bar() {
  return 2;
}
function foo() {
  return bar();
}
function main() {
  console.log(foo());
}
main();

在以上的代码示例中,展示了调用栈的基本工作原理。在执行 main() 函数时,它会调用 foo() 函数,将 foo() 函数的调用信息添加到调用栈中。然后,在 foo() 函数中,又调用了 bar() 函数,将 bar() 函数的调用信息也添加到调用栈中。最后,在 bar() 函数中返回结果后,将 bar() 函数的调用信息从调用栈中弹出。接着执行 foo() 函数返回结果,再将 foo() 函数的调用信息从调用栈中弹出。最后,main() 函数也执行完毕,调用栈为空。

六、事件循环机制及其工作原理

事件循环是JavaScript执行上下文中的一种机制,用于处理异步操作。它的核心思想是将所有的异步任务放入一个队列中,然后按照队列中的顺序依次执行,直到队列为空为止。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

事件循环的工作原理可以用以下步骤来概括:

  • 1.先执行同步代码,所有同步代码都在主线程上执行,形成一个执行栈
  • 2.当遇到异步任务时,会将其挂起并添加到任务队列中,宏任务放入宏任务队列,微任务放进微任务队列
  • 3.当执行栈为空时,事件循环从任务队列中取出一个任务,加入到执行栈中执行
  • 4.重复上述步骤,直到任务队列为空

七、代码示例

接下来,我们通过代码示例来更好地理解事件循环的工作原理。

1.示例一

console.log('1')
setTimeout(() => {
  console.log('2')
}, 1000)
console.log('3')  //1 3 2

举一个比较经典的例子,在上面的代码示例中,首先输出1,然后调用 setTimeout()函数,但是1秒钟后才执行,所以先输出 3,最后输出2,那么学了Event Loop事件循环机制后,有什么更好的解释呢?

console.log('1')
setTimeout(() => {
  console.log('2')
}, 0)
console.log('3')  //1 3 2

虽然这里延迟时间设置的是0毫秒,但是HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。
而且setTimeout()是异步代码,是宏任务,被挂起(也就是不放入执行栈中),回调函数会添加到任务队列中。由于事件循环机制,这个回调函数不会立即执行,而是要等到执行栈为空时才会执行。因此,输出3后,事件循环机制开始执行任务队列中的回调函数,输出2

2.示例二

一个稍微复杂、综合的例子,其中微任务队列的顺序是【async333 endPromise111async111 endPromise222】。只有then执行了,才会返回promise对象,后面接的then才会执行,才会把then的回调函数放入微任务队列。

async function async1() {
  console.log(111); //1
  await async2(); //await会阻塞它下一行的代码
  await async3(); //异步代码中的微任务1,先挂起
  console.log('async111 end');//7 微任务3 等async3执行完毕后进入任务队列
}
async function async2() {
  console.log('async222 end'); //2
}
async function async3() {
  console.log('async333 end'); //5
}
async1();
setTimeout(() => { //异步代码中的宏任务1,先挂起
  console.log('setTimeout'); //9
}, 0)
new Promise(resolve => {
  console.log('Promise'); //3
  resolve()
})
.then(() => { //异步代码中的微任务2,先挂起
  console.log('Promise111'); //6
})
.then(() => { //异步代码中的微任务4,先挂起  前一个.then的回调函数执行后被进入任务队列
  console.log('Promise222'); //8
})
console.log('end'); //4
// 111
// async222 end
// Promise
// end
// async333 end
// Promise111
// async111 end
// Promise222
// setTimeout

3.示例三

再来看一个有意思的例子,因为它每次的执行结果都不相同,但是可以分为五类,其中xxx表示输出的随机值。只有Promise成功执行,后面接的then方法里的回调函数才会执行,否则在catch()方法中打印错误信息。

  1. 1、xxx、2、error
  2. 1、xxx、2、succeed-1、1、xxx、error
  3. 1、xxx、2、succeed-1、1、xxx、succeed-2、1、xxx、error
  4. 1、xxx、2、succeed-1、1、xxx、succeed-2、1、xxx、succeed-3、1、xxx
  5. 1、xxx、2、succeed-1、1、xxx、succeed-2、1、xxx、succeed-3、1、xxx、error
function executor(resolve, reject) {
  let rand = Math.random();
  console.log(1)
  console.log(rand)
  if (rand > 0.5)
      resolve()
  else
      reject()
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
  console.log("succeed-1")
  return new Promise(executor)
})
var p3 = p1.then((value) => {
  console.log("succeed-2")
  return new Promise(executor)
})
var p4 = p3.then((value) => {
  console.log("succeed-3")
  return new Promise(executor)
})
p4.catch((error) => {
  console.log("error")
})
console.log(2)

八、总结

总之,事件循环是JavaScript中非常重要的一个概念,用于处理异步任务和事件。了解事件循环机制对于成为一名优秀的JavaScript开发人员至关重要。希望本文能够帮助你更好地理解JavaScript事件循环机制的工作原理。

能力一般,水平有限,如有问题欢迎指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝生活愉快!

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!