JS运行机制

2022-11-29 16:45:03 浏览数 (1)

本文阐述了浏览器端和node端的js运行机制执行的过程,还进行了两者的运行机制比较,以及同步任务和异步任务的说明,两种异步任务的必要性,以及各自有哪些回调,部分回调的优先级。

JS运行机制复述

首先js执行,会有一个函数执行栈(stack),一个任务队列(task queue),一个微任务队列(microtask queue),事件循环(event loop)。

  • 主线程:函数执行栈用来存放同步任务,按照后进先出的顺序执行;
  • 在任务队列中,存放的是宏任务。
  • 当函数执行栈为空时,会启动事件循环机制,将任务队列放到执行栈中执行。在此之前,每从任务队列中取一个任务时,如果微任务队列中存在任务,就先把微任务执行完成,在执行任务队列中的任务。
  • 依次循环,直到任务队列、微任务队列、函数执行栈均为空。

附:

「同步任务」:在主线程上执行的任务,只有前一个任务执行完成后才能执行下一个任务。 「异步任务」:不进入主线程执行,而是进入到任务队列(task queue)中执行。

Node.js中的事件循环

上段讲的是浏览器端的事件轮询,而node是多线程机制,由libuv库负责Node API的执行,将它分配给不同的线程,形成一个事件循环。

node中事件循环(event loop)大致分为六个部分。

  • timer定时器:执行setTimeout以及setInterval的回调。
  • I/O回调:处理网络、流、tcp错误等回调
  • idle空转和prepare阶段:node内部使用
  • poll轮询:执行poll中的I/O队列,检查定时器是否到时
  • check检查:存放setImmediate回调
  • close回调:关闭的回调

主要的是timer定时器、poll轮询、check 检查三大部分。在此我们只做了解。

事件循环过程:

  • 执行全局Script的同步代码。
  • 执行完同步代码调用栈清空后,执行微任务。「先执行NextTick队列」(NextTick Queue)中的所有任务,再执行其他微任务队列中的所有任务。
  • 开始执行宏任务,上面6个阶段。从第1个阶段开始,执行相应每一个阶段的「宏任务队列中所有任务」。(每个阶段的宏任务队列执行完毕后,开始执行微任务),然后在开始下一阶段的宏任务,依次构成事件循环。
  • timers Queue -> 执行微任务 -> I/O Queue -> 执行微任务 -> Check Queue 执行微任务 -> Close Callback Queue -> 执行微任务 ...

浏览器和Node端事件循环的差别

  • 两者的运行机制完全不同,实现机制也不同。
  • node.js可以理解成4个宏任务队列(timer、I/O、check、close)和2个微任务队列。但是执行宏任务时有6个阶段。
  • node.js在开始宏任务6个阶段时,每个阶段都将该宏任务队列中所有任务都取出来执行,每个阶段的宏任务执行完毕后,开始执行微任务。但是浏览器中的事件循环,是只取一个宏任务执行,然后看微任务队列是否存在,存在执行微任务,然后再取一个宏任务,构成循环。

JS异步任务

js的异步任务分为两种:宏任务、微任务。一个宏任务里面可以拥有多个微任务,在执行js代码块的时候才会去执行内部的微任务。

宏任务

macrotask,也叫tasks。一些异步任务的回调会依次进入宏任务队列,等待后续背调用。

宏任务包括:

  • setTimeout/setInterval
  • setImmediate(Node独有)
  • requestAnimationFrame(浏览器独有)
  • I/O
  • UI rendering(浏览器独有)

注意: 1、setTimeout延迟时间为0,与requestAnimationFrame比较:「requestAnimationFrame优先级大于setTimeout」

2、setTimeout延迟时间为0,与setImmediate比较:「不确定」

代码语言:javascript复制
setTimeout(() => console.log('setTimeout'), 0)
setImmediate(()=>{
  console.log('setImmediate');
})

解释: timer前的准备时间超过1ms,(loop到timer的时间大于1ms),则执行timer阶段(setTimeout)的回调函数。 timer前的准备时间小于1ms,则先执行check阶段(setTimeout)的回调函数,下次事件循环,再执行timer阶段的回调函数。

如果想要setImmediate先执行,可以使用fs文件包裹,确保在I/O回调阶段执行。这样时间循环,会先执行chack阶段,之后再执行timer阶段。

node版本中的setTimeout

代码语言:javascript复制
setTimeout(() => {
  console.log(1)
})
setTimeout(() => {
  console.log(2)
  Promise.resolve().then(function () {
    console.log('promise')
  })
})
setTimeout(() => {
  console.log(3)
})
  • node11以后的版本与浏览器端运行结果一致:1 2 promise 3。
  • node11之前的版本,执行结果为:1 2 3 promise。它会先进入timer阶段,执行第一个setTimeout并打印。再执行第二个setTimeout并打印,并将Promise放入微任务队列中。然后执行第三个setTimeout并打印。事件循环在执行下一阶段时,先执行微任务队列,打印promise。

微任务

microtask,也叫jobs。除宏任务外的一些异步回调会依次进入微任务队列,等待后续被调用。微任务包括:

  • process.nextTick(Node独有)
  • Promise.then()
  • Object.observe
  • MutationObserve

注意: process.nextTick优先级高于Promise.then()。

两种异步任务的必要性

在异步任务队列中,遵循先进先出的原则。此时,在众多异步任务中,如果存在优先级较高的任务需要优先执行,那么只有一个异步任务队列是无法满足的,此时就需要引入微任务队列,将优先级较高的任务放到微任务队列中。如果微任务队列非空,则执行微任务队列,否则执行宏任务队列。 如果只有一种异步任务,那么优先级高的异步任务无法优先执行。

补充

async/await

async/await本质上还是基于Promise的一些封装

「async」函数在await之前的代码都是「同步」执行的,可以理解为await之前的代码属于new Promise时传入的代码,「await之后的所有代码都是在Promise.then中的回调」

0 人点赞