我终于搞懂了Javascript事件循环机制😀

简介: 我终于搞懂了Javascript事件循环机制😀

前言🌞


  • 一天 我在敲着代码
  • 突然经理说加个需求让UI和后端给我扔了原型图和接口  我说可以但是先把手头的码完
  • 好 等我码完了去看原型图 这时候又加了需求又有原型图和接口

我以上的执行顺序应该是怎么样的呢?

本文最后将通过下面这个贴近生活的案例分析js事件循环机制,如果不想重复查看理论知识可以直接移步到文章最后喔~


为什么 Javascript 要是单线程的 ❓


  • 我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互,也就是说javascript在处理任务的时候,所有任务只能在一个线程上排队被执行,试想一下如果javascript是多线程的,当我们用javascript操作DOM的时候,一个线程来控制增加另一个来控制删除,这时浏览器应该听哪个线程的,如何判断优先级?
  • 为了避免这样的问题,Javascript在最初就选择了单线程执行。


执行栈与事件队列🍜


执行栈

  • 当一个js文件执行的时候,会产生一个对应的执行环境也叫执行上下文。
  • 每调用一个函数会产生一个新的执行上下文。
  • 而当一系列函数被依次调用的时候,因为javascript是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方,这个地方被称为执行栈
  • 全局执行上下文会先放入栈中,然后执行一个函数时会把这个上下文放进栈中,然后进入函数中进行执行,遇到新的函数执行会把新的执行上下文再次放入栈中,然后执行函数里面的代码,知道代码执行完毕,会把对应执行上下文(出栈)销毁,等待垃圾回收。

比如这个例子:

console.log('我是第一个');
function caseFn() {
    console.log('我在case里面的上下文');
    sthFn()
    console.log('sth执行完才到我');
}
function sthFn(){
    console.log('我在sth里面的上下文');
}
caseFn();
console.log('最后才到我');
//输出:我是第一个  我在case里面的上下文  我在sth里面的上下文 sth执行完才到我 最后才到我


事件队列

  • 如果说Javascript是单线程的,那么如果某一个任务耗时特别长怎么办呢?总不能让他一直等待到执行完才执行下一步吧?
  • 上文提到Javascript的另一大特点是非阻塞,js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。
  • 当一个异步事件返回结果后,Javascript会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。
  • 被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码。

比如这个例子:

console.log('我是第一个');
function caseFn() {
    console.log('我在case里面的上下文');
}
setTimeout(function() {
    console.log('最后是我啊!')
}, 0)
caseFn();
console.log('最后才到我?');
//输出:我是第一个  我在case里面的上下文   最后才到我? 最后是我啊!

 image.png这执行栈与事件队列配合的整个过程叫做事件循环,但实际上会有这么简单吗?


宏任务与微任务👻

  • 至此之前我认为的事件循环一直以为只有执行栈和事件队列两层,当遇到异步就放到队列最后执行,但是实际上还有第三层。
  • 也就是说实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:宏任务(macro task)和微任务(micro task)。
  • 在最新标准中,它们被分别称为 taskjobs
  • macro-task(宏任务) 大概包括: setTimeout, setInterval, setImmediate(NodeJs), I/O, UI rendering
  • micro-task(微任务) 大概包括: process.nextTick(NodeJs), Promise, Object.observe(已废弃), MutationObserver(html5新特性)


事件循环机制(EventLoop)🚀

  • 在上文中我们讲过,在一个事件循环中,先执行同一个上下文中同步的代码,遇到异步事件返回结果后会被放到一个事件队列中。
  • 然而这个事件队列里面也是有优先级的,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。
  • 这个微任务队列是存在于放入执行队列之后,事件队列之前的。
  • 首先,事件循环整体代码script入栈开始。
  • 当执行栈执行完毕他会先查看微任务队列有没有可执行的事件,如果没有则去宏任务队列中取出一个事件并把对应的回到加入当前执行栈。
  • 如果有,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈。
  • 如此重复就是js事件的循环,也就是说当当前执行栈执行完毕没有东西可以跑了-->他就去找微任务-->再去找事件队列队列中的宏任务-->然后在宏任务中如果有异步事件则根据类别又放在不同的宏任务队列微任务队列--->反复循环直到没有事件可以执行。
  • 只要记住在同一次事件循环中,微任务永远在宏任务之前执行,即先清空微任务

举个栗子🌰


  • 想必如果第一次看这个概念的小伙伴肯定看不明白(我以前也是这样的(手动狗头)),没关系接下来用几个栗子来简单操作下。


简单的例子:

console.log('1');
setTimeout(function() {
    console.log('2')
}, 1000)
new Promise(resolve => {
    console.log('3')
    resolve(5);
}).then(t => {
    console.log(t)
});
console.log('4');
  • 首先我们进入执行栈,遇到同步代码打印1
  • 遇到异步事件setTimeout的回调要放到任务队列里面的宏任务队列

此时宏任务队列:setTimeout事件,微任务队列:无事件

  • 遇到异步事件PromisePromise构造函数中的第一个参数,是在new的时候执行,构造函数执行时,里面的参数进入执行栈执行;而后续的.then则会被分发到微任务队列中去。所以会先打印3,然后执行resolve,将then的回调分配到对应微任务队列

此时宏任务队列setTimeout事件,微任务队列then的回调

  • 遇到同步代码打印4
  • 第一次循环的执行栈没有代码就去找微任务队列,发现有个then的回调,所以打印5
  • 当所有的微任务执行完毕之后,表示第一轮的循环就结束了
  • 开始第二轮的循环,找到宏任务队列有个setTimeout,打印2
  • 最后的结果是 1 3 4 5 2


什么?没听明白 那我换种说法👀

console.log('完成功能1')
//这时经理突然提了需求让UI和后端分别给了设计图(Promise)和接口(setTimeout)
setTimeout(function() {
    console.log('对接接口代码')
}, 1000)
new Promise(resolve => {
    console.log('查看设计图但没写代码')
    resolve('根据设计图写布局代码');
}).then(t => {
    console.log(t)
});
console.log('完成功能2')
  • 有一天兴高采烈写着‘BUG’,很顺利一开始就完成了功能1(打印完成功能1)
  • 经理突然走过来说:'来来来,刚定了个需求我让UI和后端给你原型图和接口,你找时间完成它'
  • 后端先给我了个接口文档(这里指setTimeout),我心想我设计图都没拿到呢看接口浪费时间呀我先把工作完成,然后就等着稍后再看接口文档
  • 这时候UI给了我设计图(这里指Promise),我心想,万一要修改的页面和我现在在做的页面冲突就不好了,我先看一眼待会再切图(resolve),这时打印(查看设计图但没写代码)
  • 看完设计图发现跟现在的没啥关系嘛然后就继续完成之前的需求(完成功能2)
  • 好啦现在之前的需求都做完了,那我先看看新需求吧,找出设计图开始切图(执行then的回调,打印根据设计图写布局代码)
  • 切完图拉就要去对接接口了,找回之前后端发的文档开始对接(对接接口代码)
  • 结果很快很顺利完成了所有需求,经理很开心又奖励了一个需求....

以上比喻可能不那么恰当,但是应该可以帮助理解一下这个机制

什么?你觉得简单了,那好吧我再举一个例子👂

难一点点的例子

console.log('完成功能1')
//这时经理突然提了需求页面1让UI和后端分别给了原型图(Promise)和接口(setTimeout)
setTimeout(function() {
    //接口文档1
    console.log('对接接口文档1')
    new Promise(function(resolve) {
        // 设计图2
        console.log('查看设计图2但不切图')
        resolve()
    }).then(function() {
        console.log('根据设计图2完成布局')
    })
    }, 0)
new Promise(function(resolve) {
    //设计图1
    console.log('查看设计图1但不切图')
    resolve()
}).then(function() {
    console.log('根据设计图1完成布局')
    //经理又提了需求页面2让后端给了接口文档2
    setTimeout(function() {
        // 接口文档2
        console.log('对接接口文档2')
    })
})
console.log('完成功能2')
  • 有一天兴高采烈写着‘BUG’,很顺利一开始就完成了功能1(打印完成功能1)
  • 经理突然走过来说:'来来来,刚定了个需求我让UI和后端给你原型图和接口,你找时间完成它'
  • 后端先给我了个接口文档1(这里指第一个setTimeout),我心想我设计图都没拿到呢看接口浪费时间呀我先把工作完成,然后就等着稍后再看接口文档1
  • 这时候UI给了我设计图(这里指遇到的第一个Promise),我心想,万一要修改的页面和我现在在做的页面冲突就不好了,我先看一眼待会再切图(resolve),这时打印(查看设计图1但不切图)
  • 看完设计图发现跟现在的没啥关系嘛然后就继续完成之前的需求(打印完成功能2)
  • 好啦现在之前的需求都做完了,那我先看看新需求吧,找出设计图1开始切图(执行then的回调,打印根据设计图1完成布局)
  • 好家伙,正当我为页面1的布局苦恼时,经理走过来说:'看你做挺快呀,再给你个功能帮忙做一下',就让后端人员先把接口文档2(这里指遇到的第二个setTimeout)给了过来
  • 我心想我页面1还没做完呢,待会再看接口文档2,我找回接口文档1开始对接(打印对接接口文档1)
  • 正当我快对接完成页面1接口时,UI把设计图2(这里指遇到的第二个Promise)给了我,我先看一眼(打印查看设计图2但不切图)
  • 这时候我完成了一开始的需求和新增的第一个需求,第二个需求的设计图和接口我都有了,我就根据设计图2开始切图(打印根据设计图2完成布局)
  • 最后再找到接口文档2开始对接(打印对接接口文档2)


以上就是举得一个小例子,可能不太恰当,还请大佬们鞭策


写在最后👋


  • 我们的JavaScript的执行过程是单线程的,所有的任务可以看做存放在两个队列中——执行队列事件队列
  • 执行队列里面是所有同步代码的任务,事件队列里面是所有异步代码的宏任务,而我们的微任务,是处在两个队列之间。
  • 当JavaScript执行时,优先执行完所有同步代码,遇到对应的异步代码,就会根据其任务类型存到对应队列(宏任务放入事件队列微任务放入执行队列之后,事件队列之前);当执行完同步代码之后,就会执行位于执行队列和事件队列之间的微任务,然后再执行事件队列中的宏任务,这就是JS事件循环机制。
  • 如果您觉得这篇文章有帮助到您的的话不妨点赞支持一下哟~~😛
  • 这是我在学习之后,根据自己的想法理解所写出来的,难免会存在错误,各位大佬可以鞭策!


参考


详解JavaScript中的Event Loop(事件循环)机制

从一道题浅说 JavaScript 的事件循环


相关文章
|
3月前
|
存储 JavaScript 前端开发
深入理解JavaScript中的事件循环(Event Loop):机制与实现
【10月更文挑战第12天】深入理解JavaScript中的事件循环(Event Loop):机制与实现
126 3
|
2月前
|
JavaScript 前端开发 API
深入理解Node.js事件循环及其在后端开发中的应用
本文旨在揭示Node.js的核心特性之一——事件循环,并探讨其对后端开发实践的深远影响。通过剖析事件循环的工作原理和关键组件,我们不仅能够更好地理解Node.js的非阻塞I/O模型,还能学会如何优化我们的后端应用以提高性能和响应能力。文章将结合实例分析事件循环在处理大量并发请求时的优势,以及如何避免常见的编程陷阱,从而为读者提供从理论到实践的全面指导。
|
4月前
|
JavaScript 安全 前端开发
乾坤js隔离机制
乾坤js隔离机制
|
2月前
|
JavaScript API 开发者
深入理解Node.js中的事件循环和异步编程
【10月更文挑战第41天】本文将通过浅显易懂的语言,带领读者探索Node.js背后的核心机制之一——事件循环。我们将从一个简单的故事开始,逐步揭示事件循环的奥秘,并通过实际代码示例展示如何在Node.js中利用这一特性进行高效的异步编程。无论你是初学者还是有经验的开发者,这篇文章都能让你对Node.js有更深刻的认识。
|
2月前
|
JavaScript 安全 中间件
深入浅出Node.js中间件机制
【10月更文挑战第36天】在探索Node.js的奥秘之旅中,中间件的概念如同魔法一般,它让复杂的请求处理变得优雅而高效。本文将带你领略这一机制的魅力,从概念到实践,一步步揭示如何利用中间件简化和增强你的应用。
|
2月前
|
JavaScript 前端开发 开发者
JavaScript的事件循环
【10月更文挑战第27天】理解JavaScript的事件循环机制对于正确编写和理解JavaScript中的异步代码至关重要,它是JavaScript能够高效处理各种异步任务的关键所在。
40 1
|
2月前
|
消息中间件 JavaScript 中间件
深入浅出Node.js中间件机制
【10月更文挑战第24天】在Node.js的世界里,中间件如同厨房中的调料,为后端服务增添风味。本文将带你走进Node.js的中间件机制,从基础概念到实际应用,一探究竟。通过生动的比喻和直观的代码示例,我们将一起解锁中间件的奥秘,让你轻松成为后端料理高手。
38 1
|
2月前
|
Web App开发 JSON JavaScript
Node.js 中的中间件机制与 Express 应用
Node.js 中的中间件机制与 Express 应用
|
3月前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
45 1
|
3月前
|
Web App开发 JavaScript 前端开发
深入理解Node.js事件循环和异步编程模型
【10月更文挑战第9天】在JavaScript和Node.js中,事件循环和异步编程是实现高性能并发处理的基石。本文通过浅显易懂的语言和实际代码示例,带你一探究竟,了解事件循环的工作原理及其对Node.js异步编程的影响。从基础概念到实际应用,我们将一步步解锁Node.js背后的魔法,让你的后端开发技能更上一层楼!