众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心。可看HTML规范中的这段话:html
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.前端
为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,用户引擎必须使用 event loops。Event Loop 包含两类:一类是基于 Browsing Context ,一种是基于 Worker ,两者是独立运行的。 下面本文用一个例子,着重讲解下基于 Browsing Context 的事件循环机制。html5
来看下面这段 JavaScript 代码:ajax
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0);//前端全栈交流学习圈:866109386 //帮助1-3年前端人员,突破技术,提高思惟 Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
先猜想一下这段代码的输出顺序是什么,再去浏览器控制台输入一下,看看实际输出的顺序和你猜想出的顺序是否一致,若是一致,那就说明,你对 JavaScript 的事件循环机制仍是有必定了解的,继续往下看能够巩固下你的知识;而若是实际输出的顺序和你的猜想不一致,那么本文下面的部分会为你答疑解惑。promise
任务队列浏览器
全部的任务能够分为同步任务和异步任务,同步任务,顾名思义,就是当即执行的任务,同步任务通常会直接进入到主线程中执行;而异步任务,就是异步执行的任务,好比ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会经过任务队列( Event Queue )的机制来进行协调。具体的能够用下面的图来大体说明一下:网络
同步和异步任务分别进入不一样的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是咱们说的 Event Loop (事件循环)。异步
在事件循环中,每进行一次循环操做称为tick,经过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤能够总结以下:函数
主线程重复执行上述步骤oop
能够用一张图来讲明下流程:
这里相信有人会想问,什么是 microtasks ?规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 而且每一个宏任务结束后, 都要清空全部的微任务,这里的 Macro Task也是咱们常说的 task ,有些文章并无对其作区分,后面文章中所说起的task皆看作宏任务( macro task)。
(macro)task 主要包含:script( 总体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)
setTimeout/Promise 等API即是任务源,而进入任务队列的是由他们指定的具体执行任务。来自不一样任务源的任务会进入到不一样的任务队列。其中 setTimeout 与 setInterval 是同源的。
分析示例代码
千言万语,不如就着例子讲来的清楚。下面咱们能够按照规范,一步步执行解析下上面的例子,先贴一下例子代码(省得你往上翻)。
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0);//前端全栈交流学习圈:866109386 //帮助1-3年前端人员,提高技术,突破思惟 Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
总体 script 做为第一个宏任务进入主线程,遇到 console.log,输出 script start
遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中 遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,以后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2 遇到 console.log,输出 script end 至此,Event Queue 中存在三个任务,以下表:
宏任务 | 微任务 |
---|---|
setTimeout | then1 |
- | then2 |
看看你掌握了没
再来一个题目,来作个练习:
console.log('script start'); setTimeout(function() { console.log('timeout1'); }, 10); new Promise(resolve => { console.log('promise1'); resolve(); setTimeout(() => console.log('timeout2'), 10); }).then(function() { console.log('then1') }) console.log('script end');
这个题目就稍微有点复杂了,咱们再分析下:
首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(总体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。因此,就和上面例子相似,首先遇到了console.log,输出 script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到 promise,new promise 中的代码当即执行,输出 promise1, 而后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1; 接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2 至此,全部的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2
用流程图看更清晰:
总结
有个小 tip:从规范来看,microtask 优先于 task 执行,因此若是有须要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。
最后的最后,记住,JavaScript 是一门单线程语言,异步操做都是放到事件循环队列里面,等待主执行栈来执行的,并无专门的异步执行线程。。