分为node中的事件循环和浏览器的事件循环。
1、首先看一看浏览器的事件循环。
https://segmentfault.com/a/1190000015559210#articleHeader1 这篇文章讲的比较详细。
首先有必要了解一下浏览器的内核。
浏览器内核有多种线程在工作。
GUI 渲染线程:
负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
JS 引擎线程:
单线程工作,负责解析运行 JavaScript 脚本。
和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
事件触发线程:
当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
定时器触发线程:
浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
http 请求线程:
http 请求的时候会开启一条请求线程。
请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
其实主要是后面几个事件触发、定时器触发、http请求这几个线程之前不清楚,当他们被触发完了会被浏览器线程加入到任务队列里等待事件循环来执行。
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
JS 调用栈
JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
同步任务、异步任务
JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,
异步操作会由浏览器(OS)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(task queue)中
等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
Event Loop(事件循环是指主线程重复从消息队列中取消息,执行的过程。)
调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
定时器
定时器会开启一条定时器触发线程来触发计时,定时器会在等待了指定的时间后将事件放入到任务队列中等待读取到主线程执行。
定时器指定的延时毫秒数其实并不准确,因为定时器只是在到了指定的时间时将事件放入到任务队列中,必须要等到同步的任务和现有的任务队列中的事件全部执行完成之后,才会去读取定时器的事件到主线程执行,中间可能会存在耗时比较久的任务,那么就不可能保证在指定的时间执行。
宏任务(macro-task)、微任务(micro-task)
除了广义的同步任务和异步任务,JavaScript 单线程中的任务可以细分为宏任务和微任务。
macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver。
console.log(1);
setTimeout(function() {
console.log(2);
})
var promise = new Promise(function(resolve, reject) {
console.log(3);
resolve();
})
promise.then(function() {
console.log(4);
})
console.log(5);
示例中,setTimeout 和 Promise被称为任务源,来自不同的任务源注册的回调函数会被放入到不同的任务队列中。
有了宏任务和微任务的概念后,那 JS 的执行顺序是怎样的?是宏任务先还是微任务先?
第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否寻在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS 的执行顺序就是每次事件循环中的宏任务-微任务。
上面的示例中,第一次事件循环,整段代码作为宏任务进入主线程执行。
遇到了 setTimeout ,就会等到过了指定的时间后将回调函数放入到宏任务的任务队列中。
遇到 Promise,将 then 函数放入到微任务的任务队列中。
整个事件循环完成之后,会去检测微任务的任务队列中是否存在任务,存在就执行。
第一次的循环结果打印为: 1,3,5,4。
接着再到宏任务的任务队列中按顺序取出一个宏任务到栈中让主线程执行,那么在这次循环中的宏任务就是 setTimeout 注册的回调函数,执行完这个回调函数,发现在这次循环中并不存在微任务,就准备进行下一次事件循环。
检测到宏任务队列中已经没有了要执行的任务,那么就结束事件循环。
最终的结果就是 1,3,5,4,2。
2、node中的事件循环
javascript是单线程,而node内部其实是多线程的。
<------microTasks
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
| | <----- microTasks
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
| | <----- microTasks
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘
| | <----- microTasks
| |
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
| | └───────────────┘
| | <----- microTasks
│ ┌──────────┴────────────┐
│ │ check │
│ └──────────┬────────────┘
| | <----- microTasks
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘