黑马程序员技术交流社区

标题: 【上海校区】了解JavaScript的运行核心——事件循环(Event L... [打印本页]

作者: LittlePrince    时间: 2020-4-29 11:42
标题: 【上海校区】了解JavaScript的运行核心——事件循环(Event L...
前言什么要学底层的事件循环Event Loop,不仅仅是因为这是一道面试的常考题。作为一个程序员,了解程序的运行机制是很重要的,这样可以帮助你去输出更优质的代码。前端是一个范围很广的领域,技术一直在更新迭代,掌握了底层的原理可以应对新的技术。
JS初始设计JavaScript从诞生起就是单线程。原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。后来就约定俗成,JavaScript为一种单线程语言。(虽然 HTML5 增加了 Web Work 可用来另开一个线程,但是该线程仍受主线程的控制,所以 JavaScript 的本质依然是单线程)
什么是Event Loop?Event Loop就是事件循环,是浏览器和NodeJS用来解决Javascript单线程运行带来的问题的一种运行机制。
针对于浏览器和NodeJS两种不同环境,Event Loop也有不同的实现:
因此浏览器和NodeJS的Event Loop是两种不同的概念。不过在搞清楚Event Loop之前我们先来搞清楚一些其他概念。
概念一:进程、程序、线程【1】进程(process):是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
【2】程序:是指令和数据的有序集合,进程中的文本区域就是代码区就是程序。
【3】线程(thread):是进程的执行流,是CPU调度和分派的基本单位。
程序本身没有任何运行的含义,是一个静态的概念。而进程则是在处理机上的一次执行过程,它是一个动态的概念。同一个程序包含多个进程。简单来说,进程简单理解就是我们平常使用的程序,如 QQ,浏览器,网盘等。进程拥有自己独立的内存空间地址,拥有一个或多个线程,而线程就是对进程粒度的进一步划分。
我们通过以下这张图来加深对进程和线程的理解:

概念二:堆、栈、队列如果你学过数据结构,就一定会遇到 "堆", "栈","队列",最关键的是即使你去面试,这些还都会问到,如果你不懂对你损失很大的。
【1】堆(Heap)
1、堆是一种经过排序的树形数据结构,每个节点都有一个值,通常我们所说的堆的数据结构是指二叉树。
2、堆分为两种情况,有最大堆和最小堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
3、堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。
4、堆是应用程序在运行的时侯请求操作系统分配给自己内存,而不是在编译的时 ,一般是申请/给予的过程。
5、堆用来存储对象的值,并会用一个地址来记录存储值的位置,该地址存储在命名对象的变量中(即存储在栈内存中)。因此复制这个变量只是复制了地址,而不是复制了对象。


【2】栈(Stack)
1、栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,把另一端称为栈底。不含任何数据元素的栈称为空栈。
2、栈是一种具有后进先出的数据结构,又称为后进先出的线性表,简称 LIFO(Last In First Out)结构。可以想象为一个桶,后放进去的东西会先拿出来,而且只能在桶口操作。
3、栈是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的栈的大小。
4、栈定义了两个方法:PUSH操作在堆栈的顶部加入一个元素,POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一


【3】队列(Queue)
1、队列是一种特殊的线性表,只允许在表的前端进行删除操作,表的后端进行插入操作,进行插入操作的端称为队尾,进行删除操作的端称为队头。
2、队列中没有元素时,称为空队列。
3、队列是一种先进先出的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。


【4】JS执行栈
JS 执行栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
概念三:同步任务、异步任务【1】同步任务:可以立即执行的任务,例如声明一个变量或者执行一次加法操作等。同步任务属于宏任务。
【2】异步任务:是不会立即执行的事件任务。异步任务包括宏任务和微任务。
JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。

概念四:宏队列、微队列在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)也叫Jobs。
【1】宏队列:一些异步任务的回调会依次进入宏任务队列,等待后续被调用,这些异步任务包括
#浏览器Node
I/O✅✅
setTimeout✅✅
setInterval✅✅
setImmediate❌✅
requestAnimationFrame✅❌【2】微队列: 另一些异步任务的回调会依次进入微任务队列,等待后续被调用,这些异步任务包括:
#浏览器Node
process.nextTick❌✅
MutationObserver(html5新特性)✅❌
Promise.then catch finally✅✅概念五:浏览器内核浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。浏览器内核有多种线程在工作。
【1】GUI 渲染线程:
【2】JS 引擎线程:
【3】事件触发线程:
【4】定时器触发线程:
【5】http 请求线程:
概念六:定时器定时器会开启一条定时器触发线程来触发计时,定时器会在等待了指定的时间后将事件放入到任务队列中等待读取到主线程执行。定时器指定的延时毫秒数其实并不准确,因为定时器只是在到了指定的时间时将事件放入到任务队列中,必须要等到同步的任务和现有的任务队列中的事件全部执行完成之后,才会去读取定时器的事件到主线程执行,中间可能会存在耗时比较久的任务,那么就不可能保证在指定的时间执行。
浏览器中的Event LoopJavascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行
javascript是一种单线程语言,所有任务都在一个线程上完成(即采用排队形式:因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务)。一旦前面遇到大量任务或者遇到一个耗时的任务,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。所以 JavaScript 有一套机制去处理同步和异步操作,那就是事件循环 (Event Loop)。
一个完整浏览器端的 Event Loop 过程,可以概括为以下阶段:
">">​
主线程不断重复第三步,也就是 只要主线程空了,就会读取任务列表,该过程不断重复,这就是所谓的事件循环
总结:当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。
JavaScript代码的具体流程Node中的Event Loop【1】Node简介
Node 环境下的 Event Loop 与浏览器环境下的 Event Loop并不相同。Node.js 采用 V8 作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现
【2】Node.js的运行机制如下:
【3】六个阶段
其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
">">​
node中的事件循环的顺序:外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)
timers 阶段:timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。 同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。
pending callbacks 阶段这个阶段会执行一些和底层系统有关的操作,例如TCP连接返回的错误等。这些错误发生时,会被Node 推迟到下一个循环中执行。
poll 轮询阶段poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情:1、回到 timer 阶段执行回调   2、执行 I/O 回调。并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情:
当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。
check 阶段这个阶段会执行 setImmediate() 设置的任务。
close callbacks 阶段如果一个 socket 或 handle(句柄) 突然被关闭了,例如通过 socket.destroy() 关闭了,close 事件将会在这个阶段发出。
【4】宏队列、微队列
宏队列:在浏览器中,可以认为下面只有一个宏队列,所有的macrotask都会被加到这一个宏队列中,但是在NodeJS中,不同的macrotask会被放置在不同的宏队列中
微队列:在浏览器中,也可以认为只有一个微队列,所有的microtask都会被加到这一个微队列中,但是在NodeJS中,不同的microtask会被放置在不同的微队列中
【5】NodeJS的Event Loop过程
【6】setTimeout 对比 setImmediate
【7】setImmediate 对比 process.nextTick


文章转载自:https://juejin.im/post/5ea7eaeb5188256d900117ff






欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2