JavaScript是一个单线程的脚本语言。它在执行任务时需一个一个的执行,那么如何保证任务有序的执行而不阻塞呢。这就需要任务队列和事件循环(EventLoop)了。
# 任务队列(消息队列)
队列
是一种先进先出(FIFO-first in first out)的数据结构。任务队列就是以先进先出的原则按顺序执行队列中的任务。有新任务时IO线程就将任务添加在队列尾部,要执行任务时就从队列头部取出执行。
但是队列中任务类型太多,而且是多个线程操作同一个任务队列,比如鼠标滚动、点击、文件读写、DOM解析、JS执行等等......
这些任务都在队列中一个一个按顺序的执行,一个任务执行需要等前面的任务执行完,所以会存在单个任务执行时间过程过久导致队列阻塞的问题。
比如如果一个DOM渲染的任务前有一个执行很长时间的任务,那么我们看到的页面就会卡住等待这个任务执行完才会显示DOM渲染的内容,这样给用户的体验很不好。
所以 为了处理高优先级的任务,和解决单任务执行时间过长的问题
,JS对任务进行了划分,分为微任务和宏任务。
在说微任务和宏任务之前,要先知道一个概念就是 同步
和 异步
# 同步和异步
前面说到任务队列中的任务如果存在需要执行很久的任务时,主线程的任务队列执行会出现阻塞,所以JS将任务分为了同步任务和异步任务
同步任务:
不需要等待可立即执行得到结果的任务异步任务:
任务执行需等待一段时间才能得到结果(如:定时器、ajax、事件绑定、回调函数、async await、promise 等)
在进程执行任务时若遇到异步任务,这个时候会把这个任务发到专门处理异步任务的模块,然后接着执行后面的任务,这样就不会出现阻塞的情况了。
也就是说,除了任务队列,还有一个专门处理需要延迟执行的模块(延迟哈希表)
接下来我们再来了解微任务和宏任务
# 微任务和宏任务
JS执行时,V8会创建一个全局执行上下文,在创建上下文的同时,V8也会在内部创建一个 微任务队列
和 宏任务队列
,在宏任务执行过程中如果有新的微任务产生,就添加到微任务队列中
微任务包含:
promise回调、proxy、MutationObserver(监听DOM)、node中的process.nextTick 等宏任务包含:
渲染事件、请求、script、setTimeout、setInterval、node中的setImmediate、I/O 等
# 事件循环(EventLoop)
事件循环其实就是JS的运行机制,结合上述概念简单的说就是,将任务队列一个一个取出放入执行栈中执行,即:一个宏任务,所有微任务,渲染,一个宏任务,所有微任务,渲染......
循环过程:
- 所有同步任务都在主线程上依次执行,形成一个执行栈(调用栈),异步任务处理完后则一个一个放入任务队列
- 当执行栈中任务执行完后,再去检测微任务队列里的任务是否为空,有就执行,如执行过程中又产生新的微任务,就添加到微任务队列末尾继续执行,直到把微任务执行完
- 微任务执行完后,再到任务队列中取出最先进入队列的宏任务,压入执行栈中执行其同步代码。
- 然后回到第2步执行所有的微任务,如此反复,知道宏任务执行完,如此循环