目录↑

JS运行机制、宏任务与微任务

By blockXun

JS是单线程的

avaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
——阮老师

JS运行机制

要了解事件循环和任务队列,弄明白JS的运行机制就明白了

  1. 所有的同步任务都在“主线程”上执行,形成一个“执行栈”。
  2. 在主线程之外,还有一个叫“任务队列”的东西,只要有异步任务执行完毕,就会把运行结果放在“任务队列”中“排队”。
  3. 当“执行栈”中的所有任务都执行完了,系统就会看看“任务队列”中有没有正在排队的事件,如果有就开始按照排队的顺序处理这些事件。

主线程: JS是单线程的,这个线程就被成为主线程,执行代码用的。主线程会先执行执行栈中的东西,当执行栈为空时,才会看任务队列中是否存在需要被执行的代码,如果有,就把任务队列中的东西按顺序拿到执行栈中执行
执行栈: 执行栈是一个用来存放等待执行的同步任务的地方,当我们调用一个方法(<script><function>等)时,JS会生成一个与这个方法相对应的执行环境,也叫执行上下文(执行环境不是执行栈),执行环境中存在着这个方法的私有参数、作用域等东西,因为JS是单线程的,所以当很多方法被依次调用时,JS会先 解析 这些方法,把同步任务放在“执行栈”中排队
任务队列: 任务队列也叫消息队列,回调队列,异步操作会被放在任务队列中等待执行

事件循环

在“JS运行机制”中,主线程每次清空一次“执行栈”后,就算是执行了一次事件循环。
主线程每次清空栈后,就去任务队列中检查是否有任务,如果有,就每次取出一个放在执行战中执行,一直循环,这个过程被称为“事件循环”

事件循环
经典老图.jpg

事件循环和宏任务、微任务的关系

事件循环和宏任务、微任务的关系
经典老图2.jpg

宏任务和微任务

宏任务: 每一次执行栈执行的代码就是一个宏任务

常见宏任务

  • script(整体代码)
  • setTimeout
  • setInterval

微任务: 在每一次宏任务执行结束后执行的任务(在下一个宏任务之前,也在渲染之前)

常见微任务

  • Promise.then
  • async await

执行顺序

执行栈 宏任务(执行栈) 微任务(如果有的话) 宏任务 微任务 宏任务 微任务
备注 最初js代码 任务队列中的宏任务放在执行栈中执行

例子

async function ac() {
    console.log('ac-start')
    await ac2();
    console.log('ac-end')
}

async function ac2 () {
    console.log('ac2')
}
setTimeout(() => {
    console.log('setTimeout')
    new Promise(resolve => {
        console.log('promise-body')
        resolve()
    }).then(() => {
        console.log('promise-then')
    })
}, 0)
setTimeout(() => { // setTimeout2
    console.log('setTimeout2')
})
ac()
console.log('window')
  • 首先是第一遍事件循环
  • 从第一行开始往下走,首先创建函数ac和函数ac2,没有调用,所以没有执行,
  • 往下走 看到了setTimeout,setTimeout的第二个形参为”0”,也就是说延时0秒执行,所以将它在0秒后排到任务队列中(0秒或者为空都不会立即执行,而是放在任务队列中)
  • 再往下走,又是一个setTimeout,我们暂时叫他setTimeout2,同样当做宏任务放在任务队列中
  • 接下来是ac(),调用ac函数,
  • 在ac函数中,首先输出log‘ac-start’,接下来是ac2(),开始执行ac2
  • ac2输出log‘ac2’,没有返回值,返回undefined,回到ac函数中
  • 由于ac函数时async函数,在await后的内容放在微任务队列中
  • ac()调用完毕后继续往下走,输出log‘window’
  • 主进程走完后,看一下是否有微任务,有的话按顺序执行微任务,于是开始继续执行ac函数,输出log‘ac-end’,然后ac函数返回undefined
  • 第一遍事件循环结束,现在输出ac-start — ac2 — window — ac-end
  • 第二遍事件循环,从任务队列中取第一个,也就是setTimeout中的函数,开始执行
  • 首先输出log‘setTimeout’
  • 往下走看见promise函数,执行函数中的内容
  • 第一行输出log‘promise-body’
  • 第二行promise函数返回resolve,把then中的内容放在微任务队列中
  • setTimeout执行结束,开始查看有没有微任务,执行微任务
  • 执行then中的函数,输出log‘promise-then’
  • 第二遍事件循环结束,现在输出ac-start — ac2 — window — ac-end — setTimeout — promise-body — promise-then
  • 第三遍事件循环,从任务队列中取出第一个(也是最后一个),setTimeout2中的函数,开始执行
  • 输出log‘setTimeout2’
  • 在之后没有宏任务额微任务了,执行结束
  • 最终输出 ac-start — ac2 — window — ac-end — setTimeout — promise-body — promise-then — setTimeout2