Event Loop的理解

127 阅读7分钟

谈到JavaScript的运行机制,Event Loop是一个绕不开的话题.想要理解这个问题,首先要明白以下几个名词的含义:

程序中的堆、栈、队列

Heap(堆):堆是一种动态的存储结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆是线性数据结构,相当于一维数组,有唯一后继。

Stack(栈):栈在程序中的设定是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照 后进先出 (LIFO: last-in-first-out)的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。 栈是 只能在某一端插入和删除的特殊线性表

Queue(队列):队列特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。 进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列。

队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO: first-in-first-out)

JavaScript语言中的堆、栈、队列

堆, 动态分配的内存,大小不定也不会自动释放,存放引用类型,指那些可能由多个值构成的对象,保存在堆内存中,包含引用类型的变量,实际上保存的不是变量本身,而是指向该对象的指针。可以简单理解为存储代码块

js中的栈准确来将应该叫调用栈(EC Stack),会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。

栈的作用:存储基本类型值,还有一个很要的作用。提供代码执行的环境

队列, js中的队列可以叫做任务队列或异步队列,任务队列里存放各种异步操作所注册的回调,里面分为两种任务类型,宏任务(macroTask)和微任务(microTask)。

为什么会出现Event Loop 呢?

众所周知的JS是一门单线程的非阻塞脚本语言,Event Loop就是为了解决JS异步编程的一种解决方案

JS为什么是单线程语言,它又是怎么实现异步编程的呢?

第一个问题: JavaScript的诞生就是为了处理浏览器网页的交互(DOM操作的处理、UI动画等), 设计成单线程的原因就是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果(两个线程修改了同一个DOM节点就会产生不必要的麻烦),这对于一种网页脚本语言来说这就太复杂了。

第二个问题: JavaScript是单线程的但它所运行的宿主环境—浏览器是多线程,浏览器提供了各种线程供Event Loop调度来协调JS单线程运行时不会阻塞。

    个人对JS运行机制的理解:代码执行开启一个全局调用栈(主栈)提供代码运行的环境,在执行过程中同步的代码立即执行,遇到异步任务的回
调注册到任务队列中,等待同步代码执行完毕查看异步是否完成,如果完成,则将当前的异步任务回调拿到主栈中执行

JavaScript单线程是指JavaScript引擎只有一个线程,这样就意味着所有的任务需要排队,前一个任务结束才会执行后一个任务.如果前一个任务耗时过程后一个任务就不得不一直等着.js引起执行异步代码而不需要一直等待是因为有任务队列和事件轮询:

任务队列:任务队列是一个先进先出的队列,它里面存放在各种任务回调

事件轮询:事件轮询是指主线程重复从任务队列中取任务,执行任务的过程

浏览器中Event Loop执行顺序

Processing model 规范定义了Eveent Loop的循环过程:

一个Event Loop只要存在,就会不断执行下边的步骤:

  • 在tasks(任务)队列中选择最老的一个task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到下边的microtasks步骤
  • 将上边选择的task设置为 正在运行的task
  • Run: 运行被选择的task。
  • 将Eveent Loop的currently running task变为null。
  • 从task队列里移除前边运行的task。
  • Microtasks: 执行microtasks任务检查点。(也就是执行microtasks队列里的任务)
  • 更新渲染(Update the rendering):可以简单理解为浏览器渲染...
  • 如果这是一个worker event loop,但是没有任务在task队列中,并且WorkerGlobalScope对象的closing标识为true,则销毁Eveent Loop,中止这些步骤,然后进行定义在Web workers章节的run a worker
  • 返回到第一步

宏任务和微任务

  在任务队列(queue)中注册的异步回调又分为两种类型,宏任务和微任务.我们为了理解方便
  可以认为任务队列中中宏任务队列和微任务队列.宏任务队列有多个,微任务队列只有一个
  
  • 宏任务 1. script(整体代码)
    2.setTimeout / setInterval 3.setImmediate(Node环境) 4.UI 渲染 5.requestAnimationFrame

  • 微任务

    1.Promisethen()、catch()、finally()里面的回调
    2.process.nextTick(Node 环境)
    

我理解中的JavaScript执行顺序: - 代码从开始执行调用一个全局执行栈,script标签作为宏任务执行. - 执行过程中同步代码立即执行,异步代码放到任务队列中,任务队列存放有两种类型的异步任务:宏任务队列和微任务队列 - 同步代码执行完毕也就意味着第一个宏任务执行完毕(script)

     1.先查看任务队列中的微任务队列是否存在宏任务执行过程中所产生的微任务
     
        1-1 若有的话就将微任务队列中的所有微任务清空,微任务执行过程中所产生的微任
        务放到微任务队列中,在此次执行过程中也一并清空
     
     	1-2 若没有的话再去宏任务队列看有没有宏任务,有的话执行,没有的事件轮询第一波结束
 

举个例子

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。

async/await 在底层转换成了 promise 和 then 回调函数。 也就是说,这是 promise 的语法糖。 每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。 async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。