前言:
在快节奏的开发任务中,我们越来越依赖各种基于JS的框架,这确实能快速提高我们的开发速度,但是我们不可能只停留在对框架、语言的基础应用上;在写完越来越多的bug之后,我们深刻的体会到了解底层对一个程序员来说是多么必要的一件事;在了解和掌握各种底层原理之后,我们就可以站在更高的维度更加快速、精准、合理的解决工作学习中遇到的问题;那么我们就开始从JS的基础开始,了解JS的运行机制;接下来我们将一起整理探讨JS的事件循环机制(EnentLoop)
我们都知道JS在设计之初就是单线程语言,在遇到需要耗时的操作时我们会将需要耗时的异步操作放到一个异步队列,然后等待JS空闲再去循环执行事件队列。这种事件队列(EventQueue)+事件循环机制(EventLoop)方式,就是JS用来实现单线程异步编程的事件驱动机制(event-driven)
在理解JS的事件循环机制之前需要了解一些概念:
众所周知我们的JS是单线程运行的,但是如果有一个段代码需要很长时间才会有结果,总不能让下面代码一直等着吧,于是任务队列就出现了。我们可以把需要耗时的任务扔进任务队列,等主线程的代码运行完毕之后,再去清理任务队列的任务;(注意:上面的宏任务、微任务涉及到的setTime,Prompise等只是事件源,真正放进任务队列的是他们的回调函数)
事件循环机制流程:
如此便是JS的事件循环了;下面我们解和具体实例来深入理解一下它的循环过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
setTimeout(() => { console.log('宏任务1time1'); new Promise(function (resolve, reject) { console.log('宏任务1promise'); setTimeout(() => { console.log('宏任务2time2'); }, 0); resolve(); }).then(function () { console.log('宏任务1time1微任务then') }) }, 0); console.log('script主线程宏任务1'); setTimeout(() => { console.log('宏任务time2'); }, 0); new Promise(function (resolve, reject) { console.log('宏任务1promise'); // reject(); resolve(); }).then(function () { console.log('微任务1then') }).catch(function () { console.log('微任务1catch') }) console.log('script主线程宏任务2'); |
1:JS由上到下依次加载代码,首先发现 '5:宏任务1time1' 的 setTimeout, 将他放入异步队列的宏任务队列中;接着打印' 1:script主线程宏任务1 '
2:然后又发现 '8:宏任务time2' 的 setTimeout ,将他放入异步队列的宏任务队列中 ;然后执行 new Promise 发现有同步代码 '2:宏任务1promise' ,就打印 '2:宏任务1promise' 。然后发现promise的then和catch,将他们放入异步队列的微任务中
3:接着发现 '3:script主线程宏任务2' ,打印 '3:script主线程宏任务2' 。如此JS的主线程的代码全部运行完毕;
4:接着JS开始取异步队列的微任务,发现有一个 '2:宏任务1promise' 的then的回调,然后推进主栈并运行,接着打印 '4:微任务1then' 。然后主栈又运行完毕,再去微任务队列发现微任务队列也被清空;
5:接着js开始去宏任务队列里找到 '5:宏任务1time1' 的 setTimeout的回调,将他推入主栈并运行,发现 '5:宏任务1time1' ,打印 '5:宏任务1time1' ;
6:接着运行 '6:宏任务1promise' 的promise,然后打印 '6:宏任务1promise' ;再接着运行发现 '9:宏任务2time2' 的 setTimeout ,将它的回调推入宏任务队列;
7:接着发现 '6:宏任务1promise' 的promise的then,然后将它的回调推入微任务队列;至此第一个宏任务运行完毕,主栈代码运行完毕;接着JS再去微任务队列发现 '7:宏任务1time1微任务then' 的回调,并将此推入到主线程并运行,然后打印 '7:宏任务1time1微任务then' 。主栈又被清空,再去微任务队列,发现微任务队列也被清空
8:然后就进入到宏任务队列中发现 '8:宏任务time2'的 setTimeout ,将它的回调推入主栈,主栈开始运行并打印 '8:宏任务time2';然后主栈执行完,第二个宏任务结束;再去查看微任务列表发现是空的,再去宏任务队列
9:发现宏任务队列 '9:宏任务2time2' 的回调,将此推入主栈,然后运行主栈打印 '9:宏任务2time2' ,然后主栈代码运行完毕,再去微任务队列发现也被清空后,再去宏任务发现也被清空,至此整个js运行完毕
接下来再看一个例子:
1 2 3 4 5 6 7 8 9 10 11 |
for (var i = 0; i > 10; i--) { setTimeout(() => { console.log(i) //打印的都是10 }) } for (let i = 0; i > 10; i--) { setTimeout(() => { console.log(i) //依次打印0~9 }) } |
我们来具体分析一下这两个for循环为什么能打印处不一样的东西: