写在前面:本文仅是个人对于学习js执行机制的一些理解在此记录一下,有任何问题或错误请多多指点,感谢!!!
一、什么是 event loop:
Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程执行异步任务时不被阻塞的一种机制;
js主线程从“任务队列”中循环不断的读取执行事件,这个机制被称为事件循环。
二、js是单线程是指js的宿主环境(浏览器或node)中一个进程中(可理解为浏览器的一个tab页)提供的js引擎线程是唯一的,但它的宿主环境提供了其它线程来辅助js引擎线程来完成异步任务的执行,了解js运行机制前可能需先了解浏览器主要有哪些线程:
1、JS引擎线程(唯一主线程):也叫js内核,负责解析Javascript脚本,运行代码。例如V8引擎。与GUI渲染线程互斥,不可同时工作。
2、GUI渲染线程(唯一):负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等,与JS引擎线程互斥,不可同时工作(所以当JS加载事件过长时,会导致阻塞页面加载。这就是为什么建议将<script>标签写在body的最末端)。
3、事件触发线程:当JS引擎执行代码块如鼠标点击、AJAX异步请求等,会将对应任务添加到事件触发线程中。
4、定时触发器线程:js引擎运行setInterval与setTimeout异步任务时,会将该任务丢进该线程执行,将执行完成的回调事件添加到任务队列中,等待JS引擎空闲后执行。
5、异步HTTP请求线程:用于处理请求XMLHttpRequest,在连接后是通过浏览器新开一个线程请求。检测到状态变更时,如果设置有回调函数,该线程就产生状态变更事件,将回调函数放入任务队列中,等待JS引擎空闲后执行。
其中 3、4、5 属于WebAPIs相关线程(是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;)
三、js中的微任务与宏任务:
宏任务:
script(整体代码) setTimeout setInterval I/O UI交互事件 postMessage MessageChannel setImmediate(Node.js 环境)
微任务:
Promise.then Object.observe MutationObserver process.nextTick(Node.js 环境)
四、任务队列(task queue 也叫事件队列或异步回调队列,存放WebAPIs相关线程中执行完成后的回调事件,等待主线程空闲时执行,先进先出的原则):
1、宏任务队列 MacroTask:从前到后依次存放宏任务执行完成后的回调事件,GUI页面渲染完成后执行最前面的一个宏任务。
2、微任务队列 MicroTask:从前到后依次存放微任务执行完成后的回调事件,每个宏任务中的同步代码执行完成后,会执行这个宏任务中的所有微任务(微任务一定是在宏任务内部,因为整个script标签代码块也属于一个宏任务),所有微任务执行完成之后会通知GUI渲染页面。
五、浏览器解析一个html页面的过程:
1、GUI渲染线程先解析HTML,CSS渲染页面,页面渲染完成后GUI渲染线程挂起,js引擎线程接管工作
2、js引擎线程将每个 <script></script> 中的代码视为一个宏任务,从上到下依次将其添加到宏任务队列中(一个页面可能存在多个script标签),等待主线程执行
3、js引擎线程开始执行宏任务队列中最前面的一个任务,任务内部的同步代码会被按顺序添加到执行栈中(后进先出的原则),任务内部的异步代码浏览器会单独开启一个线程来处理(根据任务类型开启的线程不同:事件触发线程、定时触发器线程、异步HTTP请求线程),在异步任务执行有了结果后会
将执行完成后的回调根据任务类型添加到任务队列下面的宏任务队列或微任务队列的最后面,等待主线程空闲时执行。
4、当执行栈中所有同步任务从上到下执行完成后表示主线程空闲,主线程就会去微任务队列的最前面读取一个回调事件放入执行栈中执行
5、主线程从前到后执行完所有微任务后,会判断页面是否需要渲染,如果需要,此时js引擎线程会被挂起,GUI渲染线程接管工作,对页面进行渲染,渲染的结果不会立刻呈现在屏幕上,等屏幕刷新时才会呈现出来。屏幕刷新频率一般60HZ,即16.6ms刷新一次屏幕。渲染完成后GUI渲染线程被挂起,js引擎线程继续接管工作。
6、不断循环3、4、5 步,直到任务队列被清空。
六、为什么会存在微任务与宏任务?
微任务队列来处理优先级较高的任务。页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。
七、练习题(说出下面的输出结果)
第一题:
async function async1() { console.log(1); const result = await async2(); console.log(3); } async function async2() { console.log(2); } Promise.resolve().then(() => { console.log(4); }); setTimeout(() => { console.log(5); },1000); async1(); console.log(6);
1、整个代码块为一个宏任务添加到宏任务队列后从上到下开始执行
2、执行到 Promise4 时首先将 Promise4 添加到微任务队列中继续向下执行 ,此时微任务队列为 [ ()=>{ console.log(4) } ]; (这里我把宏任务队列和微任务队列想象成一个数组,按顺序存放着回调函数)
3、执行到 setTimeout5 时 ,等待1秒后将 setTimeout5的回调 添加到宏任务队列中继续向下执行,此时宏任务队列为 [ ()=>{ console.log(5) } ]
4、开始调用async1,执行async1,输出1,然后异步调用async2, async2会被立即执行输出2,await下面的代码等价于async2().then( console.log(3) )所以它是一个微任务添加到微任务队列,此时微任务队列为 [ ()=>{ console.log(4) },()=>{ console.log(3) } ]
5、async1执行完毕,继续向下输出6
6、到这里第一个宏任务就执行完成了,开始从前到后执行所有微任务,输出 4、3
7、所有微任务执行完后,执行下一个宏任务,输出5
所以这个段代码的输出顺序为:1 、2、6、 4、3、5
第二题:
setTimeout(() => {
console.log('1');
}, 2000);
setTimeout(() => { console.log('2'); Promise.resolve().then(() => { console.log('3'); }) }, 0); new Promise(function(resolve, reject) { console.log('4'); setTimeout(function() { console.log('5'); resolve('6') }, 1000) }).then((res) => { console.log('7'); setTimeout(() => { console.log(res); }, 0) })
这题稍微复杂一点,我们来慢慢缕一缕:
首先整个代码块是一个宏任务,开始从上到下执行,我们先看所有同步代码只有new Promise中的 console.log('4'),所以直接输出4
接着从上到下看所有异步代码,有三个setTimeout异步任务,分别是
setTimeout(() => { console.log('1'); }, 2000); setTimeout(() => { console.log('2'); Promise.resolve().then(() => { console.log('3'); }) }, 0); setTimeout(function() { console.log('5'); resolve('6') }, 1000)
注意代码中 new Promise 后面的.then()方法并不会立即添加到微任务中,因为resolve('6')是在一个宏任务setTimeout当中,我们这里把resolve('6')直接替换成Promise.then()后面执行的代码
setTimeout(() => { console.log('1'); }, 2000); setTimeout(() => { console.log('2'); Promise.resolve().then(() => { console.log('3'); }) }, 0); setTimeout(function() { console.log('5'); console.log('7'); // 这里将resolve('6') 直接替换成.then()中的代码 setTimeout(() => { console.log(6); }, 0) }, 1000)
接着按setTimeout的执行时间,将异步任务执行完成的回调放入任务队列中,
首先运行的是第二个setTimeout,直接输出2,然后将()=>{console.log('3')} 放入微任务队列,当前宏任务执行完毕,执行微任务,输出3
接着运行的是第三个setTimeout,直接输出5、7,然后将()=>{console.log('6')} 添加到宏任务队列中,接着执行,执行下一个宏任务,输出6,
然后再执行第一个setTimeout,输出1
所以最终输出结果为:4、2、3、5、7、6、1
提示:可能最后6 和 1这里的输出顺序有点乱,这样理解,因为 console.log('1') 所在的setTimeout是2秒后将执行完成的回调丢到宏任务队列中,而 console.log(6)
所在的setTimeout是父级1秒再加自身4毫秒(setTimeout的最小值是4毫秒,0为默认的4毫秒),所以是1秒4毫秒后将执行完成的回调丢到宏任务队列中。所以6的输出在1的前面。