咱们一般说 JavaScript 是单线程的,其实是指在 JS 引擎中负责解释和执行 JS 代码的线程只有一个,通常成为主线程,在这种前提下,为了让用户的操做不存在阻塞感,前端 APP 的运行须要依赖于大量的异步过程,因此固然浏览器中还存在一些其余的线程,好比处理 http 请求的线程、处理 DOM 事件的线程、定时器线程、处理文件读写的 I/O 线程等等。前端
一个异步过程一般是这样的:web
异步过程当中,工做线程在异步操做完成后须要通知主线程,那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。消息队列是一个先进先出的队列,里面存放着各类消息,咱们能够简单的理解为消息就是注册异步任务时添加的回调函数;事件循环是指主线程重复从消息队列中获取消息、执行回调的过程,之因此称为事件循环,就是由于它常常被用相似以下方式来实现:promise
while (queue.waitForMessage()) { queue.processNextMessage(); }
消息队列是一个存储着待执行任务的队列,其中的任务严格按照时间前后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。消息队列每次仅执行一个任务,在该任务执行完毕以后,再执行下一个任务。执行栈则是一个相似于函数调用栈的运行容器,当执行栈为空时 JS 引擎便检查消息队列,若是不为空消息队列便将第一个任务压入执行栈中运行。浏览器
下面咱们来看下述代码来验证咱们的想法:异步
setTimeout(function() { console.log(4) }, 0); new Promise(function(resolve) { console.log(1) for (var i = 0; i < 10000; i++) { i == 9999 && resolve() } console.log(2) }).then(function() { console.log(5) }); console.log(3);
按道理来讲,执行 setTimeout 时由于延迟为0,因此 console.log(4) 直接插入至消息队列;建立 Promise 实例时同步执行其函数体内的代码,先打印 1,再循环10000次后执行 resolve 将 then 中的回调函数 console.log(5) 插入至消息队列,而后打印 2;最后执行 console.log(3) 打印 3;在主线程执行完成后读取消息队列,依次打印4和5。函数
上面的想法固然是比较天真的,实际上浏览器中仅有一个事件循环,而后消息队列是能够有多个的。
macro-queue: script (总体代码), setTimeout, setInterval, setImmediate, I/O, UI Rendering
micro-queue: process.nextTick, Promise, Object.observe, MutationObserveroop
而且 micro-queue 的任务优先级高于 macro-queue 的任务优先级,这两个任务队列执行顺序以下:取1个 macro-task 执行之,而后把全部 micro-task 顺序执行完,再取 macro-task 中的下一个任务,以此类推依次进行。线程
优先级:process.nextTick > promise.then > setTimeout > setImmediate
Tip:process.nextTick 永远大于 promise.then 缘由其实很简单:在 NodeJS 中,_tickCallback 在每一次执行完 TaskQueue 中的一个任务后被调用,而在这个_tickCallback 中实质上干了两件事:code
总结:浏览器环境通常只能有一个事件循环(实际上有两类:browsing contexts 和 web workers),而一个事件循环能够多个任务队列,每一个任务都有一个任务源。相同任务源的任务,只能放到一个任务队列中;不一样任务源的任务,能够放到不一样任务队列中。举个栗子,客户端可能实现一个包含鼠标键盘事件的任务队列,还有其余的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它,这样就能保证流畅的交互性,并且别的任务也能执行到。server
至此,再返回去看以前的代码就不难分析出:代码执行开始把 setTimeout 的回调插入至 macro-queue 中,而打印完1后把 promise.then 的回调函数插入至 micro-queue 中,总体代码执行完后,按照消息队列的优先级,先执行 micro-task 即打印5,最后执行 macro-task 即打印4。