众所周知JavaScript是基于单线程运行的,同时又是能够异步执行的,通常来讲这种既是单线程又是异步的语言都是基于事件来驱动的,刚好浏览器就给JavaScript提供了这么一个环境javascript
1 setTimeout(function(argument) { 2 console.log('---1---') 3 }, 0) 4 5 console.time("test") 6 for (var i = 0; i < 1000000; i++) { 7 i === (100000 - 1) 8 } 9 console.timeEnd("test") 10 11 console.log('---2---')
在我电脑上输出的是:html
1 test: 5.4892578125ms 2 3 ---2--- 4 5 ---1---
咦,它不讲道理啊,明明我设置的是0毫秒以后打印‘---1---’的
有状况,打开前端圣经瞧瞧,里头有句话:
This is because even though setTimeout was called with a delay of zero, it's placed on a queue and scheduled to run at the next opportunity; not immediately. Currently-executing code must complete before functions on the queue are executed, thus the resulting execution order may not be as expected.
原来setTimeout的等待时间结束后并非直接执行的而是先推入浏览器的一个任务队列,在同步队列结束后在依次调用任务队列中的任务。前端
这就牵涉到浏览器的几个线程了,通常来讲浏览器会有如下几个线程java
js引擎线程 (解释执行js代码、用户输入、网络请求)git
GUI线程 (绘制用户界面、与js主线程是互斥的)web
http网络请求线程 (处理用户的get、post等请求,等返回结果后将回调函数推入任务队列)api
定时触发器线程 (setTimeout、setInterval等待时间结束后把执行函数推入任务队列中)promise
浏览器事件处理线程 (将click、mouse等交互事件发生后将这些事件放入事件队列中)浏览器
1 function test1() { 2 test2() 3 console.log('你们好,我是test1') 4 } 5 6 function test2() { 7 console.log('你们好,我是test2') 8 } 9 10 function main() { 11 console.log('你们好,我是main') 12 setTimeout(() => { 13 console.log('你们好,我是setTimeout') 14 }, 0) 15 test1() 16 } 17 18 main()
执行结果以下:网络
你们好,我是main
你们好,我是test2
你们好,我是test1
你们好,我是setTimeout
当咱们调用一个函数,它的地址、参数、局部变量都会压入到一个 stack 中
step1:main()函数首先被调用,进入执行栈打印‘你们好,我是main’
step2:遇到setTimeout,将回调函数放入任务队列中
step3:main调用test1,test1函数进入stack中将被执行
step4:test1执行,test1调用test2
step5:test2执行,打印‘你们好,我是test2’
step6:test2执行完毕从stack弹出回到test1,打印‘你们好,我是test1’
step6:主线程执行完毕,进入callback queue队列执行setTimeout的回调函数打印‘你们好,我是setTimeout’ 至此整个程序执行完毕,不过event loop一直在等待着其余的回调函数。
用代码表示的话大概是这样
1 while (queue.waitForMessage()) { 2 queue.processNextMessage() 3 }
这个线程会一直在等待其余回调函数过来例如click、setTimeout等
用一张图来表示以下所示:
接下来看段代码想一想这个的运行结果是怎样的
1 setTimeout1 = setTimeout(function() { 2 console.log('---1---') 3 }, 0) 4 5 setTimeout2 = setTimeout(function() { 6 Promise.resolve() 7 .then(() => { 8 console.log('---2---') 9 }) 10 console.log('---3---') 11 }, 0) 12 13 new Promise(function(resolve) { 14 15 console.time("Promise") 16 for (var i = 0; i < 1000000; i++) { 17 i === (1000000 - 1) && resolve() 18 } 19 console.timeEnd("Promise") 20 21 }).then(function() { 22 console.log('---4---') 23 }); 24 25 console.log('---5---')
在上面咱们有分析到浏览器会将异步的回调函数放进一个任务队列中去,按照此思路分析当程序运行的时候首先会遇到setTimeout函数,并将setTimeout的回调函数放入任务队列中去,继续往下执行又会遇到个setTimeout函数,不过这个setTimeout的回调函数有些例外,回调函数中又有个Promise对象,不过这个咱暂且无论等到任务队列里头轮到它执行再说,接下来浏览器的解释器会遇到个正在new Promise对象的操做,这个总算不是异步执行的了,首先会开始一个程序运行计时器,随后会输出从0自增到1000000所用的时间,在i = 999999时,Promise的状态会变成resolve随后将resolve所要执行的回调函数推入任务队列,最后执行到程序末尾,此时输出的是‘---5---`。
按照上面的分析咱们的程序输出顺序是:
程序从0自增到1000000所须要的时间
---5---
---1---
---3---
---4---
---2---
用一张图来表示以下所示:
接下来给扔浏览器里头执行一遍看下结果,结果以下:
Promise: 5.151123046875ms
---5---
---4---
---1---
---3---
---2---
由于浏览器的任务队列不止一个,还有 microtasks 和 macrotasks
microtasks:
macrotasks:
据whatwg规范介绍:
so,正确的执行步骤应该为:
step1:执行脚本,把这段脚本压入task queue,此时
1 stacks: [] 2 task queue: [script] 3 microtask queue: []
step2:遇到setTimeout1,setTimeout可看成一个task,因此此时
1 stacks: [script] 2 task queue: [setTimeout1] 3 microtask queue: []
step3:继续往下执行,遇到setTimeout2,将setTimeout2压入task queue中
1 stacks: [script] 2 task queue: [setTimeout1,setTimeout2] 3 microtask queue: []
step4:继续往下执行,new Promise,按同步流程往下走,输出new Promise内的执行时间
1 stacks: [script] 2 task queue: [setTimeout1,setTimeout2] 3 microtask queue: []
step5:在i = 99999的时候,resolve()发生,把回调成功的代码段压入microtask queue,此时
1 stacks: [script] 2 task queue: [setTimeout1,setTimeout2] 3 microtask queue: [console.log('---4---')]
step6:继续往下执行就是console.log('---5---'),此时stacks队列里头的任务结束
1 stacks: [] 2 task queue: [setTimeout1,setTimeout2] 3 microtask queue: [console.log('---4---')]
step7:stacks队列为空,此时事件轮询线程会将microtask queue队列里头的任务推入stacks中去,而后执行输出’---4---‘
1 stacks: [] 2 task queue: [setTimeout1,setTimeout2] 3 microtask queue: []
step8:stacks队列又为空了且microtask queue队列也是空着的,这时就会开始执行task queue里头的任务了,首先是setTimeout1,输出’---1---‘
1 stacks: [setTimeout1,setTimeout2] 2 task queue: [] 3 microtask queue: []
step8:接着setTimeout2在里头遇到个Promise.resolve(),将回调成功的函数压入microtask queue队列里头,而后输出’---3---‘
1 stacks: [setTimeout2] 2 task queue: [] 3 microtask queue: [Promise.resolve()]
step9:当setTimeout2执行完毕以后执行microtask queue,输出’---2---‘,至此该代码段执行完毕
1 stacks: [] 2 task queue: [] 3 microtask queue: []
用一句话简单的来讲:整个的js代码macrotask先执行,同步代码执行完后有microtask执行microtask,没有microtask执行下一个macrotask,如此往复循环至结束
若是你喜欢咱们的文章,关注咱们的公众号和咱们互动吧。