JS的执行原理,一文了解Event Loop事件循环、微任务、宏任务
面试官:你了解JavaScript事件循环吗,掌握多少,把你知道的都说一下。
今天我们就来说一下,JavaScript作为一门单线程语言,如何通过事件循环(Event Loop)和任务队列(Task Queue)的机制,高效地处理异步任务,保证用户体验的流畅性。在本文中,我们将详细探讨事件循环、任务队列,以及在实际开发中的一些应用场景(面试常见笔试题)。
在事件循环中,当主线程执行完当前的同步任务后,会检查事件队列中是否有待处理的事件。如果有,主线程会取出事件并执行对应的回调函数。这个循环的过程被称为事件循环(Event Loop),它由主线程和任务队列两部分组成。主线程负责执行同步任务,而异步任务则通过任务队列进行处理。这种机制保证了异步任务在适当的时机能够插入执行,从而实现了JavaScript的非阻塞异步执行。
事件循环流程如下:
- 主线程读取JavaScript代码,形成相应的堆和执行栈。
- 当主线程遇到异步任务时,将其委托给对应的异步进程(如Web API)处理。
- 异步任务完成后,将相应的回调函数推入任务队列。
- 主线程执行完同步任务后,检查任务队列,如果有任务,则按照先进先出的原则将任务推入主线程执行。
- 重复执行以上步骤,形成事件循环。
同步任务是按照代码的书写顺序一步一步执行的任务。当主线程执行同步任务时,会阻塞后续的代码执行,直到当前任务执行完成。典型的同步任务包括函数调用、变量赋值、算术运算等。例如:
在上面的例子中,console.log(\’Step 1\’) 执行完毕后才会执行函数调用 add(2, 3),并等待 add 函数返回结果后才会继续执行后续代码。
异步任务是在主线程执行的同时,通过回调函数或其他机制委托给其他线程或事件来处理的任务。在执行异步任务时,主线程不会等待任务完成,而是继续执行后续代码。包括:
- 回调函数 callback
- Promise/async await
- Generator
- 事件监听
- 发布/订阅
- 计时器
- requestAnimationFrame
- MutationObserver
- process.nextTick
- I/O操作
不得不说,异步执行的机制使得 JavaScript 能够更好地处理耗时操作,保持页面的响应性。
在上述例子中,setTimeout 是一个异步任务,它会在1秒后将回调函数推入任务队列,而主线程不会等待这个1秒,而是继续执行后面的 console.log(\’End\’)。当主线程的同步任务执行完成后,它会检查任务队列,将异步任务的回调函数推入执行栈,最终输出 \’Timeout callback\’。
上面我们讨论了同步任务和异步任务的执行过程,接下来我们将进一步探讨任务队列,了解它的最小颗粒度是如何执行的。
任务队列分为宏任务队列(macrotask queue)和微任务队列(microtask queue)两种。JavaScript 引擎遵循事件循环的机制,在执行完当前宏任务后,会检查微任务队列,执行其中的微任务,然后再取下一个宏任务执行。这个过程不断循环,形成事件循环。
1、宏任务(Macrotasks)是一些较大粒度的任务,包括:
- 所有同步任务
- I/O操作,如文件读写、数据库数据读写等
- setTimeout、setInterval
- setImmediate(Node.js环境)
- requestAnimationFrame
- 事件监听回调函数等
- …
2、微任务(Microtasks)是一些较小粒度、高优先级的任务,包括:
- Promise的then、catch、finally
- async/await中的代码
- Generator函数
- MutationObserver
- process.nextTick(Node.js 环境)
- …
首先,必须要明确,在JavaScript中,所有任务都在主线程上执行。任务执行过程分为同步任务和异步任务两个阶段。异步任务的处理经历两个主要阶段:Event Table(事件表)和 Event Queue(事件队列)。
Event Table存储了宏任务的相关信息,包括事件监听和相应的回调函数。当特定类型的事件发生时,对应的回调函数被添加到事件队列中,等待执行。例如,你可以通过addEventListener来将事件监听器注册到事件表上:
微任务与 Event Queue 密切相关。当执行栈中的代码执行完毕后,JavaScript引擎会不断地检查事件队列。如果队列不为空,就将队列中的事件一个个取出,并执行相应的回调函数。
任务队列的执行流程可概括为:
- 同步任务在主线程排队执行,异步任务在事件队列排队等待进入主线程执行。
- 遇到宏任务则推进宏任务队列,遇到微任务则推进微任务队列。
- 执行宏任务,执行完毕后检查当前层的微任务并执行。
- 继续执行下一个宏任务,执行对应层次的微任务,直至全部执行完毕。
这个流程确保了异步任务能够在适当的时机插入执行,保持程序的高效性和响应性。
如果看到这里,还觉得有点懵,我们不妨看看下面这个示例解析,一定会让你清晰明了!!!
执行顺序解析:1 => 3 => 4 => 5 => 7 => 6 => 2。
- 创建Promise实例是同步的,所以1、3、4、5、7是同步执行的。
- then方法是微任务,放入微任务队列中,在当前脚本执行完毕后立即发生。
- 同步任务执行完毕后,执行微任务队列中的微任务。
- 最后,setTimeout放入宏任务队列,按照先进先出的原则执行。
注意:出现async、await,等价于promise、then。
作者:Sailing链接:https://juejin.cn/post/7318619321421217832
Nice! 一图搞懂JS工作原理
找了资料,结合图文和整理。我来讲解介绍下JS的工作原理。
Javascript确实很牛X!是一门能够上天的编程语言!2019年,SpaceX 公司发射的“龙飞船”(Dragon)2 号将 JavaScript 技术带入了太空。(图转自bytebytego,翻译整理by dogstar)
和编译语言不同,JavaScript由浏览器或JavaScript引擎解释执行,例如V8引擎和JIT优化技术,而不是事先编译成机器语言。这样它就可以跨平台运行。
PS:我是看大佬这本书长大的。
ES6 入门教程 es6.ruanyifeng.com/
和面向对象编程OOP不同,在JavaScript世界里,函数反而是一等公民。这意味着函数可以被保存、作为参数传递,以及作为函数的结果返回。(PS:有兴趣可以继续了解下什么是 匿名函数、闭包函数、回调函数、高阶函数,面试时我总喜欢提的问题)。
既然作为一门动态语言,JS可以不必提前场景变量类型,统一用 var 或 let 或 const关键字即可,并且类型可以在运行过程中动态修改。
JavaScript支持异步编程、文件读取、HTTP请求,以及在后台查询数据库等操作,并在完成后触发回调或promises。在Web网站 开发中特别管用,可以提高性能和用户体验。
与基于类的面向对象语言不同,JavaScript使用原型进行继承。这意味着对象可以从其他对象继承属性和方法。(PS:像Ruby这种元编程更好玩,还可以动态给自己新增方法)
JavaScript会自动回收程序不再使用的对象占用的内存,防止内存泄漏并优化应用程序的性能。
与Python或Java等编程语言相比,JavaScript主要用于Web网站开发。
众所周知,Python提供了良好的代码可读性和多功能性,而Java以其结构和健壮性而闻名,JavaScript是一种无需编译即可直接在浏览器上运行的解释型语言,强调灵活性和动态性。
TypeScript 是JavaScript的超集,这意味着它通过向语言添加功能来扩展JavaScript,尤其是类型注释。这种关系允许任何有效的 JavaScript代码也被视为有效的TypeScript代码。(PS:有点像 Scala/Processing语言扩展了Java一样)
TypeScript 中文网
ts.nodejs.cn/
React 官网: React Native 以其灵活性和大量社区驱动的插件而闻名,而Vue https://cn.vuejs.org/ 则干净直观,具有高度集成和响应迅速的功能。另一方面,Angular Angular 中文文档 为企业级JS 开发提供了一套严格的开发规范。(PS:还有古老的jQuery,哈哈;还有Nodejs https://nodejs.org/en 算不算一个;UniApp uni-app官网 也很不错 )
带你学会js的执行上下文原理与本质
函数执行上下文的本质— 程序的本质是状态机,换句话说,程序的执行只干两件事情,就是对数据的读与写。而执行上下文就是程序查找变量的空间的一个抽象。
执行上下文,分为两种。全局的执行上下文(全局变量)和函数执行上下文(局部变量)。
Lexical Environment 是由全局对象、全局scope、outer 组成。
1.全局对象在浏览器环境是window对象;
2.let、const、class 记录在全局scope中
3.全局执行上下文中outer为null;
This Binding 就是当你使用this的时候,就是读取This Binding存储的值。全局执行上下文中,ThisBinding就是指全局对象
1.处理声明:找到所有的全局中的var声明、找到所有顶级全局函数声明。将其记录在全局对象中。
2.检查重复定义:就是看全局scope中有没有重复的定义,有则报错。但全局对象中可以重复定义
3.创建绑定:先var声明的变量被赋值为undefinend,在把函数声明会创建函数对象,然后让变量指向对象。这一步就不会对let、const、class 赋值。
全局执行上下文生成完毕,在执行代码。
当函数执行的前一刻,生成该函数的执行上下文,并压入栈顶,函数执行结束,则它的执行上下文弹出栈顶函数的执行上下文与全局执行上下文生成流程差不多,区别是:
·函数执行上下文中没有全局对象,Lexical Environment 由 outter和 scope 组成
·函数的 this Binding 默认是全局对象,严格模式下是 undefined,可以通过不同的调用方式更 this Binding 的值(具体后面再说)
全局执行上下文的 outter 是什么? 还记得吗,是null
函数执行上下文有所区别,它的值是,函数对象的 [[Scope]] 属性,这个 outter 的作用是什么呢?那就不得不说一下函数中查找变量的机制了:
在执行上下文中找不到变量时,则会 查找 outter 指向的执行上下文,这其实就是所谓的 作用域链 的底层机制。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。