前言
某些耗时任务,如setTimeout、网络请求等往往是需要等待的,但这并不会使得“单线程”的js受到阻塞,这可不就是因为Eventloop吗,即我们常说的事件循环。
先梳理几个基本概念。
单线程与多线程
说起JavaScript的一大特点,你可能会脱口而出“单线程”,那它为何不是多线程的呢?想想JavaScript的用途,它作为浏览器脚本语言,是用于解决页面交互,操作DOM的,如果同时存在多个线程去操作页面,那页面岂不乱哉!
堆、栈与队列
- 堆(Heap):树形结构,有最大堆、最小堆
- 栈(Stack):FILO(先进后出)
- 队列(Queue):FIFO(先进先出)
JS的事件循环机制(Event Loop)
事件循环机制由事件触发线程控制,所有的任务都在调用栈(call stack) 中等待主线程执行,先执行的函数先入栈到栈顶,当函数执行完就出栈。
同步任务
只用同步任务时,函数的调用比较简单,看一个例子:
function multiply(a, b) {
return a * b
}
function square(a) {
return multiply(a, a)
}
function printSquare(a) {
let result = square(a)
console.log(result)
}
printSquare(10)
入栈和出栈顺序:
根据栈FILO的原则 multiply 先出栈,直到栈被清空。
异步任务
下面这段代码加入了setTimeout、Promise
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(res => {
console.log(3)
}).then(res => {
console.log(4)
})
console.log(5)
大家都知道打印结果不是按顺序输出的,而是1、5、3、4、2,setTimeout和Promise实现了异步,那么这里就不仅仅是调用栈的问题了,尽管最终它们也会在调用栈中执行。js运行环境的运行机制是怎样的呢,一言不合先上图:
在js主线程中堆(Heap)主要负责内存分配,我们这里主要关注调用栈。
结合上面的图示模型,那么上面异步任务的例子是如何运行的?
- js开始执行。
- console.log(1)入栈并打印 1
- console.log(1)出栈,setTimeout入栈
- setTimeout回调放入浏览器提供的web APIs中,其回调进入宏任务队列,setTimeout出栈
- Promise入栈,Promise cb1(第一个then) 放入web APIs中,进入微任务队列
- Promise出栈,console.log(5)入栈
- 打印5之后,console.log(5)出栈
- 最后主线程执行执行完毕,main()出栈,栈空。此时的状态如下:
- 栈空,任务队列中微任务队列不为空
- Promise.then 1入栈,console.log(3)入栈,打印3
- console.log(3)、Promise.then 1依次出栈,Promise.then 2入栈
- Promise.then 2放入web APIs,回调进入微任务队列
- 检查到微任务队列不为空
- Promise.then 2入栈,console.log(4),打印4
- console.log(4)、Promise.then 2依次出栈
- 微任务队列为空,检查宏任务队列
- setTimeout cb入栈
- console.log(5)入栈,打印5
- console.log(5)出栈
- setTimeout cb出栈
至此,栈空。
整个过程可以用下图描述:
注意,当主线程的所有同步任务执行完后,会先检查是否有微任务,如果有微任务会依次执行微任务,执行完当前所有的微任务之后再执行下一个宏任务。
宏任务与微任务
- 宏任务(Task):setTimeout setInterval、setImmediate、requestAnimationFrame、I/O、UI rendering ...
- 微任务(MicroTask):Promise、Process.nextTick(Node独有)、MutationObserver...
总结
- JavaScript的用途决定了它是单线程的,但是同时它又是非阻塞的,因为存在事件循环机制。
- 浏览器提供了JS引擎线程去解释和执行JS代码,但是也提供了事件触发线程、定时器触发线程等(Web APIs),JS引擎中维护了一个调用栈,用于代码的执行。
- 在处理异步任务的时候,其回调会放入任务队列中等待执行,任务队列分宏任务队列和微任务队列,当当前宏任务入栈执行完毕后会优先执行完当前微任务对垒的所有的微任务,最后再让下一个宏任务入栈,以此往复。
- 只有栈被清空了之后,任务队列中的任务才会进入栈中被执行。
- 我们可以认为任务队列中存放的都是回调,网络请求的、setTimeout的、JS事件的。
- 如果同步代码中存在一段计算复杂的耗时任务,就会阻塞异步代码中回调函数的执行。
- 关于Web APIs何时将回调放入任务队列中:在合适的时候。如定时器在到了执行时间的时候、如click事件在目标元素被点击的时候、如promise.then在Promise的状态改变的时候。
以上分析了浏览器中的Eventloop,而在node环境中Eventloop也有自己的模型,与浏览器不完全相似,有机会再做探究。
参考链接
- 详解JavaScript中的Event Loop(事件循环)机制
- 这一次,彻底弄懂 JavaScript 执行机制
- 一次弄懂Event Loop(彻底解决此类面试问题)
- 深入理解 JavaScript Event Loop
- 从event loop规范探究javaScript异步及浏览器更新渲染时机 #5
- 大神教你理解JavaScript 事件循环 event loop -- 视频
还有一篇英文文档待阅读: Tasks, microtasks, queues and schedules