我是这样理解EventLoop的
我是这样理解EventLoop的
一、前言
  众所周知,在使用javascript时,经常需要考虑程序中存在异步的情况,如果对异步考虑不周,很容易在开发中出现技术错误和业务错误。作为一名合格的javascript使用者,了解异步的存在和运行机制十分重要且有必要;那么,异步究竟是何方神圣呢?我们不得不提Event Loop:也叫做事件循环,是指浏览器或Node环境的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是实现异步的原理。作为一种单线程语言,javascript本身是没有异步这一说法的,是由其宿主环境提供的(EventLoop优秀文章网上有很多,这篇文章是自己的整合和理解)。
注意:Event Loop 并不是在 ECMAScript 标准中定义的,而是在 HTML 标准中定义的;
二、Event Loop知识铺垫
  javascript代码运行时,任务被分为两种,宏任务(MacroTask/Task)和微任务(MircoTask);Event Loop在执行和协调各种任务时也将任务队列分为Task Queue和MircoTak Queue分别对应管理宏任务(MacroTask/Task)和微任务(MircoTask);作为队列,Task Queue和MircoTak Queue也具备队列特性:先进先出(FIFO—first in first out)。
1、微任务(MircoTask)
在 HTML 标准中,并没有明确规定 Microtask,但是实际开发中包含以下四种:
- Promise中的then、catch、finally(原理参考:【js进阶】手撕Promise,一码一解析 包懂)
- MutationObserver(监视 DOM 变动的API,详情参考MDN)
- Object.observe(废弃:监听标准对象的变化)
- Process.nextTick(Node环境,通常也被认为是微任务)
2、宏任务(MacroTask/Task)
  基本上,我们将javascript中非微任务(MircoTask)的所有任务都归为宏任务,比如:
- script中全部代码
- DOM操作
- 用户交互操作
- 所有的网路请求
- 定时器相关的 setTimeout、setInterval 等
- ···
3、javascript runtime
  javascript runtime:为 JavaScript 提供一些对象或机制,使它能够与外界交互,是javascript的执行环境。javascript执行时会创建一个main thread主线程和call-stack 调用栈(执行栈,遵循后进先出的规则),所有的任务都会被放到调用栈/执行栈等待主线程执行。其运行机制如下:
- 1)主线程自上而下依次执行所有代码;
- 2)同步任务直接进入到主线程被执行;
- 3)异步任务进入到Event Table,当异步任务有结果后,将相对应的回调函数进行注册,放入Event Queue;
- 4)主线程任务执行完空闲下来后,从Event Queue(FIFO)中读取任务,放入主线程执行;
- 5)放入主线程的Event Queue任务继续从第一步开始,如此循环执行;
 上述步骤执行过程就是我们所说的事件循环(Event Loop),上图展示了事件循环中的一个完整循环过程。
三、浏览器环境的Event Loop
  不同的执行环境中,Event Loop的执行机制是不同的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 实现并提供了 ECMAScript 标准中的所有数据类型、操作符、对象和方法(注意并没有 DOM)。但它们的 Runtime 并不一样:Chrome 提供了 window、DOM,而 Node.js 则是 require、process 等等。我们在了解浏览器中Event Loop的具体表现前需要先整理同步、异步、微任务、宏任务之间的关系!
1、同步、异步 和 宏任务、微任务
  看到这里,可能会有很多疑惑:同步异步很好理解,宏任务微任务上面也进行了分类,但是当他们四个在一起后就感觉很混乱了,冥冥之中觉得同步异步和宏任务微任务有内在联系,但是他们之间有联系吗?又是什么联系呢?网上有的文章说宏任务就是同步的,微任务就是异步的 这种说法明显是错的!
  其实我更愿意如此描述:宏任务和微任务是相对而言的,根据代码执时循环的先后,将代码执行分层理解,在每一层(一次)的事件循环中,首先整体代码块看作一个宏任务,宏任务中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是该宏任务层的微任务;宏任务中的同步代码进入主线程中立即执行的,宏任务中的非微任务异步执行代码将作为下一次循环的宏任务时进入调用栈等待执行的;此时,调用栈中等待执行的队列分为两种,优先级较高先执行的本层循环微任务队列(MicroTask Queue),和优先级低的下层循环执行的宏任务队列(MacroTask Queue)!
注意:每一次/层循环,都是首先从宏任务开始,微任务结束;
2、简单实例分析
上面的描叙相对拗口,结合代码和图片分析理解:
  答案暂时不给出,我们先进行代码分析:这是一个简单而典型的双层循环的事件循环执行案例,在这个循环中可以按照以下步骤进行分析:
- 1、首先区分出该层宏任务的范围(整个代码);
- 2、区分宏任务中同步代码和异步代码
 同步代码:console.log('script start');、console.log('enter promise');和console.log('script end');;
 异步代码块:setTimeout和Promise的then(注意:Promise中只有then、catch、finally的执行需要等到结果,Promise传入的回调函数属于同步执行代码);
- 3、在异步中找出同层的微任务(代码中的Promise的then)和下层事件循环的宏任务(代码中的setTimeout)
- 4、宏任务的同步代码优先进入主线程,按照自上而下顺序执行完毕;
 输出顺序为:
//同步代码执行输出
script start
enter promise
script end
- 5、当主线程空闲时,执行该层的微任务
//同层微任务队列代码执行输出
promise then 1
promise then 2
- 6、首层事件循环结束,进入第二层事件循环(setTimeout包含的执行代码,只有一个同步代码)
//第二层宏任务队列代码执行输出
setTimeout
综合分析最终得出数据结果为:
//首层宏任务代码执行输出
script start
enter promise
script end
//首层微任务队列代码执行输出
promise then 1
promise then 2
//第二层宏任务队列代码执行输出
setTimeout
3、复杂案例分析
那么,你是否已经了解上述执行过程了呢?如果完全理解上述实例,说明你已经大概知道浏览器中Event Loop的执行机制,但是,要想知道自己是不是完全明白,不妨对于下列多循环的事件循环进行分析检验,给出你的结果:
console.log('1');
setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
    setTimeout(function() {
	    console.log('5');
	    new Promise(function(resolve) {
	        console.log('6');
	        resolve();
	    }).then(function() {
	        console.log('7')
	    })
	})
	console.log('14');
})
new Promise(function(resolve) {
    console.log('8');
    resolve();
}).then(function() {
    console.log('9')
})
setTimeout(function() {
    console.log('10');
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
console.log('13')
分析:如下图草稿所示,左上角标a为宏任务队列,左上角标i为微任务队列,同一层循环中,本层宏任务先执行,再执行微任务;本层宏任务中的非微任务异步代码块作为下层循环的宏任务进入下次循环,如此循环执行;
如果你的与下面的结果一致,恭喜你浏览器环境的Event Loop你已经完全掌握,那么请开始下面的学习:
1->8->13->9->2->3->14->4->10->11->12->5->6->7
四、Node 环境下的 Event Loop
  在Node环境下,浏览器的EventLoop机制并不适用,切记不能混为一谈。这里借用网上很多博客上的一句总结(其实我也是真不太懂):Node中的Event Loop是基于libuv实现的:libuv是 Node 的新跨平台抽象层,libuv使用异步,事件驱动的编程方式,核心是提供i/o的事件循环和异步回调。libuv的API包含有时间,非阻塞的网络,异步文件操作,子进程等等。
1、Event Loop的6阶段
  Node的Event loop一共分为6个阶段,每个细节具体如下:
- timers:执行setTimeout和setInterval中到期的callback。
- pending callback:上一轮循环中少数的callback会放在这一阶段执行。
- idle, prepare:仅在内部使用。
- poll:最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段。
- check:执行setImmediate的callback。
- close callbacks:执行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。
 注意:上面六个阶段都不包括- process.nextTick()
 
重点:如上图所,在Node.js中,一次宏任务可以认为是包含上述6个阶段、微任务microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
2、process.nextTick()
  在第二节中就了解到,process.nextTick()属于微任务,但是这里需要重点提及下:
- process.nextTick()虽然它是异步API的一部分,但未在图中显示。因为- process.nextTick()从技术上讲,它不是事件循环的一部分;
- 当每个阶段完成后,如果存在 nextTick,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行(可以理解为微任务中优先级最高的)
3、实例分析
老规矩,线上代码:
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')
    })
})
console.log('13')
将代码的执行分区进行解释
分析:如下图草稿所示,左上角标a为宏任务队列,左上角标i为微任务队列,左上角标t为timers阶段队列,左上角标p为nextTick队列同一层循环中,本层宏任务先执行,再执行微任务;本层宏任务中的非微任务异步代码块作为下层循环的宏任务进入下次循环,如此循环执行:
- 1、整体代码可以看做宏任务,同步代码直接进入主线程执行,输出1,7,13,接着执行同层微任务且nextTick优先执行输出6,8;
- 2、二层中宏任务中只存在setTimeout,两个setTimeout代码块依次进入6阶段中的timer阶段以t1、t2进入队列;代码等价于:
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
- 3、setTimeout中的同步代码立即执行输出2,4,9,11,nextTick和Pormise.then进入微任务执行输出3,10,5,12;
- 4、二层中不存在6阶段中的其他阶段,循环完毕,最终输出结果为:1->7->13->6->8->2->4->9->11->3->10->5->12;
4、当堂小考
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')
        setTimeout(function() {
          console.log('6');
          process.nextTick(function() {
              console.log('7');
          })
          new Promise(function(resolve) {
              console.log('8');
              resolve();
          }).then(function() {
              console.log('9')
          })
      })
    })
})
process.nextTick(function() {
    console.log('10');
})
new Promise(function(resolve) {
    console.log('11');
    resolve();
}).then(function() {
    console.log('12')
    setTimeout(function() {
      console.log('13');
      process.nextTick(function() {
          console.log('14');
      })
      new Promise(function(resolve) {
          console.log('15');
          resolve();
      }).then(function() {
          console.log('16')
      })
  })
})
setTimeout(function() {
    console.log('17');
    process.nextTick(function() {
        console.log('18');
    })
    new Promise(function(resolve) {
        console.log('19');
        resolve();
    }).then(function() {
        console.log('20')
    })
})
console.log('21')
五、总结
  浏览器和Node环境下,microtask 任务队列的执行时机不同:Node 端,microtask 在事件循环的各个阶段之间执行;浏览器端,microtask 在事件循环的 macrotask 执行完之后执行;
参考借鉴
我是这样理解EventLoop的的更多相关文章
- CSharpGL(55)我是这样理解PBR的
		CSharpGL(55)我是这样理解PBR的 简介 PBR(Physically Based Rendering),基于物理的渲染,据说是目前最先进的实时渲染方法.它比Blinn-Phong方法的真实 ... 
- 我是如何理解并使用maven的
		前言 一直想写一篇关于Maven的文章,但是不知如何下笔,如果说能使用,会使用Maven的话,一.两个小时足矣,不需要搞懂各种概念.那么给大家来分享下我是如何理解并使用maven的. 什么是Maven ... 
- vue是一个渐进式的框架,我是这么理解的
		vue是一个渐进式的框架,我是这么理解的 原文地址 时间:2017-10-26 10:37来源:未知 作者:admin 每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主 ... 
- 我是如何理解Java抽象类和接口的
		在面试中我们经常被问到:Java中抽象类和接口的区别是什么? 然后,我们就大说一通抽象类可以有方法,接口不能有实际的方法啦:一个类只能继承一个抽象类,却可以继承多个接口啦,balabala一大堆,就好 ... 
- 我是如何理解ThreadLocal
		ThreadLocal的概念 ThreadLocal从英文的角度看,可以看成thread和local的组合,就是线程本地的意思,我们都知道,看过jvm内存分配的人都知道在jvm虚拟机中对每一个线程都分 ... 
- 理解 EventLoop
		链接 链接 node 浏览器 执行顺序有差异 macrotask microtask 一个线程会有 堆 栈 消息队列; 栈函数执行是用的, 堆用了存放定义的对象, 消息队列来处理异步的操作 a() ... 
- 我是这样理解--SVM,不需要繁杂公式的那种!(附代码)
		1. 讲讲SVM 1.1 一个关于SVM的童话故事 支持向量机(Support Vector Machine,SVM)是众多监督学习方法中十分出色的一种,几乎所有讲述经典机器学习方法的教材都会介绍.关 ... 
- 我是如何理解Android的Handler模型_3
		AsyncTask则相当于现代化的电话系统,接线员的功能被完全封装了. 对于上例,新建更新TextView的类并继承AsyncTack类,如下: class UpdataTV extends Asyn ... 
- 我是如何理解Android的Handler模型_2
		对比例程说明,如: 例:在新新线程中替换TextView显示内容. 界面如下,单击按键后original data 替换为 changed data Handler Message部分实现步骤: 1. ... 
随机推荐
- drf的权限扩充
			drf框架为我们提供了基本的权限验证.主要包括三种验证 1.AllowAny 所有用户 2.IsAuthenticated 验证过的用户 3.IsAdminUser 超级管理员 这些权限人员不一定满足 ... 
- 使用PyQt(Python+Qt)+moviepy开发的视频截取、音视频分离、MP4转GIF动图工具免费下载分享
			专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 在因博文素材需要将软件操作制作成动画时,发现网上相关绿色使用工具都需要 ... 
- 第15.40节、PyQt(Python+Qt)实战:moviepy实现MP4视频转gif动图的工具
			专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 一.引言 在写<第15.39节.splitDockWidget和 ... 
- 利用Python特殊变量__dict__快速实现__repr__的一种方法
			在<第8.15节 Python重写自定义类的__repr__方法>.<Python中repr(变量)和str(变量)的返回值有什么区别和联系>.<第8.13节 Pytho ... 
- 转:http协议学习系列(响应头---Response Headers)
			HTTP最常见的响应头如下所示: ·Allow:服务器支持哪些请求方法(如GET.POST等): ·Content-Encoding:文档的编码(Encode)方法.只有在解码之后才可以得到Conte ... 
- PyQt(Python+Qt)学习随笔:QToolBox工具箱currentItem对应的index、text、name、icon、ToolTip属性
			老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 在Designer中,toolBox主要有如下属性: 可以看到,toolBox的属性主要是与当前项相 ... 
- 第11.10节 Python正则表达式的非贪婪模式的重复匹配:'*?', '+?',和 '??'
			在<第11.9节 Pytho正则表达式的贪婪模式和非贪婪模式>老猿简单介绍了贪婪模式和非贪婪模式,并说明'', '+',和 '?' 修饰符都是 贪婪的:它们在字符串进行尽可能多的匹配.有时 ... 
- PyQt(Python+Qt)学习随笔:布局控件layoutSpacing属性
			在Qt Designer中布局控件有4个,分别是Vertical Layout(垂直布局).Horizontal Layout(水平布局).Grid Layout(网格布局).Form Layout( ... 
- 计算机语言与JAVA的发展
			计算机语言与JAVA的发展 第一代语言 2进制 第二代语言 汇编语言 解决人类无法读懂的问题 指令替代二进制 目前应用 逆向工程 机器人 病毒 第三代语言 摩尔定律 性能提升愈来愈慢 高级语言 面向过 ... 
- docker 使用ubuntu 系统
			1.安装Ubuntu系统命令:docker pull ubuntu这是一个极度精简的系统,连最基本的wget命令都没有:所以先要apt-get update升级系统和安装apt-get install ... 
