深入理解JavaScript的事件执行机制

目录
  • 前言
  • 浏览器 JS 异步执行的原理
  • 浏览器中的事件循环
    • 执行栈与任务队列
    • 宏任务和微任务
  • Async/await的运行顺序
    • 特点
    • 示例
    • 个人分析

前言

熟悉事件循环,了解浏览器运行机制将对我们理解 JavaScript 的执行过程和排查运行问题有很大帮助。以下是总结的一些浏览器事件循环的一些原理和示例。

浏览器 JS 异步执行的原理

  • JS 是单线程的,也就是同一个时刻只能做一件事情,那么,为什么浏览器可以同时执行异步任务呢?
  • 因为浏览器是多线程的,当 JS 需要执行异步任务时,浏览器会另外启动一个线程去执行该任务。
  • 也就是说,JS 是单线程的指的是 执行JS 代码的线程只有一个,是浏览器提供的 JS 引擎线程(主线程)。浏览器中还有定时器线程和 HTTP 请求线程等,这些线程主要不是来跑 JS 代码的。
  • 比如主线程中需要发一个 AJAX 请求,就把这个任务交给另一个浏览器线程(HTTP 请求线程)去真正发送请求,待请求回来了,再将 callback 里需要执行的 JS 回调交给 JS 引擎线程去执行。
  • 即浏览器才是真正执行发送请求这个任务的角色,而 JS 只是负责执行最后的回调处理。所以这里的异步不是 JS 自身实现的,其实是浏览器为其提供的能力。

浏览器中的事件循环

执行栈与任务队列

JS 在解析一段代码时,会将同步代码按顺序排在某个地方,即执行栈,然后依次执行里面的函数。
遇到异步任务时就交给其他线程处理,待当前执行栈所有同步代码执行完成后,会从一个队列中去取出已完成的异步任务的回调加入执行栈继续执行。
遇到异步任务时又交给其他线程,.....,如此循环往复。
而其他异步任务完成后,将回调放入任务队列中待执行栈来取出执行。

宏任务和微任务

根据任务的种类不同,可以分为微任务(micro task)队列和宏任务(macro task)队列。
事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列是否有任务需要执行,如果没有,再去宏任务队列检查是否有任务执行,如此往复。
微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环,因此,微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个。另外我们常见的点击和键盘等事件也属于宏任务。

常见宏任务:

  • setTimeout()
  • setInterval()
  • UI交互事件
  • postMessage
  • setImmediate() -- nodeJs

常见微任务:

  • promise.then()、promise.catch()
  • new MutaionObserver()
  • process.nextTick() -- nodeJs

如下示例:

console.log('同步代码1');
setTimeout(() => {
    console.log('setTimeout')
}, 0)

new Promise((resolve) => {
  console.log('同步代码2')
  resolve()
}).then(() => {
    console.log('promise.then')
})
console.log('同步代码3');

// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"

具体分析如下:

  • setTimeout 回调和 promise.then 都是异步执行的,将在所有同步代码之后执行;
  • 虽然 promise.then 写在后面,但是执行顺序却比 setTimeout 优先,因为它是微任务;
  • new Promise 是同步执行的,promise.then 里面的回调才是异步的。

注意: 在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms。
微任务和宏任务的本质区别:

  • 宏任务特征:有明确的异步任务需要执行和回调;需要其他异步线程支持。
  • 微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持。

Async/await的运行顺序

特点

  • async声明的函数只是把该函数的return包装了,使得无论如何都会返回promise对象(非promise会转化为promise{resolve})。
  • await声明只能用在async函数中。
    • 当执行async函数时,遇到await声明,会先将await后面的内容按照'平常的执行规则'执行完(此处注意,当执行函数内容中又遇到await声明,此时接着执行该await声明内容)。
    • 执行完后立马跳出async函数,去执行主线程其他内容,等到主线程执行完再回到await处继续执行后面的内容。

示例

const a = async () => {
  console.log("a");
  await b();
  await e();
};

const b = async () => {
  console.log("b start");
  await c();
  console.log("b end");
};

const c = async () => {
  console.log("c start");
  await d();
  console.log("c end");
};

const d = async () => {
  console.log("d");
};

const e = async () => {
  console.log("e");
};

console.log('start');
a();
console.log('end');

运行结果

个人分析

  • 在当前同步环境中,先执行console.log('start');输出'start'。
  • 遇到同步函数a(),执行a()。
  • a()是sync/await构造,函数内遇到console.log("a") 输出 'a',遇到await声明 await b(),为异步函数,进入函数b()内执行。(类似的操作,我自己想的)并将 await b()后的内容推入微任务队列中。我们可以记做[await b()后]。
  • b()是sync/await构造,顺序执行遇到console.log("c start") 输出 'c start',遇到await声明 await c(),为异步函数,进入函数c()内执行。并将 await c()后的内容推入微任务队列中。我们可以记做[await c()后, await b()后]。
  • c()是sync/await构造,顺序执行遇到console.log("b start") 输出 'b start',遇到await声明 await d(),为异步函数,进入函数d()内执行。并将 await d()后的内容推入微任务队列中。我们可以记做[await d()后, await c()后, await b()后]。
  • d()中,顺序执行遇到console.log("") 输出 'd',d()函数运行结束。
  • 这是执行完 d()后,无可执行异步函数,此时进入同步环境,执行 a()后的内容。
  • 遇到console.log("end") 输出 'end'。此时同步环境中主线程执行完,检查微任务队列是否有微任务。
  • 微任务队列中[await d()后, await c()后, await b()后]有微任务,执行await d()后的内容。
  • wait d()后的内容是,console.log("c end"), 输出 'c end'。此时内容执行完毕,再从微任务队列中[await c()后, await b()后]检查,执行await c()后的内容。
  • 执行await c()后的内容,console.log("b end");, 输出 'b end'。此时内容执行完毕,再从微任务队列中[await b()后]检查,执行await b()后的内容。
  • await d()后的内容是,await e(),遇到await声明,执行e()。并判断并将 await e()后无运行代码,不用入微任务队列。
  • 执行e(),顺序执行console.log("e");,输出 'e'。此时函数结束。
  • 微任务队列中[]无微任务,执行结束。进入同步环境。

到此这篇关于深入理解JavaScript的事件执行机制的文章就介绍到这了,更多相关JavaScript 事件执行机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript 运行机制详解再浅谈Event Loop

    目录 一.为什么JavaScript是单线程? 二.任务队列 三.事件和回调函数 四.Event Loop 五.定时器 六.Node.js的Event Loop 一.为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. JavaScript的单线程,与它的用途有关.作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM.这决定了它只能是单线

  • 简单谈谈JavaScript变量提升

    目录 前言 1. 什么变量提升? 2. 为什么会有变量提升? (1)提高性能 (2)容错性更好 3. 变量提升导致的问题 (1)变量被覆盖 (2)变量没有被销毁 4. 禁用变量提升 5. JS如何支持块级作用域 (1)创建执行上下文 (2)执行代码 6. 暂时性死区 总结 前言 在 ECMAScript6 中,新增了 let 和 const 关键字用来声明变量.在前端面试中也常被问到 let.const和 var 的区别,这就涉及到了变量提升.暂时性死区等知识点.下面就来看看什么是变量提升和暂时

  • JavaScript 上传文件限制参数案例详解

    项目场景: 1,上传文件限制 功能作用: 1,防止前端操作上传异常文件 2,限制符合的规则,优化展示模型 功能实现: 1,获取file实例 2,执行校验规则方法 代码如下: //大小限制 checkFileSize(file, rules) { return new Promise((resolve, reject) => { file.size / 1024 / 1024 > rules ? reject() : resolve() }).then( () => { return tr

  • JavaScript实现飞机大战游戏

    本文实例为大家分享了canvas ,js 实现一个简单的飞机大战,供大家参考,具体内容如下 预览图: 代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport"

  • Python 协程与 JavaScript 协程的对比

    目录 1.前言 2.什么是协程? 3.混乱的历史 3.1 Python 协程的进化 4.JavaScript 协程的进化 5.Python 协程成熟体 5.1 协程(coroutine) 5.2 任务(Task 对象) 5.3 未来对象(Future) 5.4几种事件循环(event loop) 6.JavaScript 协程成熟体 6.1Promise 继续使用 6.2 async.await语法糖 6.3 js 异步执行的运行机制 6.4 event loop 将任务划分 7.总结与对比 1

  • 8个工程必备的JavaScript代码片段

    目录 1. 获取文件后缀名 2. 复制内容到剪贴板 3. 休眠多少毫秒 4. 生成随机字符串 5. 简单的深拷贝 6. 数组去重 7. 对象转化为FormData对象 8.保留到小数点以后n位 1. 获取文件后缀名 使用场景:上传文件判断后缀名 /** * 获取文件后缀名 * @param {String} filename */ export function getExt(filename) { if (typeof filename == 'string') { return filena

  • JavaScript 数组去重详解

    目录 1.数组去重 2.数组去重里面的对象去重 3.根据数组某个字段相同,修改另外字段值 总结 1.数组去重 /********************************************** ╚description: ╚作者: 麒麟社 ╚时间: 2021-09-13 22:26:21 ╚名称: V1.0.5 ***********************************************/ var obj = ['麒麟','社','CC','DD','麒麟','社'

  • Javascript基础知识中关于内置对象的知识

    目录 1.内置对象介绍 1.1 Math对象 1.2 Math中的方法 1.3 Date对象 2.Date中的方法 3.经典案例:倒计时效果: 4.Array数组对象 4.1 数组的创建 4.2 数组中的常用方法 5.字符串String 1.内置对象介绍 JavaScript组成: ECMAScript | DOM | BOM ECMAScript: 变量 , 函数, 数据类型 ,流程控制,内置对象 js中的对象: 自定义对象 , 内置对象 , 浏览器对象(不属于ECMAScript) 1.1

  • JavaScript实现经典贪吃蛇游戏

    本文实例为大家分享了JavaScript实现经典贪吃蛇游戏的具体代码,供大家参考,具体内容如下 主要使用单例模式,所有元素动态创建: 1.创建地图 var Map; function map(){ this.mapp=null; this.makemap=function(){ this.mapp=document.createElement ("div"); this.mapp.className ="mymap"; document.body.appendChi

  • JavaScript setinterval延迟一秒解决方案

    当使用setinterval时,发现它刚打开页面时会延迟一秒过后在执行.因为setinterval定时器先执行了自己的一秒钟,执行过后在对里面的内容进行操作,这样就会导致不能立即显示出来 举个例子:先创建一个div盒子,然后写script代码 var div = document.querySelector('div'); var num = 10; setInterval(function(){ if(num==1){ div.innerHTML = null; return fn1; }el

  • JavaScript中常用的数组操作方法

    目录 一.concat() 二.join() 三.push() 五.shift() 六.unshift() 七.slice() 八.splice() 九.substring() 和 substr() 十.sort 排序 十一.reverse() 十二.indexOf 与 lastIndexOf 十三.every 对数组 十四.some 十五.filter 十六.map 十七.forEach 数组遍历 1.find(): 2.findIndex(): 3.fill(): 4.copyWithin(

  • javascript实现鼠标拖尾特效

    鼠标特效需要使用定时器setTimeout在固定时间生成节点,删除节点,生成的节点赋予随机的宽高,随机颜色,使每个特效节点都看起来不一样 注意:生成的节点需要设置绝对定位,使其脱离文档流,不影响页面中其他元素的位置 代码示例: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Com

  • 深入浅出探究JavaScript中的async与await

    目录 1.前言 2.详解 2.1.async 2.1.1.函数返回非Promise对象 2.1.2.函数返回Promise对象 2.2.await 2.3.async.await结合使用 2.4.async.await异常处理 3.总结 1.前言 ​ async函数,也就是我们常说的async/await,是在ES2017(ES8)引入的新特性,主要目的是为了简化使用基于Promise的API时所需的语法.async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,

  • JavaScript实现简单拖拽效果

    本文实例为大家分享了JavaScript实现简单拖拽效果的具体代码,供大家参考,具体内容如下 先看实现的效果: 思路:里面用到了三个事件,鼠标按下.移动.松开事件 那么首先创建盒子并且给它赋予css样式 HTML: // html <div> <p>我是个蓝色的盒子</p> </div> CSS: CSS *{margin: 0;padding: 0;} div{ width: 100px; height: 100px; background-color:

随机推荐