前言
本文试图讲解 JavaScript 的执行机制,理解事件循环,读完这篇文章,本文围绕以下两个问题讲解:
- 为什么 JavaScript 是单线程和异步?
- JavaScript 如何实现异步?
知识储备
- 进程、线程
- 进程阻塞
- 阻塞 IO 和非阻塞 IO
- 同步IO与异步IO
JavaScript 的特点
单线程
JavaScript 的核心之一是 DOM 对象,在 jQuery 时代 DOM 是 JavaScript 的直接操作单元,试想如果是多线程操作 DOM 会是怎样?如下场景:
场景描述:
DOM 对象中含有一个 a 节点,如果 process1 对 a 节点进行删除操作, 而 process2 对 a 节点进行编辑操作,那么浏览器该如何保证 JavaScript 有条不紊的执行?
多线程的读写操作中,通常是通过加锁保证资源的一致性(状态同步),可是控制如此复杂的 DOM 结构岂是简单的加锁就能解决?因此,JavaScript 就索性使用单线程机制。 单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销。
异步
基于单线程条件下如果采取同步等待执行任务,那么那些复杂的 DOM 操作以及受外界影响的 IO 操作会导致执行阻塞,用户会感觉到页面很卡顿,从而导致用户体验很差,所以 JavaScript 执行引擎采取了单线程异步执方案。
单线程异步实现原理
综上述,我们已经知道, JavaScript 的两大特点就是单线程、异步,那么如何在单线程中实现异步呢?这就是本文的重点。
However, 如果所有的操作(包括IO/定时器/文件读写)都在只有一个线程来完成,那么就算实现完美的异步(CPU 100%利用,没有空闲)也将很慢。所以 JavaScript 所谓的单线程是指 JavaScript 的解释引擎中执行 JavaScript 代码的线程只有一个,称作主线程,除此之外,还存在一些负责 AJAX 请求、定时器、IO 操作的线程,这些线程称作工作线程。
任务分类
严格来讲,执行引擎将 JavaScript 代码分为两类,宏任务和微任务:
- macro-task(宏任务):包括整体代码(script 标签),定时器(setTimeout、setInterval)
- micro-task(微任务):Promise,process.nextTick
事件循环
主线程执行,遇到异步任务时,有工作线程执行异步任务,并在主线程中注册回调函数
在 JavaScript 执行之前,会先将任务放入宏任务队列和微任务队列中。一次 JavaScript 执行按照如下流程进行,宏任务作为 JavaScript 的执行单位开始执行,宏任务执行结束检查是否有可执行的微任务,如果有则执行所有的微任务,如果没有则结束本次执行。
这只是一次 JavaScript 的执行片段,所有微任务执行完毕后(如果有)JavaScript 引擎会重复以上步骤,直到所有的代码都执行完,这个过程就是事件循环(Eventloop)。
案例分析
1 | // 开始事件循环 |
主线程执行同步任务(形成一个执行栈)输出 1 , setTimeout1 进入异步宏过任务队列,
process1 进入异步微任务队列,遇到 new Promise 输出 7, then 进入异步微任务队列
setTimeOut2 进入宏任务队列, 处理微任务队列 process1 和 promise1.then 输出 6,8开始第三次事件循环处理setTimeout1 ,主线程输出 2 , 4 然后处理异步微任务输出 3,5
开始第四次事件循环处理 setTimeout2 ,主线程输出 9, 11 然后处理异步微任务输出 10, 12