51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

关于JS执行顺序

背景 {#背景}

众所周知,JS是单线程语言,但它支持异步操作,其核心机制就是JS引擎的事件循环

先上一段代码:

|------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | console.log(1) setTimeout(() => { console.log(2) }) new Promise(resolve => { console.log(3) resolve() }).then(() => { console.log(4) }) console.log(5) // 1 3 5 4 2 |

背后的原因就是事件循环中的宏任务微任务

原理 {#原理}

总的来说,流程图如下:

流程图

  1. Promise中的代码块是立即执行的。

    下列代码可以证明:

    |---------------------------|--------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | console.log(1) new Promise(() => { console.log(2) }) console.log(3) // 1 2 3 |

    这部分很简单了,从上到下同步执行。

  2. Promise后的then传入的方法是微任务

    下面代码为V8引擎源码,注意它是用V8内部语言Torque编写,我们只需要看它是继承了Microtask即可知它是一个微任务,无需在意更多细节:

    |-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | @generateCppClass extern class PromiseResolveThenableJobTask extends Microtask { context: Context; promise_to_resolve: JSPromise; thenable: JSReceiver; then: JSReceiver; } |

    所以说它会在最外层代码执行完后再去执行。

  3. setTimeout或者setInterval都是宏任务

    我这里实在是没找到源码哪里表明了这个东西,于是直接在NodeJS里换个方式证明一下。

    NodeJS中,process.nextTick可以设置一个微任务,使用下列代码测试:

    |---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | setTimeout(() => { console.log(1) }) process.nextTick(() => { console.log(2) }) process.nextTick(() => { console.log(3) }) console.log(4) // 4 2 3 1 |

    我们把setTimeout放在最开始,而且不管设置了几次nextTicksetTimeout里的函数体总是最后执行的,由此可见它的确是一个宏任务。

更复杂一点 {#更复杂一点}

不管我的Promise怎么组合,怎么套,由于setTimeout设置的是宏任务,所以它始终在这些微任务都执行完成之后才会运行:

|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | setTimeout(() => { console.log(1) }) Promise.resolve().then(() => { console.log(2) return Promise.resolve() }).then(() => { console.log(3) Promise.resolve().then(() => { console.log(4) }) }) console.log(5) // 5 2 3 4 1 |

可以看出,2 3 4全都是微任务,而5是最外层同步执行的代码,1是由setTimeout设置的下一个宏任务。

总结 {#总结}

再回到开头的程序:

|------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | console.log(1) setTimeout(() => { console.log(2) }) new Promise(resolve => { console.log(3) resolve() }).then(() => { console.log(4) }) console.log(5) // 1 3 5 4 2 |

因为1,3,5都是同步执行的,所以它们按顺序排列; 2是宏任务,会放到下一次事件循环时执行; 4是微任务,在首次运行时就把它添加到了微任务队列中,所以在下一次事件循环之前就会被执行。

通过这样的事件循环,使得单线程的JS也可以拥有异步的能力,使得如AJAX请求这样费时间的操作可以被安排到后面来执行,不影响页面的加载和渲染。

赞(3)
未经允许不得转载:工具盒子 » 关于JS执行顺序