JS是单线程语言

JS是一种单线程(single thread)语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,从上往下一行一行进行执行,否则会带来很复杂的同步问题。

  • JS是单线程语言,只能同时做一件事儿
    • 异步不会阻塞代码执行
    • 同步会阻塞代码执行
  • JS和DOM渲染共用同一个线程,因为JS可修改DOM结构
  • DOM事件也使用回调,基于event loop

Event loop

event loop

由图所知 event loop 是由 JS RunTime(主线程)、Web Apis (外部API)以及 Callback Queue(任务队列) 组成的。

  • stack:用于分配原始类型
  • heap:用于分配对象类型
  • queue:先进先出,排在前面的事件,优先被处理。

同步代码,一行一行放在主线程执行,会将不同类型的数据保存到堆和栈中。遇到异步也就是调用各种不同的Web Apis,会先“记录”下,等待时机(定时、网络请求等)。时机到了,就添加到到Callback Queue。如主线程为空(即同步代码执行完)Event Loop开始工作,循环查找Callback Queue,如有则移动到主线程执行,然后继续循环查找。

宏任务微任务

任务队列有宏任务和微任务之分:

  • 宏任务队列(macro task queue):script(整体代码),ajax、setTimeout、setInterval、DOM监听、UI Rendering 等

  • 微任务队列(micro task queue):Promise 的 then 回调、 Mutation Observer API、queueMicrotask() 等

事件循环执行的优先级:主程序 —> 微任务 —> 宏任务

1、main script 中的代码优先执行(编写的顶层script代码);

2、先执行微任务,当微任务队列为空后再执行宏任务

举个例子

async function async1 () {
  console.log('async1 start')
  await async2();
  console.log('async1 end') // 加入到微任务队列
}

async function async2 () {
  // return undefined --> new Promise,因为async 函数返回一定是个 promise
  console.log('async2') 
}

console.log('script start')

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

async1();

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

console.log('script end')
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

先执行 script (整体代码),第一步执行console.log('script start')asycn1() 函数和async2()函数只是已定义但还没声明,所以先不管它们。console.log('script start')执行完后将跳出函数执行栈。

event_loop_01

遇到了setTimeout函数,进入marco task 队列:

event_loop_02

然后执行async1()函数,打印 'async1 start' 将 await 后面的代码 console.log('async1 end') 加入到 micro task队列中

event_loop_03

执行async2()函数,打印 'async2' 后再执行 promise 函数

event_loop_04

再执行promise函数后会打印 promise1,后 promise 状态转变为 resolve,将console.log('promise2')加入micro task 队列。最后再执行 console.log('script end')

event_loop_05

至此主程序执行完毕,就先执行micro task 队列里的函数,主程序执行完毕后,再执行macro task 队列里的函数。

PS:零延迟

当 setTimeout 函数的 delay 为零的时候并不意味着回调会立即执行。其等待的时间取决于队列里待处理的消息数量。因为只有队列里的所有任务都处理完后才会处理 setTimeout 函数。

Reference

JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志 (ruanyifeng.com)

The event loop - JavaScript | MDN (mozilla.org)

前端基础进阶(十二):深入核心,详解事件循环机制 - SegmentFault 思否