JavaScript异步机制 | 知识整理

829 阅读5分钟

写这篇文章主要是帮助自己梳理知识点,文章的大部分内容来源于其他关于异步和事件循环的文章,文章末尾附上链接。

如何理解javascript是单线程语言

什么是单线程?我理解的单线程是这样的,现在有A事,B事,C事需要做完,但是只有甲这一个人做,而多线程就是除了甲,还有乙可以做这些事。

再看看js的通常行为,比如js是甲,甲现在做完了A(console.log('hello')), 现在做B(一个http请求),发现B比较耗时,把B交给了乙,接着做完了C(console.log('world')),这是发现乙已经把B做的差不多了(拿到数据了),甲接着把B做完,这样看来好像js不是单线程语言。

其实我们所说的javascript是单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程, 这里我们称它为主线程。而像处理DOM事件的线程,处理AJAX请求的线程,这里我们称它为工作线程。下面我列出浏览器端和Node服务器端都有那些工作线程。

浏览器端

  • 渲染引擎线程:该线程负责页面的渲染
  • 定时触发器线程:处理定时事件,比如setTimeout, setInterval
  • 事件触发线程:处理DOM事件
  • 异步http请求线程:处理http请求

Node服务端

具体我不清楚,我就列出来,不知道对不对

  • timers
  • I/O callback
  • idle,prepare
  • poll
  • check

异步过程的构成

从主线程的角度看,一个异步过程包括下面两个要素:

  • 发起函数(或叫注册函数)
  • 回调函数callbackFn

它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。 比如:

// 其中的setTimeout就是异步过程的发起函数,fn是回调函数
setTimeout(fn, 1000);

消息队列和事件循环

上文讲到,异步过程中,工作线程在异步操作完成后需要通知主线程。那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。

主线程首先会执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。

工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。

  • 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
  • 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的回调函数。

还是上面那个例子:

// 这里可以理解为: 1000ms后消息队列,将进入一个消息,这个消息的内容为执行fn
setTimeout(fn, 1000);

宏任务和微任务

宏任务(MacroTask)指的是上面的工作线程执行的任务后的回调函数,同时宏任务(MacroTask)也存在着优先级,比如当主线程(js引擎)空闲时查看消息队列,会先看事件触发线程有没有消息,然后再接着再处理其他异步任务。

微任务(MicroTask)相对于宏任务存在,对于每个宏任务而言,其内部都有一个微任务队列。微任务(MicroTask)会放到当前宏任务的末尾,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务,执行完成才去执行下一个宏任务,常见的微任务有MutationObserver、Promise.then(或.reject)。

检验下是否掌握了

console.log('script start 同步任务')
async function async1(){
    console.log('async1 start 同步任务')
    await async2()
    console.log('async1 end 微任务')
}

async function async2(){
    console.log('async2 同步任务')
}

setTimeout(() => {
    console.log('setTimeout 宏任务')
})

async1()

new Promise((resolve) => {
    console.log('promise1 同步任务')
    resolve()
})
.then(() => {
    console.log('promise2 微任务')
})

document.addEventListener('click', function(){
    console.log('click 事件任务');
})
;(() => {
   // 同步任务阻塞5秒,请在5秒内,点击一下,
   // 可以看到事件线程的相关代码 在定时任务setTimeout后面,打印却在前面。
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('同步任务完成,当前宏任务结束,开始执行微任务'); 
})()
console.log('script end 同步任务')

结果

我5秒内疯狂点击屏幕12次, 我这手速👻👻👻

结果分析

  1. 首先执行同步任务(可以把上面整段代码看成一个宏任务)
script start 同步任务
async1 start 同步任务
async2 同步任务
promise1 同步任务
同步任务完成,当前宏任务结束,开始执行微任务 // 阻塞5秒,假设期间,你点了屏幕的任意位置了
script end 同步任务
  1. 执行微任务(同步任务完成后检查微任务队列)
async1 end 微任务
promise2 微任务
  1. 执行优先级高的宏任务(比如上面的点击事件)
click 事件任务 // 假设你在同步阻塞期间,你点击了屏幕的任意位置。
  1. 执行一般的宏任务
setTimeout 宏任务

我翻阅(chao xi)的文章

JavaScript异步机制详解

JavaScript:彻底理解同步、异步和事件循环(Event Loop)