JS学习笔记——事件循环机制Event loop(面试必问)

不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。

Event Loop 这个概念相信大家或多或少都了解过,但是有一次被一个小伙伴问到它具体的原理的时候,感觉自己只知道个大概印象,于是计划着写一篇文章,用输出倒逼输入,让自己重新学习这个概念,同时也能帮助更多的人理解它。

1、JavaScript单线程

先简单介绍几个相关概念:

  • 进程: cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)。比如你正在运行的浏览器,它会有一个进程
  • 线程: cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。线程是负责执行代码的
  • 单线程从头执行到尾,一行一行执行,如果其中一行代码报错,那么剩下代码将不再执行。同时容易代码阻塞
  • 多线程: 代码运行的环境不同,各线程独立互不影响,避免阻塞。

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事,代码执行是同步并且阻塞的。

那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript原因:

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2、任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

比如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

2.1、堆、栈、队列

同样,先介绍接个相关概念:

堆(heap)

  • 对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域
  • 堆是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
  • 堆是线性数据结构,相当于一维数组,有唯一后继

(最大堆)
在这里插入图片描述

(最小堆)
在这里插入图片描述
栈(stack)

  • 栈是一种 LIFO(Last In, First Out)的数据结构,特点即后进先出。是限定仅在表尾进行插入删除操作的线性表。
  • 栈按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。
  • 栈是仅在某一端(表尾)进行插入或删除操作的线性表

在这里插入图片描述

执行栈(stack)/ 调用栈(call stack)

  • 本质上当然是个栈,关键在于它里面装的是一个个待执行的函数
  • 运行同步代码。执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。

队列(Queue)

  • 只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。和栈一样,队列是一种操作受限制的线性表
  • 队列是先进先出(FIFO—first in first out),队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除

在这里插入图片描述
任务队列(callback queue)

  • 本质上当然是个队列,是一个先进先出的数据结构

  • "任务队列"是一个事件的队列(也可以理解成消息的队列),里面存放异步任务,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击页面滚动等等)

  • 异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。(所谓"回调函数"(callback),就是那些会被主线程挂起来的代码

  • 主线程读取"任务队列",就是读取里面有哪些事件

2.2、同步任务和异步任务

于是,从广义上讲,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)

  • 同步任务:指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 异步任务:指的是不进入主线程,而进入"任务队列"(task queue)的任务,只有"任务队列"(task queue)通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。
在这里插入图片描述
上图可以用下面文字来解释:

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

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

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

主线程不断重复上面的第三步。也就是常说的Event Loop(事件循环)

JS的执行机制是:
在这里插入图片描述
上图可以用下面文字来解释:

(1)首先判断JS是同步和异步任务,同步的进入主线程,异步的进入Event Table并注册函数

(2)异步任务在Event table中注册函数,当满足触发条件后,会将这个函数推入Event queue

(3)主线程内的任务执行完毕为空(此时JS引擎空闲),会去Event Queue读取对应的函数,进入主线程执行

上述过程会不断重复,也就是常说的Event Loop(事件循环)
2.3、宏任务和微任务

除了广义上的定义,我们可以将异步任务进行更精细的定义,分为宏任务微任务

宏任务:

script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate   (Node.js 环境,浏览器暂时不支持)

(macro)Task可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得JS内部(macro)TaskDOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)Task -> 渲染 -> (macro)Task -> ······

微任务:

Promise.then
Object.observe  (已废弃;`proxy`代替)
MutationObserver
process.nextTick  (Node.js 环境)

microtask可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

2.4、JS运行机制

JS运行机制:

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

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

在这里插入图片描述
在这里插入图片描述

注意:

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行宏任务(Task/macro Task),否则就一次性执行完所有微任务。

每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。

3、案例

3.1、案例一

(案例来源:Tasks, microtasks, queues and schedules

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

第一次执行:

Tasks(宏任务):run script、 setTimeout callback

Microtasks(微任务):Promise then	

JS stack(执行栈): script	

Log(控制台打印): script start、script end

执行完同步代码,然后将宏任务(Tasks)微任务(Microtasks)划分到各自队列中。

第二次执行:

Tasks:run script、 setTimeout callback

Microtasks:Promise2 then	

JS stack: Promise2 callback	

Log: script start、script end、promise1、promise2

执行宏任务(即script)后,检测到本次宏任务执行过程中产生的微任务(Microtasks)队列中不为空,执行Promise1,执行完成Promise1后,调用Promise2.then,放入微任务(Microtasks)队列中,再执行Promise2.then

第三次执行:

Tasks:setTimeout callback

Microtasks:	

JS stack: setTimeout callback

Log: script start、script end、promise1、promise2、setTimeout

这时第一次宏任务(Tasks)和其产生的微任务(Microtasks)已经执行完毕,所以此时微任务(Microtasks)队列为空,当微任务(Microtasks)队列中为空时,继续执行下个宏任务(Tasks),执行setTimeout callback,打印日志。

四次执行:

Tasks:setTimeout callback

Microtasks:	

JS stack: 

Log: script start、script end、promise1、promise2、setTimeout

清空Tasks队列和JS stack

gif图来源 : 掘金

在这里插入图片描述

3.2、案例二(async/await)
console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
.then(function() {
  console.log('promise1')
})
.then(function() {
  console.log('promise2')
})

console.log('script end')

关于async/await,之前我也写过一篇博客,可以参考JS学习笔记——异步回调中Async Await和Promise区别

async/await 在底层转换成了 promisethen 回调函数,是 promise 的语法糖。

每次我们使用 await,解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。

async/await 的实现,离不开 Promise。从字面意思来理解,async“异步”的简写,而 awaitasync wait 的简写,可以认为是等待异步方法执行完成

打印结果(当前谷歌浏览器版本: 94.0.4606.81(正式版本)

script start
async2 end
 Promise
script end
async1 end
promise1
promise2
setTimeout

本篇博客参考文章:

  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值