JS异步编程1:认识JavaScript的事件循环模型

Posted 沿着路走到底

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS异步编程1:认识JavaScript的事件循环模型相关的知识,希望对你有一定的参考价值。

javascript的运⾏顺序就是完全单线程的异步模型:同步在前,异步在后。所有的异步任务都要等待当前的同步任 务执⾏完毕之后才能执⾏。
var a = 1
var b = 2
var d1 = new Date().getTime()
var d2 = new Date().getTime()
setTimeout(function()
 console.log('我是⼀个异步任务')
,1000)
while(d2-d1<2000)
 d2 = new Date().getTime()

//这段代码在输出3之前会进⼊假死状态,'我是⼀个异步任务'⼀定会在3之后输出
console.log(a+b)
观察上⾯的程序我们实际运⾏之后就会感受到单线程异步模型的执⾏顺序了,并且这⾥我们会发现 setTimeout设置 的时间是 1000 毫秒但是在 while 的阻塞 2000 毫秒的循环之后并没有等待 1秒⽽是直接输出了我是⼀个异步任务,这是因为 setTimout 的时间计算是从 setTimeout() 这个函数执⾏时开始计算的。

JS的线程组成

虽然浏览器是单线程执⾏ JavaScript 代码的,但是浏览器实际是以多个线程协助操 作来实现单线程异步模型的,具体线程组成如下: 1. GUI 渲染线程 2. JavaScript 引擎线程 3. 事件触发线程 4. 定时器触发线程 5. http 请求线程 6. 其他线程 按照真实的浏览器线程组成分析,我们会发现实际上运⾏ JavaScript 的线程其实并不是⼀个,但是为什么说 JavaScript是⼀⻔单线程的语⾔呢?因为这些线程中实际参与代码执⾏的线程并不是所有线程,⽐如 GUI 渲染线程为 什么单独存在,这个是防⽌我们在html ⽹⻚渲染⼀半的时候突然执⾏了⼀段阻塞式的 JS 代码⽽导致⽹⻚卡在⼀半停 住这种效果。 JavaScript 代码运⾏的过程中实际执⾏程序时同时只存在⼀个活动线程,这⾥实现同步异步就是靠 多线程切换的形式来进⾏实现的 所以我们通常分析时,将上⾯的细分线程归纳为下列两条线程: 1. 【主线程】:这个线程⽤了执⾏⻚⾯的渲染, JavaScript 代码的运⾏,事件的触发等等 2. 【⼯作线程】:这个线程是在幕后⼯作的,⽤来处理异步任务的执⾏来实现⾮阻塞的运⾏模式

JavaScript的运⾏模型

上图是 JavaScript运⾏时的⼀个⼯作流程和内存划分的简要描述,我们根据图中可以得知主线程就是我们JavaScript 执⾏代码的线程,主线程代码在运⾏时,会按照同步和异步代码将其分成两个去处,如果是同步代码执⾏,就会直 接将该任务放在⼀个叫做 函数执⾏栈”的空间进⾏执⾏,执⾏栈是典型的【栈结构】(先进后出),程序在运⾏的时候会将同步代码按顺序⼊栈,将异步代码放到【⼯作线程】中暂时挂起,【⼯作线程】中保存的是定时任务函数、 JS 的交互事件、 JS的⽹络请求等耗时操作。当【主线程】将代码块筛选完毕后,进⼊执⾏栈的函数会按照从外到内的顺序依次运⾏,运⾏中涉及到的对象数据是在堆内存中进⾏保存和管理的。当执⾏栈内的任务全部执⾏完毕 后,执⾏栈就会清空。执⾏栈清空后, 事件循环 就会⼯作, 事件循环”会检测【任务队列】中是否有要执⾏的任务,那么这个任务队列的任务来源就是⼯作线程,程序运⾏期间,⼯作线程会把到期的定时任务、返回数据的http任务等【异步任务】按照先后顺序插⼊到【任务队列】中,等执⾏栈清空后,事件循环会访问任务队列,将任务队列中存在的任务,按顺序(先进先出)放在执⾏栈中继续执⾏,直到任务队列清空。
function task1()
console.log('第⼀个任务') 
function task2()
console.log('第⼆个任务') 
function task3()
console.log('第三个任务') 
function task4()
console.log('第四个任务') 
task1()
setTimeout(task2,1000)
setTimeout(task3,500)
task4()
我们创建了四个函数代表 4 个任务,函数本身都是同步代码。在执⾏的时候会按照 1 2 3 4 进⾏ 解析,解析过程中我们发现任务 2 和任务 3 setTimeout 进⾏了定时托管,这样就只能先运⾏任务 1 和任务 4了。当任务 1 和任务 4 运⾏完毕之后 500 毫秒后运⾏任务 3 1000 毫⽶后运⾏任务 2 图解分析:

如上图,在上述代码刚开始运⾏的时候我们的主线程即将⼯作,按照顺序从上到下进⾏解释执⾏,此时执⾏栈、⼯作线程、任务队列都是空的,事件循环也没有⼯作。接下来我们分析下⼀个阶段程序做了什么事情。

 结合上图可以看出程序在主线程执⾏之后就将任务14和任务23分别放进了两个⽅向,任务1和任务4都是⽴即执⾏任务所以会按照1->4的顺序进栈出栈(这⾥由于任务12是平⾏任务所以会先执⾏任务1的进出栈再执⾏任务4的进出栈),⽽任务2和任务3由于是异步任务就会进⼊⼯作线程挂起并开始计时,并不影响主线程运⾏,此时的任务队列还是空置的。

我们发现同步任务的执⾏速度是⻜快的,这样⼀下执⾏栈已经空了,⽽任务 2 和任务 3 还没有到时间,这样我们的事件循环就会开始⼯作等待任务队列中的任务进⼊,接下来就是执⾏异步任务的时候了。

我们发现任务队列并不是⼀下⼦就会将任务2和任务三⼀起放进去,⽽是哪个计时器到时间了哪个放进去,这样我们的事件循环就会发现队列中的任务,并且将任务拿到执⾏栈中进⾏消费,此时会输出任务3的内容。

到这就是最后⼀次执⾏,当执⾏完毕后⼯作线程中没有计时任务,任务队列的任务清空程序到此执⾏完毕。

宏任务和微任务

JavaScript 运⾏环境中,包括主线程代码在内,可以理解为所有的任务内部都存在⼀个微任务队列,在每下⼀个宏任务执⾏前,事件循环系统都会先检测当前的代码块中是否包含已经注册的微任务,并将队列中的微任务优先执⾏完毕,进⽽执⾏下⼀个宏任务。所以实际的 任务队列的结构是这样的,如图:

宏任务与微任务的介绍

由上述内容得知 JavaScript 中存在两种异步任务,⼀种是宏任务⼀种是微任务,他们的特点如下: 宏任务 宏任务是 JavaScript 中最原始的异步任务,包括 setTimeout setInterVal、I/O(I/O 包含 AJAX、事件注册,node中的fs文件读写)等,在代码执⾏环境中按照同步代码的顺序,逐个进⼊⼯作线程挂起,再按照异步任务到达的时间节点,逐个进⼊异步任务队列,最终按照队列中的顺序进⼊函数执⾏栈进⾏执⾏。 微任务 微任务是随着 ECMA 标准升级提出的新的异步任务,微任务在异步任务队列的基础上增加了【微任务】的概念,每⼀个宏任务执⾏前,程序会先检测中是否有当次事件循环未执⾏的微任务,优先清空本次的微任务后,再执⾏下⼀个宏任务,每⼀个宏任务内部可注册当次任务的微任务队列,再下⼀个宏任务执⾏前运⾏,微任务也是按照进⼊队列的顺序执⾏的。 总结 JavaScript 的运⾏环境中,代码的执⾏流程是这样的: 1. 默认的同步代码按照顺序从上到下,从左到右运⾏,运⾏过程中注册本次的微任务和后续的宏任务: 2. 执⾏本次同步代码中注册的微任务,并向任务队列注册微任务中包含的宏任务和微任务 3. 将下⼀个宏任务开始前的所有微任务执⾏完毕 4. 执⾏最先进⼊队列的宏任务,并注册当次的微任务和后续的宏任务,宏任务会按照当前任务队列的队尾继续向 下排列

常⻅的宏任务和微任务划分

宏任务

 

微任务

 

1

以上是关于JS异步编程1:认识JavaScript的事件循环模型的主要内容,如果未能解决你的问题,请参考以下文章

对javascript EventLoop事件循环机制不一样的理解

进阶学习5:JavaScript异步编程——同步模式异步模式调用栈工作线程消息队列事件循环回调函数

JavaScript 中的事件循环和 Node.js 中的异步非阻塞 I/O 有啥区别?

JavaScript异步编程

JavaScript中异步编程

JavaScript同步异步及事件循环