资讯专栏INFORMATION COLUMN

浅谈不同环境下的JavaScript执行机制 + 示例详解

wanghui / 2638人阅读

摘要:如果没有其他异步任务要处理比如到期的定时器,会一直停留在这个阶段,等待请求返回结果。执行的执行事件关闭请求的,例如事件循环的每一次循环都需要依次经过上述的阶段。因此,才会早于执行。

概念 同步任务(Synchronous)

在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务

异步任务(Asynchronous)

不进入主线程,而是进入“任务队列”的任务,只有主线执行栈清空,异步任务才进入主线执行栈执行

任务队列(Task Queue)

包含有异步任务的队列,包括“宏任务”与“微任务”

宏任务(Macrotasks / Task)

创建文档对象、解析 HTML、执行主线程代码(script)

执行各种事件:页面加载、输入、点击

setTimoutsetIntervalsetImmediate

I/O,Ajax,UI rendering

微任务(Microtasks / Jobs)

process.nextTick

Promise.then

Object.observe(已废弃)

MutationObserver

事件循环(Event Loop)
浏览器下的事件循环

事件循环是js实现异步的一种方法,也是js的执行机制

JavaScript 主线程会在执行栈清空后,读取任务队列,入栈第一个宏任务,主线程执行完该任务后又会先检查微任务队列并完成里面的所有微任务,包括新创建的微任务,完成一次事件循环。之后再次去读取任务队列,不断循环

注意:

每次循环只会入栈一个宏任务,所以多个宏任务需要多次事件循环才能执行完

每次循环会执行所有的微任务,所以每次循环结束后微任务队列被清空

Node.JS下的事件循环

timers

执行 setTimeoutsetInterval 中到期的 callback

I/O callbacks

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

setTimeoutsetIntervalsetImmediatecallback

用于执行 close 事件(关闭请求)的 callback,例如 socket.on("close", callback)

idle, prepare

libuv 内部调用

poll

最为重要的阶段,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

check

执行 setImmediatecallback

close callbacks

执行 close 事件(关闭请求)的 callback,例如 socket.on("close", callback)

事件循环的每一次循环都需要依次经过上述的阶段。 每个阶段都有自己的 callback 队列,每当进入某个阶段,都会从所属的队列中取出callback来执行,当队列为空或者被执行callback的数量达到系统的最大数量时,进入下一阶段。这六个阶段都执行完毕称为一轮循环

注意:

不同于浏览器的是,在每个阶段完成后,microTask队列就会被执行,而不是MacroTask任务完成后。

每个阶段完成后,微任务队列就会被执行。

如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了 setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理

递归的调用 process.nextTick 会导致 I/O starving,官方推荐使用 setImmediate

Node.JS与浏览器下的差异

浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行

Node.js中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务

setTimeout(()=>{
    console.log("timer1")

    Promise.resolve().then(function() {
        console.log("promise1")
    })
}, 0)

setTimeout(()=>{
    console.log("timer2")

    Promise.resolve().then(function() {
        console.log("promise2")
    })
}, 0)

// 浏览器输出:
// time1
// promise1
// time2
// promise2

// Node输出:
// time1
// time2
// promise1
// promise2
定时器 setTimeout(callback, time)

经过指定时间后,把要执行的任务 callback 加入到任务队列中

因为JS是单线程,任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间可能远远大于指定时间(time ms)

setTimeout(callback, 0)

指定某个任务 callback 在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行

0ms 实际上是不可能的,在浏览器中 setTimeout() / setInterval() 的每调用一次定时器的最小间隔 >=4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的 setInterval 的回调函数阻塞导致的

在 Node.JS 环境为 1ms,但也取决于系统当时的状况

setTimeout(function () {
    console.log("1");
}, 0)

console.log(2)

// 输出 2 1
setInterval(callback, time)

每过指定时间(time ms),会有 callback 进入任务队列。

callback 执行时间超过了指定时间,那么就会导致 callback 连续执行,完全看不出来有时间间隔了

setImmediate(callback)

Node.JS 特有定时器,在事件循环的 check 阶段执行

process.nextTick(callback)

Node.JS 特有定时器,在事件循环各个阶段结束后执行

从技术上讲,它不是事件循环的一部分

同循环下 process.nextTick 会优于 Promise.then

Promise.resolve().then(() => console.log(1));
process.nextTick(() => console.log(2));

// 输出 2 1
注意

连续的 setTimeoutsetImmediate 在再 timer 阶段的执行顺序是不确定的,取决于系统当时的状况

但是把 setTimeoutsetImmediate 放到一个 I/O 回调里面,就一定是 setImmediate 先执行,因为 poll 阶段后面就是 check 阶段

setImmediate(() => {
  console.log("timer1")

  Promise.resolve().then(function () {
    console.log("promise1")
  })
})

setTimeout(() => {
  console.log("timer2")

  Promise.resolve().then(function () {
    console.log("promise2")
  })
}, 0)

// Node输出:
// timer1               timer2
// promise1    或者     promise2
// timer2               timer1
// promise2             promise1
fs.readFile("test.js", () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
})

// 输出 2 1

// 先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 下一次事件循环的 timers 阶段。因此,setImmediate 才会早于setTimeout 执行。
示例
console.log(0)

new Promise(function(resolve) {
    console.log(1);
    resolve();
}).then(function() {
    console.log(2)
})

setTimeout(function() {
    console.log(3);
    new Promise(function(resolve) {
        console.log(4);
        resolve();
    }).then(function() {
        console.log(5)
    })
})

new Promise(function(resolve) {
    console.log(6);
    resolve();
}).then(function() {
    console.log(7)
})

setTimeout(function() {
    console.log(8);
    new Promise(function(resolve) {
        console.log(9);
        resolve();
    }).then(function() {
        console.log(10)
    })
})

console.log(11)
浏览器环境
第一轮事件循环

第一轮事件循环宏任务

开始执行代码,遇到 console.log 输出 0

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 1

遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数作为异步任务进入“宏任务队列”

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 6

遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数作为异步任务进入“宏任务队列”

最后遇到 console.log 输出 11

此时第一轮事件循环宏任务结束,依次输出 0 1 6 11

第一轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 2

遇到 then 执行其回调输出 7

此时第一轮事件循环微任务结束,依次输出 2 7

第一轮事件循环结束,此时“微任务队列”已被清空,“宏任务队列”里有两个 setTimeout 的回调函数,第一个 setTimeout 被送入主线程执行栈

第二轮事件循环

第二轮事件循环宏任务

开始执行第一个 setTimeout 回调函数

遇到 console.log 输出 3

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 4

遇到 then 回调函数作为异步任务进入“微任务队列”

此时第二轮事件循环宏任务结束,依次输出 3 4

第二轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 5

此时第二轮事件循环微任务结束,输出 5

第二轮事件循环结束,此时“微任务队列”已被清空,“宏任务队列”里剩下一个 setTimeout 的回调函数,其被送入主线程执行栈

第三轮事件循环

第三轮事件循环宏任务

开始执行第二个 setTimeout 回调函数

遇到 console.log 输出 8

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 9

遇到 then 回调函数作为异步任务进入“微任务队列”

此时第三轮事件循环宏任务结束,依次输出 8 9

第三轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 10

此时第三轮事件循环微任务结束,输出 10

第三轮事件循环结束,此时“微任务队列”已被清空,“宏任务队列”已被清空

至此整段代码执行完毕,完整输出结果为:0 1 6 11 2 7 3 4 5 8 9 10

Node.JS 环境
Node.JS 环境下任务队列有层级之分,按层级执行任务队列
第一轮事件循环

第一轮事件循环宏任务

开始执行代码,遇到 console.log 输出 0

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 1

遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数注册到 timer 阶段

接着遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 6

遇到 then 回调函数作为异步任务进入“微任务队列”

接着遇到 setTimeout 其回调函数注册到 timer 阶段

最后遇到 console.log 输出 11

此时第一轮事件循环宏任务结束,依次输出 0 1 6 11

第一轮事件循环微任务

执行注册在“微任务队列”里的微任务,遇到 then 执行其回调输出 2

遇到 then 执行其回调输出 7

此时第一轮事件循环微任务结束,依次输出 2 7

第一轮事件循环结束,此时“微任务队列”已被清空,timer 队列里有两个 setTimeout 的回调函数

第二轮事件循环

第二轮事件循环 timer 阶段

执行第一个 setTimeout 回调函数

遇到 console.log 输出 3

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 4

遇到 then 回调函数作为异步任务进入“微任务队列”

执行第二个 setTimeout 回调函数

遇到 console.log 输出 8

遇到 new Promise 其回调函数作为同步任务直接执行

遇到 console.log 输出 9

遇到 then 回调函数作为异步任务进入“微任务队列”

此时第二轮事件循 timer 阶段结束,依次输出 3 4 8 9

第二轮事件循环 timer 阶段微任务

执行注册在“微任务队列”里的微任务

遇到 then 执行其回调输出 5

遇到 then 执行其回调输出 10

此时第二轮事件循环 timer 阶段微任务结束,输出 5 10

第二轮事件循环结束,至此整段代码执行完毕,完整输出结果为:0 1 6 11 2 7 3 4 8 9 5 10

参考文章
阮一峰 - JavaScript 运行机制详解:再谈Event Loop

阮一峰 - Node 定时器详解

Philip Roberts - Help,I’m stuck in an event loop

lynnelv - 深入理解js事件循环机制(Node.js篇)

这一次,彻底弄懂 JavaScript 执行机制

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101469.html

相关文章

  • 浅谈JavaScript代码预解析 + 示例详解

    摘要:知识点声明的变量在预解析的时候只执行声明,不会执行定义,默认值是。声明的函数在预解析的时候会提前声明并且会同时定义。 showImg(https://segmentfault.com/img/bVbnY76?w=2500&h=1250); 知识点 var 声明的变量在预解析的时候只执行声明,不会执行定义,默认值是 undefined。 function 声明的函数在预解析的时候会...

    sunnyxd 评论0 收藏0
  • Java学习路线总结,搬砖工逆袭Java架构师(全网最强)

    摘要:哪吒社区技能树打卡打卡贴函数式接口简介领域优质创作者哪吒公众号作者架构师奋斗者扫描主页左侧二维码,加入群聊,一起学习一起进步欢迎点赞收藏留言前情提要无意间听到领导们的谈话,现在公司的现状是码农太多,但能独立带队的人太少,简而言之,不缺干 ? 哪吒社区Java技能树打卡 【打卡贴 day2...

    Scorpion 评论0 收藏0
  • 2017文章总结

    摘要:欢迎来我的个人站点性能优化其他优化浏览器关键渲染路径开启性能优化之旅高性能滚动及页面渲染优化理论写法对压缩率的影响唯快不破应用的个优化步骤进阶鹅厂大神用直出实现网页瞬开缓存网页性能管理详解写给后端程序员的缓存原理介绍年底补课缓存机制优化动 欢迎来我的个人站点 性能优化 其他 优化浏览器关键渲染路径 - 开启性能优化之旅 高性能滚动 scroll 及页面渲染优化 理论 | HTML写法...

    dailybird 评论0 收藏0
  • 2017文章总结

    摘要:欢迎来我的个人站点性能优化其他优化浏览器关键渲染路径开启性能优化之旅高性能滚动及页面渲染优化理论写法对压缩率的影响唯快不破应用的个优化步骤进阶鹅厂大神用直出实现网页瞬开缓存网页性能管理详解写给后端程序员的缓存原理介绍年底补课缓存机制优化动 欢迎来我的个人站点 性能优化 其他 优化浏览器关键渲染路径 - 开启性能优化之旅 高性能滚动 scroll 及页面渲染优化 理论 | HTML写法...

    hellowoody 评论0 收藏0

发表评论

0条评论

wanghui

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<