js的事件环(事件循环)机制

前言

js是一门单线程的语言,就是说,在js中,永远都只有一个主线程在处理任务。因为js的运行环境在浏览器中,浏览器需要随时与用户进行交互,需要进行各种各样的dom操作,如果js是多线程的,那么就有可能出现一个线程在删除dom,一个线程在操作这个dom,程序就会出现不可预料的bug。所以js选择只有一个主线程执行代码,保证程序执行的一致性。

web worker 技术在一定程度上解决了js单线程的问题,它新开了多个子线程帮助主线程处理任务,一定程度上加快了js处理任务的效率。但是这些子线程也有一些局限性,他们不能独立执行,并且也不能执行I/O操作。

虽然js是单线程的,但它有一个很重要的特点——异步非阻塞。那么,它作为单线程的语言,是如何实现异步非阻塞的呢?这就是这篇文章的主要讨论内容。

事件循环(事件环)

js 中的任务在大方向上分为两类:同步和异步。

其中,同步任务按顺序依次执行,而异步任务(如ajax请求数据)则在结果返回之前先处于挂起状态,等结果返回之后再执行回调函数中的逻辑代码。

那么,JavaScript引擎怎么对其进行控制呢?就是依赖于事件环机制,了解事件环机制之前首先需要引入两个概念——执行栈和事件队列。

执行栈

刚刚提到,js在执行同步代码时,会按照代码顺序依次执行,其实js在执行一个文件时,首先会解析其中的代码,将其中的同步代码依次放入执行栈中,然后从头开始顺序执行。

比如,js遇到一个方法需要执行,那么它会将这个方法的执行环境(方法的作用域、上层作用域的指向、方法的参数、方法中的局部变量、这个作用域的this对象)添加到执行栈中,然后进入这个执行环境执行其中的代码,执行完后,js就会退出这个执行环境并将其销毁,返回上一层的执行环境。这个过程会一直循环,直到执行栈中的代码全部执行完毕。

事件队列

而异步任务,js 会在其返回结果之前将其挂起,继续执行执行栈中的后续代码,当这个异步任务返回结果后,js 会将这个异步任务加入到 事件队列 中,等待当前执行栈中的所有任务都执行完毕后,主线程处于闲置状态,它就会去查找事件队列中的任务,然后按照先进先出的顺序,取出之前放入的异步事件,并把对应事件的回调函数放入执行栈中,开始执行其中的同步代码。这个过程不断重复,就形成了一个无限循环,这个过程也称之为“事件循环”。

异步任务分类:微任务和宏任务

异步任务还可细分为 微任务( micro task ) 和 宏任务( macro task ),setInterval() 和 setTimeout() 方法属于宏任务,Promise 方法属于微任务。

相同的,事件队列也分为微任务队列和宏任务队列。微任务的执行优先级高于宏任务。即在当前执行栈为空时,主线程会优先查看微任务队列中的事件,依次执行对应事件的回调函数,直到微任务队列为空,再去宏任务队列查找、处理事件。

扩展:node 环境下的事件循环机制

node 环境中的事件循环机制其实与浏览器中的大致一致,只是依靠的引擎不同,node 中的事件循环依靠 libuv 引擎。chrome v8 引擎作为node的解释器,将node中的js代码分析后去调用对应的node api,这些api最终都由libuv 引擎驱动,执行对应任务,对其进行区分,放在不同的队列中等待主线程执行。

从上图可看出 node 中事件循环的顺序,从incoming->poll->check->close callbacks->timers->I/O callbacks->idle,prepare

各个阶段的大致功能:

  • poll:处理I/O事件;查看 poll queue 中是否有待处理的任务,如果有则按先进先出的顺序执行

  • check:执行 setImmediate() 的回调函数;poll queue中的任务处理完毕后,进入该阶段

  • close callbacks:执行 close 事件的回调函数;当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去。

    process.nextTick():用来推迟任务与执行,效果与定时器和setImmediate()类似
    
    使用process.nextTick()发送的事件会存入 nextTick queue 中,这个队列中的事件会在每一个阶段执行完毕,准备进入下一个阶段之前优先执行,即当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先将其中的任务执行完毕
    
    setTimeout中的回调会在我们所指定的时间间隔后第一时间去执行(timers 阶段)
    
    setImmediate是在固定的阶段执行(check 阶段)
    
    所以 setTimeout 和 setImmediate 的执行顺序并不固定,但可以确定的是,在一个I/O事件的回调函数中,它们的顺序是固定的,即 setImmediate 在 setTimeout 之前,因为 check 阶段在 timers 阶段之前执行
    
  • timers:执行 setInterval() 、setTimeout() 定时器中的回调函数

  • I/O callbacks:执行大部分事件的回调函数,除了 close 事件、定时器 和 setImmediate()

  • idle,prepare:在node内部使用

posted @ 2021-08-09 09:19  Upward123  阅读(221)  评论(0编辑  收藏  举报