1.JavaScript是单线程语言
既然JavaScript是单线程语言,那么就会存在一个问题,所有的代码都是一句一句的来执行。
一、概念
1.事件循环(Event Loop)
什么是Event Loop?
javascript有一个主线程 main thread [θred], 和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。
1.任务队列(task queue)
每一个语句就是一个任务, task Queue就是承载的任务队列。JavaScript的Event Loop 就是会不断的过来找这个queue,问有没有task可以运行
2.同步任务(SyncTask)、异步任务(AsyncTask)
同步任务说白了就是主线程来执行的时候立即就能执行的代码
异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行
setTimeout(()=>{console.log(2)});console.log(1);
例如: setTimeout就是一个异步任务,主线程去执行的时候遇到setTimeout发现是一个异步任务。就先注册一个任务的回调,然后接着执行下面的语句console.log(1),等上面的异步任务等待时间到了之后,在执行console.log(2)。
1.主线程自上而下执行所有代码
2.同步任务直接进入到主线程被执行,异步任务则进入到Event Table 并注册相应的回调函数
3.异步任务完成后,Event Table 会将这个函数移入Event Queue
4.主线程任务执行完了以后,会从Event Queue中读取任务,进入到主线程去执行
5.循环如上
上述动作不断循环,就是我们所说的事件循环(Event Loop)
let data = [];$.ajax({url:www.javascript.com,data:data,success:() => {console.log('发送成功!');}})console.log('代码执行结束');1.ajax进入Event Table,注册回调函数success2.执行console.log('代码执行结束')3.ajax事件完成,回调函数success进入Event Queue4.主线程从Event Queue读取回调函数success并执行
3.宏任务(MacroTask)、微任务(MicroTask)
JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)。
先说说 MacroTask,所有的同步任务代码都是MacroTask(这么说其实不是很严谨,下面解释),setTimeout、setInterval、I/O、UI Rendering 等都是宏任务。
MicroTask,为什么说上述不严谨我却还是强调所有的同步任务都是 MacroTask 呢,因为我们仅仅需要记住几个 MicroTask 即可,排除法!别的都是 MacroTask。MicroTask 包括:Process.nextTick、Promise.then catch finally(注意我不是说 Promise)、MutationObserver。
一次事件循环回来后,开始去执行Task Queue中的Task;但是这里的Task有有优先级。所以优先执行MicroTask Queue 中的task,执行完后在执行MacroTask Queue中的task
console.log('1');setTimeout(function() {console.log('2');process.nextTick(function() {console.log('3');})new Promise(function(resolve) {console.log('4');resolve();}).then(function() {console.log('5')})})process.nextTick(function() {console.log('6');})new Promise(function(resolve) {console.log('7');resolve();}).then(function() {console.log('8')})setTimeout(function() {console.log('9');process.nextTick(function() {console.log('10');})new Promise(function(resolve) {console.log('11');resolve();}).then(function() {console.log('12')})})
第一轮事件循环流程分析如下:
- 整体script作为第一个宏任务进入主线程,遇到
console.log,输出1。 - 遇到
setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。 - 遇到
process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。 - 遇到
Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。 又遇到了
setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | setTimeout1 | process1 | | setTimeout2 | then1 |上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
- 我们发现了
process1和then1两个微任务。 - 执行
process1,输出6。 - 执行
then1,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:
首先输出2。接下来遇到了
process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | setTimeout2 | process2 | | | then2 |第二轮事件循环宏任务结束,我们发现有
process2和then2两个微任务可以执行。- 输出3。
- 输出5。
- 第二轮事件循环结束,第二轮输出2,4,3,5。
- 第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 直接输出9。
- 将
process.nextTick()分发到微任务Event Queue中。记为process3。 - 直接执行
new Promise,输出11。 将
then分发到微任务Event Queue中,记为then3。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | | process3 | | | then3 |第三轮事件循环宏任务执行结束,执行两个微任务
process3和then3。- 输出10。
- 输出12。
- 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
