为什么要EventLoop?

  JS 作为浏览器脚本语言,为了避免复杂的同步问题(例如用户操作事件以及操作DOM),这就决定了被设计成单线程语言,而且也将会一直保持是单线程的。而在单线程中若是遇到了耗时的操作(IO,定时器,网络请求)将会一直等待,CPU利用率将会大打折扣,时间大量浪费。所以需要设计一种方案让一些耗时的操作放在一边等待,让后面的函数先执行,于是有了EventLoop的设计。

  将任务分为两种:

  • 同步任务
  • 异步任务
  1. 定时器都是异步操作
  2. 事件绑定都是异步操作
  3. AJAX中一般采取的异步操作(虽然也可以同步)
  4. 回调函数(不严谨的异步)

 阮一峰老师《JavaScript 运行机制详解:再谈Event Loop》

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

  任务都会按顺序进入调用栈(call stack),即图1-1的stack,然后按栈的顺序依次执行。若全是同步任务,就会正常地顺序执行。当遇到异步任务时(其实就是执行到了一个耗时的任务,它发起后,需要它的回调函数等待拿到结果之后才继续进行)将会放到WebAPIs中(图1-1),等待这个耗时操作返回结果,也有网友把这个 WebAPIs 称之为 Event Table。如果异步任务在WebAPIs中等待有了结果(比如setTimeout的时间截止了,xhr得到响应结果了,用户click事件发生了),就会将这个结果作为一个事件置于任务队列中。 【或者称之为:注册回调函数】

  那么任务队列又是什么?个人认为就是图中的callback queue,或称之为 Event Queue 。就是存放了各种耗时操作最后响应结果的各个事件(说白了,就是已经拿到结果的,就会从WebAPIs放到任务队列里来)

图 1-1 转自Philip Roberts的演讲《Help, I'm stuck in an event-loop》

  搞懂上面两段话后,就可以谈EventLoop的作用了:

  • 在调用栈和任务队列之间进行“轮询”
  • 但轮询的规则是:只有每当调用栈为空,才能去“询问”任务队列中是否有事件需要处理
  • 若任务队列存在事件,则会将该事件相应的回调函数(异步操作)结束等待,置于调用栈中开始执行
  • 如果调用栈一直不为空,那就一直不会“询问”任务队列

  以上过程是不断循环的,js引擎中,存在一个叫monitoring process的进程,这个进程会不断的检查主线程的执行情况,一旦为空,就会去任务队列检查有哪些待执行的函数。这里的整个过程可以参考 一个工具 loupe 对整个调用过程进行查看。

  

图 1-2 loupe, 也是从其他地方发现的这个东西,很直观

  针对call stack调用栈多说一句:通俗地讲,将调用栈比喻为程序员,各个任务比喻为需求,任务队列比喻为总监。当总监提需求时,程序员就要交接需求过来,然后完成它。如果没有需求,就一直等待总监给需求。给了就做,不给就等。

  搞懂同步任务与异步任务的具体执行流程后,再谈谈为什么要设计宏任务和微任务。

 为什么有宏任务、微任务?

  页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么若只有一种类型的任务就不合适了,所以引入了微任务队列。

  至此,任务队列已被分为:

  • 宏任务队列,即上文说的任务队列,callback queue,用于存放宏任务
  • 微任务队列,再开辟一个队列,用于存放微任务

图 2-1 微任务Microtask Queue的加入

  首先列举一下哪些是宏任务、哪些是微任务

  宏任务

  • script(主代码)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI交互事件
  • setImmediate(Node.js)
  • requestAnimationFrame(浏览器)

  微任务

  • new Promise().then(回调)
  • MutationObserver(html5 新特性)
  • process.nextTick(Node.js)在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前

  紧接着第一节里说的EventLoop,当时没有考虑什么宏任务微任务,现在再加入微任务的概念再来考虑整个流程:

  1. 依旧是在调用栈和任务队列中轮询。(此时的任务队列指的是宏任务队列)
  2. 调用栈为空后,优先检查微任务队列,如果微任务队列中存在事件,则加入到调用栈中进行执行(为什么先询问的是微任务队列而不是宏任务队列,在后面解释)
    • 注:如果在执行微任务队列中的函数时,产生了新的微任务(比如then函数嵌套),则会继续在本次执行中执行(就是说如果期间一直有微任务产生,那就会永远卡在微任务队列执行)
  3. 如果微任务队列为空,那就取宏任务队列中的事件加入到调用栈中进行执行
  4. 若在执行宏任务的时候,产生了新的微任务,就会将该微任务加入到微任务队列,该微任务队列将会在下一次宏任务执行之前执行,如图2-2。
  5. 循环。

  依旧是:两个任务队列(宏、微)只有有任务,那么主进程的调用栈就会调过去执行,没有任务的话,主进程就一直等着,直到又有任务。

图 2-2 宏任务与微任务的执行顺序

  注意的是,图2-2看起来是宏任务先执行,微任务后执行,这仅仅是宏任务与微任务的先后次序,但不代表宏任务优先级比微任务高。事实是微任务的优先级是高于宏任务的。因为微任务其实是产生于宏任务的,不可能凭空产生微任务,也就不可能一开始就出现几个微任务。在本次宏任务产生微任务后,将会在下次宏任务执行之前,优先执行这些微任务。自然也就映证了设计微任务的初衷:为了让某些任务尽快执行。

  总结完整的EventLoop流程:

  1. 执行一个宏任务(调用栈中没有就从宏、微任务队列中获取)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 当前微任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  5. 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

  微任务在本次宏任务之后执行,在本次渲染之前执行,在下次宏任务之前执行。(宏任务 -> 微任务 -> 渲染 -> 宏任务)

 包含宏任务、微任务的异步代码分析:

// 知乎作者:Miku
// 链接:https://zhuanlan.zhihu.com/p/257069622
// 注意:代码中的process.netxTick 函数存在于Node.js中

console.log('1'); setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5');
});
});
process.nextTick(function() {
console.log('6');
});
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8');
}); setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
});
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12');
});
});

第一轮循环:

1)、首先打印 1
2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
3)、接下来是 process 微任务 加入微任务队列 记为 process1
4)、接下来是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务 记为 then1
5)、setTimeout 宏任务 记为 setTimeout2 第一轮循环打印出的是 1 7
当前宏任务队列:setTimeout1, setTimeout2
当前微任务队列:process1, then1,

第二轮循环:

1)、执行所有微任务
2)、执行process1,打印出 6
3)、执行then1 打印出8
4)、微任务都执行结束了,开始执行第一个宏任务
5)、执行 setTimeout1 也就是 第 3 - 14 行
6)、首先打印出 2
7)、遇到 process 微任务 记为 process2
8)、new Promise中resolve 打印出 4
9)、then 微任务 记为 then2 第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
当前宏任务队列:setTimeout2
当前微任务队列:process2, then2

第三轮循环:

1)、执行所有的微任务
2)、执行 process2 打印出 3
3)、执行 then2 打印出 5
4)、执行第一个宏任务,也就是执行 setTimeout2 对应代码中的 25 - 36 行
5)、首先打印出 9
6)、process 微任务 记为 process3
7)、new Promise执行resolve 打印出 11
8)、then 微任务 记为 then3 第三轮循环结束,当前打印顺序为:1 7 6 8 2 4 3 5 9 11
当前宏任务队列为空
当前微任务队列:process3,then3

第四轮循环:

1)、执行所有的微任务
2)、执行process3 打印出 10
3)、执行then3 打印出 12 代码执行结束:
最终打印顺序为:1 7 6 8 2 4 3 5 9 11 10 12

参考

JS中EventLoop、宏任务与微任务的个人理解的更多相关文章

  1. js中的宏任务与微任务

    如果你已经知道了js中存在宏任务和微任务,那么你一定已经了解过promise了.因为在js中promise是微任务的一个入口. 先来看一道题: setTimeout(function(){ conso ...

  2. javascript中的宏任务和微任务(一)

    一.宏任务和微任务有哪些 宏任务:setTimeout,setInterval,ajax,dom,宏任务是由浏览器提供的 微任务:promise,async/await,微任务是由es6提供的 二.微 ...

  3. Js中关于构造函数,原型,原型链深入理解

    在 ES6之前,在Javascript不存在类(Class)的概念,javascript中不是基于类的,而是通过构造函数(constructor)和原型链(prototype chains)实现的.但 ...

  4. javascript中的宏任务和微任务(二)

    js事件轮询执行顺序总结: 1)所有的同步任务都在主线程上执行,行成一个执行栈. 2)除了主线程之外,还存在一个任务列队,只要异步任务有了运行结果,就在任务列队中植入一个时间标记. 3)主线程完成所有 ...

  5. JS异步之宏队列与微队列

    1. 原理图 2. 说明 JS 中用来存储待执行回调函数的队列包含 2 个不同特定的列队 宏列队:用来保存待执行的宏任务(回调),比如:定时器回调.DOM 事件回调.ajax 回调 微列队:用来保存待 ...

  6. 关于JS中变量提升的规则和原理的一点理解

        关于变量提升,以前在一些教程和书籍上都听到过,平时开发中也知道有这个规律,但是今天突然在一个公开课中听到时,第一反应时一脸懵逼,然后一百度,瞬间觉得好熟悉啊,差点被这个概念给唬住了,不信我给你 ...

  7. 关于JS中变量提升的规则和原理的一点理解(二)

    上篇文章中讲到变量提升和函数提升的先后顺序时蒙了,后来去查了一下资料,特别整理一下. 在<你不知道的JavaScript(上卷)>一书的第40页中写到:函数会首先被提升,然后才是变量. 书 ...

  8. JS中some(),every(),fiflter(),map()各种循环的区别理解

    1.some():返回一个Boolean,判断是否有元素符合func条件const arr = [1,2,3,4]; arr.some((item)=>{return item>1}) 打 ...

  9. JavaScript同步模式,异步模式及宏任务,微任务队列

    首先JavaScript是单线程的语言,也就是说JS执行环境中,负责执行代码的线程只有一个.一次只能执行一个任务,如果有多个任务的话, 就要排队,然后依次执行,优点就是更安全,更简单.缺点就是遇到耗时 ...

随机推荐

  1. GitHub Ribbons : 谈网站的安全性-资源链接如何 预防/实现 爬虫的批量下载!

    GitHub Ribbons : 谈网站的安全性-资源链接如何 预防/实现 爬虫的批量下载! 预防方法: 1. 使用随机数字符串,拼接URL! https://camo.githubuserconte ...

  2. GPU 加速 & WebGL

    GPU 加速 & WebGL 开启 GPU 加速, 硬件加速 垃圾面试官,瞎忽悠 holy shit 美国想象力英语,前端 leader WebGL 加速 ??? 是什么鬼 ??? three ...

  3. CSS 实现文本的竖向排版

    CSS 实现文本的竖向排版 demos .text{ word-wrap: break-word; width: 0px; } writing-mode .text{ writing-mode: ve ...

  4. Flutter: 显示&关闭系统叠加层ui

    import 'package:flutter/services.dart'; /// 关闭 SystemChrome.setEnabledSystemUIOverlays([]); /// 显示 S ...

  5. subline3 如何设置es6高亮

    步骤: 1.操作Ctrl+Shift+P , 然后在弹出的框内输入Package Control: in,2.选择Package Control: install package,3.等待再次弹出输入 ...

  6. NGK算力持有好处多多!SPC、VAST等免费拿!

    众所周知,NGK是分布式存储的,作为Web3.0以及数字经济时代的基础设施,为数字加密市场带来了全新的商业模式和经济业态,但是,这只是一个重要的起点,真正的价值还在后面! 为了满足NGK生态建设者强烈 ...

  7. DeFi下半场,除了YFI,还有BGV!

    自今年夏季开始,DeFi市场便已经进入火热态势,且持续至今.其中,去中心化交易所(DEX)以及各种金融衍生品的出现,吸引了大批资金的进入,资本市场的目光已从传统金融移到了DeFi市场,期望着能够从De ...

  8. css整理之-----------布局相关

    文档流 文档流指的是元素排版布局过程中,元素会默认自动从左往右,从上往下的流式排列方式布局,文档流可以分为定位流.浮动流.普通流三种 普通流(Normal flow) 在常规流中,盒一个接着一个排列, ...

  9. 利用 Java 操作 Jenkins API 实现对 Jenkins 的控制详解

    本文转载自利用 Java 操作 Jenkins API 实现对 Jenkins 的控制详解 导语 由于最近工作需要利用 Jenkins 远程 API 操作 Jenkins 来完成一些列操作,就抽空研究 ...

  10. 身份认证:JSON Web Token

    JSON Web Token(JWT)是一种基于JSON的开放标准((RFC 7519),也是目前最流行的跨域认证解决方案. 传统的 cookie 认证方式看起来遵守了 REST 架构的无状态要求,但 ...