浏览器环境下和node.js环境下的事件循环机制+JS为什么是单线程

事件循环机制:事件循环分为两种,分别是浏览器事件循环和node.js事件循环,JavaScript是一门单线程语言,其主线程只有一个。Event Loop事件循环,其实就是JS引擎管理事件执行的一个流程,具体由运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。

​ 事件循环机制告诉了我们JS代码的执行顺序,是指浏览器或Node的一种解决JS单线程运行时不会阻塞的一种机制。

浏览器环境下和node环境下的事件循环都分为同步任务和异步任务

node.js的事件循环机制(实现异步I/O)和浏览器事件循环机制执行顺序趋于一致,但浏览器依赖v8引擎,node.js依赖LIBUV库/引擎

下面具体说的是浏览器环境中的事件循环,和在node.js环境中事件循环顺序差不多,只是有些不同的API

JS为什么是单线程?
什么是进程
我们都知道,CPU是计算机的核心,承担所有的计算任务
在这里插入图片描述

官网说法,进程是CPU资源分配的最小单位
字面意思就是进行中的程序,可以将它理解为一个可以独立运行且拥有自己的资源空间的任务程序
进程包括运行中的程序和程序所使用到的内存和系统资源
CPU可以有很多进程,我们的电脑每打开一个软件就会产生一个或多个进程,为什么电脑运行的软件多就会卡,是因为CPU给每个进程分配资源空间,但是一个CPU一共就那么多资源,分出去越多,越卡,每个进程之间是相互独立的,CPU在运行一个进程时,其他的进程处于非运行状态,CPU使用 时间片轮转调度算法 来实现同时运行多个进程

并行传输的基本单元是线程,串行传输的基本单元是进程

什么是线程
线程是CPU调度的最小单位
线程是建立在进程的基础上的一次程序运行单位,通俗点解释线程就是程序中的一个执行流,一个进程可以有多个线程
一个进程中只有一个执行流称作单线程,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行
一个进程中有多个执行流称作多线程,即在一个程序中可以同时运行多个不同的线程来执行不同的任务, 也就是说允许单个程序创建多个并行执行的线程来完成各自的任务

进程和线程的区别
进程是操作系统分配资源的最小单位,线程是程序执行的最小单位
一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线
进程之间相互独立,但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)
调度和切换:线程上下文切换比进程上下文切换要快得多

多进程和多线程
多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如大家可以在网易云听歌的同时打开编辑器敲代码,编辑器和网易云的进程之间不会相互干扰
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务

JS为什么是单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JS的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

浏览器包含哪些进程:Browser进程、第三方插件进程、GPU进程、渲染进程(重)
浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等,其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程(浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的)、事件触发线程、定时器触发线程、http 异步请求线程等主要线程。

总结:一个浏览器每个 tab 页就是一个进程,页的进程还有Browser进程、第三方插件进程、GPU进程、渲染进程(重)等进程,渲染进程(重)进程下还有多个线程,js单线程是由JS 引擎线程(浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的)

为什么浏览器要多进程?
我们假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差,同理如果插件崩溃了也会影响整个浏览器

js单线程具体可以参考这篇文章

单线程,就是指一次只能完成一件任务,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务。但如果有一个任务的执行时间很长,比如文件的读取或者数据的请求等等,那么后面的任务就要一直等待,这就会影响用户的使用体验,js 引擎并不会一直等待其返回结果,而是会将异步事件挂起,继续执行执行栈中的其他任务。
为了解决这种情况,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
事件循环机制是为了解决同步任务和异步任务的问题

同步: 同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。
异步:异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务

 <script>
      setTimeout(function(){
         console.log(3);
        },5000)
      console.log(1);
      console.log(2);
 </script>

上述代码同步和异步基本的执行过程:

  1. 先执行执行栈中的同步任务
  2. 遇到异步任务(回调函数)就放入任务队列/事件队列中
  3. 一旦执行栈中的同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,被读取的异步任务结束等待状态,进入执行栈开始执行。
    在这里插入图片描述

那么,JavaScript中的异步是怎么实现的呢?那要需要说下回调和事件循环这两个概念啦

首先要先说下任务队列,前面也介绍了,异步任务是不会进入主线程,而是会先进入任务队列,任务队列其实是一个先进先出的数据结构,也是一个事件队列,比如说文件读取操作,因为这是一个异步任务,因此该任务会被添加到任务队列中,等到IO完成后,就会在任务队列中添加一个事件,表示异步任务完成啦,可以进入执行栈啦~ 但是这时候,主线程不一定有空,当主线程处理完其它任务有空时,就会读取任务队列,读取里面有哪些事件,排在前面的事件会被优先进行处理,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码,也就是执行异步任务啦

单线程从任务队列中读取任务是不断循环的,每次执行栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等待,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫做事件循环

总的来说,JavaScript的异步机制包括以下几个步骤:

1. 所有同步任务都在主线程上执行,形成一个执行栈。
2. 主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件
3. 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,
 先执行微任务队列再执行宏任务队列。
5. 主线程不断的重复上面的第三

回调函数(回调函数就是将一个函数当作另一个主函数的参数来使用的函数)是异步编程最基本的方法
回调函数是传统的一种异步编程解决方案,其原理就是将一个函数当作参数传递到另一个主函数中,当主函数执行完自身的内容之后,在运行传递进来的回调函数。

采用这种方式,我们把同步操作变成了异步操作,test1()不会堵塞程序运行,相当于先执行主程序的主要逻辑,将耗时的操作推迟执行。

优点:简单,容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱
缺点:一个任务只能有一个回调函数

可以解释宏任务微任务执行顺序   控制台输出1 7 13 8 2 4 5 9  11 12  
执行顺序就是宏任务->微任务,以此循环
具体就是主线程会先进入宏任务script片段(宏任务里可以执行的语句会先执行)中检查否有micro-task(微任务),如果有,
就将micro-task(微任务)队列中的所有任务依次执行,直到micro-task(微任务)队列为空; 之后再检查macro-task(宏任务)队列中
是否还有宏任务,如果有,则取出第一个macro-task(宏任务)加入到执行栈中,宏任务里可以执行的语句会先执行,
再直到micro-task(微任务)队列为空,之后再清空执行栈;
再取出第二个macro-task(宏任务)加入到执行栈中,以此循环,直到全部的任务都执行完成。
  <script>
console.log('1');

setTimeout(function() {
    console.log('2');
   
    new Promise(function(resolve) {
        console.log('4');
        resolve();  //只有resolve才可以用.then接收,reject用.catch接收
    }).then(function() {  
     
        console.log('5') //只有resolve了才会执行then中的回调函数
    })
})

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {  //.then是回调
    console.log('8')
})

setTimeout(function() {
    console.log('9');
   
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
console.log(13);
    </script>

事件循环(Event Loop):
同步任务和异步任务分别进入不同的执行"场所"; 同步任务进入主线程,异步任务进入Event Table并注册回调函数。
当指定的事情完成时,Event Table会将这个函数移入任务队列(task quene),等待主线程的任务执行完毕;
当栈中的代码执行完毕,执行栈(call stack)中的任务为空时,就会读取任务队列(task quene)中的任务,去执行对应的回调;
如此循环,就形成js的事件循环机制(Event Loop)。

执行过程:
1.所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。2.遇到异步任务, 进入Event Table并注册回调函数; 等到指定的事件完成(如ajax请求响应返回, setTimeout延迟到指定时间)时,Event Table会将这个回调函数移入Event Queue。
3.当栈中的代码执行完毕,执行栈(call stack)中的任务为空时,主线程会先进入宏任务script片段中检查否有micro-task(微任务),如果有,就将micro-task(微任务)队列中的所有任务依次执行,直到micro-task(微任务)队列为空; 之后再检查macro-task(宏任务)队列中是否还有任务,如果有,则取出第一个macro-task(宏任务)加入到执行栈中,直到micro-task(微任务)队列为空,之后再清空执行栈,再取出第二个macro-task(宏任务)加入到执行栈中,以此循环,直到全部的任务都执行完成。

在这里插入图片描述
在这里插入图片描述
任务队列(task queue) / 事件队列(event queue):
SetTimeout和promise都是异步任务,那么它们两个先执行谁呢?
其实在js中也是有一个机制,就是会分为宏任务和微任务。宏任务和微任务分别存放在不同的event queue,这两个队列分别为macrotack queue和microtack queue.
事件/任务队列分为: macro-task(宏任务)队列, micro-task(微任务)队列;
在最新标准中,它们被分别称为task与jobs。

事件表格(Event Table):
Event Table 可以理解成一张事件->回调函数 对应表。
用来存储 JS 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表。
当指定的事件完成(如ajax请求响应返回, setTimeout延迟到指定时间)时,Event Table会将这个回调函数移入Event Queue, 即macro-task(宏任务)队列 或 micro-task(微任务)队列。

macro-task(宏任务):
谁发起: 宿主(Node.js或者浏览器)
script(整体代码)
setTimeout
setInterval
setImmediate(node.js中)
I/O (比如Ajax操作从网络读取数据)
UI render
事件绑定

micro-task(微任务):
谁发起:js引擎
process.nextTick(可以立即执行一个异步任务)(node.js中)
new promise().then(回调)
Async/Await
MutationObserver(html5新特性)

Node.js发布于2009年5月,由Ryan Dahl开发,是一个基于Chrome V8引擎的JavaScript运行环境,使用了一个事件驱动、非阻塞式I/O模型,让JavaScript 运行在服务端的开发平台。

事件循环、观察者、请求对象、IO 线程池是构成了Node.js异步I/O模型的基本要素。

Node.js 被分为了四层,分别是 应用层、V8引擎层、Node API层 和 LIBUV层。
应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs
V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互
LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心
Node.js在主线程维护了一个事件队列,接收到请求后,就将该请求作为一个事件放入Event Queue中,然后继续接受其他请求,当主线程空闲(没有请求接收) 的时候,就开始轮询事件队列

Node.js 只是主线程是单线程,主线程中的操作是同步阻塞的。但它把所有需要异步的操作扔给libuv线程池,该线程池负责多线程。主线程通过一定的事件轮询机制与IO线程池交互得到异步数据。(底层i/o并不是单线程)(node 中事件循环的实现依赖 libuv 引擎)

事件循环是执行异步任务的核心。 C ++ Web API一旦完成一个功能(任务),便会调用回调。回调将移至事件队列并等待直到堆栈变空。因此,事件队列是事件循环的一部分,由事件循环生成。

V8引擎用于执行我们编写的javascript代码,而libuv是一个库,用于在Node.js中提供多线程功能来执行长时间运行的进程。

node.js的事件循环机制(实现异步I/O)和浏览器事件循环机制执行顺序趋于一致,但浏览器依赖v8引擎,node.js依赖LIBUV库/引擎

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值