学习
实践
活动
工具
TVP
写文章
专栏首页踏浪的文章JavaScript中的单线程运行,宏任务与微任务,EventLoop

JavaScript中的单线程运行,宏任务与微任务,EventLoop

在前端的面试中经常会问到关于代码执行顺序的问题,尤其是下面的一段代码

setTimeout( () => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then( () => {
  console.log(3)
})

Promise.resolve(5).then(() => console.log(5))

console.log(2)

问题是:在浏览器上面 1 2 3 4 5 的打印的顺序。

上面这个问题看起来对有的同学可能很简单,到有的同学可能会比较复杂。对你不管是复杂还是简单,这其中涉及到的只是点都是一样的。JavaScript单线程,宏任务与微任务,EventLoop。这些就是这个题目的考点,理解了这些,那么上面的这道题对你来说那就是信手拈来,游刃有余。

我猜你应该知道,JavaScript除了在浏览器环境中运行,还可以在Node环境中运行,虽说都是JavaScript代码,但是在这两种环境下面执行的结果是可能不一样的。所以,我们需要分两种情况来分析他们的EventLoop。

什么是EventLoop

EventLoop是一个执行模型,在不同的有不同的实现,浏览器和NodeJS基于不同的技术实现了各自的EventLoop。

  • 浏览器的EventLoop是在HTML5规范中明确定义了的
  • NodeJS的EventLoop是基于libuv实现的。可以在libuv官网NodeJS官网查看
  • libuv已经对NodeJS的EventLoop做出了实现,但是浏览器的HTML5规范只是定义了EventLoop的实现模型,具体的实现留给了浏览器厂商。

JavaScript中的单线程

JavaScript是单线程脚本语言。所以,在一行代码的执行过程过,必然不会执行另一行代码的,就行你在使用了alert(1)以后在后面疯狂的console.log(),如果执行到 alert(1),你没有关闭这个弹窗,后面的console.log()是永远都不会执行的,因为 alert() 这个任务还没有执行完成,下面的代码没法执行。通俗一点就是:如果你去食堂打饭,前面排了很长的队,如果你想要打到饭,那么你需要等前面的小可爱都能够顺利的打完饭才可以,你是不能够插队的。那什么是宏任务,什么又是微任务呢?

同样是打饭的例子,你要打饭这件事请就是宏任务。这是一个大的事件。当轮到你打饭的时候,事件执行到你这里了,这个时候阿姨开始给你打饭,后面的同学还在等待着。但是你去打饭不单单的就是打饭,你会询问每种菜是什么,价格是多少,有没有XXX菜,有没有汤一样,那这些询问可以比作是微任务。当你的宏任务与微任务都执行完成了,相当于你的这一轮时间执行完成,这个时候开始执行下一轮事件,也就是下一个同学开始打饭了。同样的,下面的一轮循环中也可能存在微任务。

通过上面的例子,如果能有大概的明白了什么是宏任务,什么是微任务了。

宏任务

macrotask,也叫 tasks,主要的工作如下

  • 创建主文档对象,解析HTML,执行主线或者全局的javascript的代码,更改url以及各种事件。
  • 页面加载,输入,网络事件,定时器。从浏览器角度看,宏任务是一个个离散的,独立的工作单元。
  • 运行完成后,浏览器可以继续其他调度,重新渲染页面的UI或者去执行垃圾回收

一些异步任务的回调会以此进入 macrotask queue(宏任务队列),等等后续被调用,这些异步函数包括:

  • setTimeout
  • setInterval
  • setImmediate (Node)
  • requestAnimationFrame (浏览器)
  • I/O
  • UI rendering (浏览器)

微任务

microtask,也叫 jobs,注意的工作如下

  • 微任务是更小的任务,微任务更新应用程序的状态,但是必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。
  • 微任务包括Promise的回调函数,DOM发生变化等,微任务需要尽可能快地,通过异步方式执行,同时不能产生全新的微任务。
  • 微任务能使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使得应用状态不连续

另一些异步回调会进入 microtask queue(微任务队列) ,等待后续被调用,这些异步函数包括:

  • process.nextTick (Node)
  • Promise.then()
  • catch
  • finally
  • Object.observe
  • MutationObserver

这里有一点需要注意的:Promise.then()new Promise(() => {}).then() 是不同的,前面的是一个微任务,后面的 new Promise() 这一部分是一个构造函数,这是一个同步任务,后面的 .then() 才是一个微任务,这一点是非常重要的。

浏览器中的EventLoop

关于宏任务与微任务我们看看下面的执行流程

最开始有一个执行栈,当执行到带有异步操作的宏任务的时候,比如 setTimeout 的时候就会将这个异步任务存在背景线程里面,待本次的事件执行完成以后再去执行微任务。即图中 Stack --> Background Thread。但是需要注意到,从 Stack --> Microtask Queue 还有一条路线,意思就是在当前这轮的任务中还有执行微任务的操作。当前轮的微任务优先于宏任务异步操作先执行,执行完成到 loop 中,进入到下一轮。下一轮执行之前的宏任务的异步操作,比如 setTimeout 。此时,如果这个异步任务中还有微任务,那么就会执行完成这个微任务,在执行下一个异步任务。就这样一次的循环。

回到最开始的那道题上面

setTimeout( () => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then( () => {
  console.log(3)
})

Promise.resolve(5).then(() => console.log(5))

console.log(2)

整个这一串代码我们所在的层级我们看做一个任务,其中我们先执行同步代码。第一行的 setTimeout 是异步代码,跳过,来到了 new Promise(...) 这一段代码。前面提到过,这种方式是一个构造函数,是一个同步代码,所以执行同步代码里面的函数,即 console.log(1),接下来是一个 then 的异步,跳过。在往下,是一个Promise.then() 的异步,跳过。最后一个是一段同步代码 console.log(2)。所以,这一轮中我们知道打印了1, 2两个值。接下来进入下一步,即之前我们跳过的异步的代码。从上午下,第一个是 setTimeout,还有两个是 Promise.then()setTimeout 是宏任务的异步,Promise.then()是微任务的异步,微任务是优先于宏任务执行的,所以,此时会先跳过 setTimeout 任务,执行两个 Promise.then() 的微任务。所以此时会执行 console.log(3)console.log(5) 两个函数。最后就只剩下 setTimeout 函数没有执行,所以最后执行 console.log(4)

综上:最后的执行结果是 1, 2, 3, 5, 4

这只是我们的推测的结果,我们来看看在浏览器中的实际的打印结果是什么?

从图中可以看到,实际的运行结果与我们推测的结果是一一致的。所以,我们上面的分析步骤是正确的。

但是有一个问题,什么呢?可以看到,在浏览器中,会有一个 undefined 的返回值。为什么呢?这是因为浏览器将上面的一整段代码当成一个函数,而这个函数执行完成以后返回了 undefined。那么?这就完了吗?没有。我们看看浏览器返回的截图中,3,5 两个数字其实是在 undefined 前面。3,5两个数是两个 Promise.then() 中的 console.log() 的打印值,而 undefined 在这里可以作为一轮任务的结束。这表明的意思就是,微任务会在下一轮任务开始前执行

这一切都是针对于浏览器的EventLoop。在NodeJS的环境中,可能就会有不同的结果。至于结果如何,我们暂时先不讨论,在来看一段代码。

setTimeout( () => {
  new Promise(resolve => {
    resolve()
    console.log(4)
  }).then(() => {
    console.log(7)
  })
})

new Promise(resolve => {
  resolve()
  console.log(1)
}).then( () => {
  console.log(3)
})

setTimeout( () => {
  Promise.resolve(6).then(() => console.log(6))
  new Promise(resolve => {
    resolve()
    console.log(8)
  }).then(() => {
    console.log(9)
  })
})

Promise.resolve(5).then(() => console.log(5))

console.log(2)

在浏览器中执行结果:点击查看

var eventloopBtn = document.getElementById("evnetloop-btn"); var eventloopResult = document.getElementById("evnetloop-result"); eventloopBtn.addEventListener("click", () => { eventloopResult.innerHTML = "1,2,3,5,4,7,8,6,9" })

上面就是关于在浏览器中的EventLoop。附上浏览器上面的可视化操作

NodeJS中的EventLoop

虽然NodeJS中的JavaScript运行环境也是V8,也是单线程,但是,还是有一些与浏览器中的表现是不一样的。

上面的图片的上半部分来自NodeJS官网。下面的图片来自互联网。

同样的两段代码,我们在node环境中执行一下,看看结果。

从上面的图中可以看到,实际的运行结果与浏览器中的运行结果并无二致。

在来看看另一段代码

setTimeout( () => {
  new Promise(resolve => {
    resolve()
    console.log(4)
  }).then(() => {
    console.log(7)
  })
})

new Promise(resolve => {
  resolve()
  console.log(1)
}).then( () => {
  console.log(3)
})

setTimeout( () => {
  Promise.resolve(6).then(() => console.log(6))
  new Promise(resolve => {
    resolve()
    console.log(8)
  }).then(() => {
    console.log(9)
  })
})

Promise.resolve(5).then(() => console.log(5))

console.log(2)

他的执行结果是:1,2,3,5,4,8,7,6,9。 与浏览器的1,2,3,5,4,7,8,6,9不同。

对比浏览器与NodeJS的不同

在大部分情况下,浏览器与NodeJS的运行没有区别,唯一有区别的是在第二轮事件执行的时候,如果有多个宏任务(setTimeout),浏览器会依次的执行宏任务,上一个宏任务执行完成了在执行下一个宏任务。在NodeJS中,则是相当于并行执行,相当于把所有的宏任务组合到一个宏任务中,再在这个组合后宏任务中,依次执行同步代码 --> 微任务 --> 宏任务

NodeJS中的process.nextTick

关于 process.nextTick,就只需要记住一点,那就是 process.nextTick 优先于其他的微任务执行

所以,下面的代码中:

console.log('1');

setTimeout(function() {
  console.log('2');
  process.nextTick(function() {
    console.log('3');
  })
  new Promise(function(resolve) {
    console.log('4');
    resolve();
  }).then(function() {
    console.log('5')
  })
})
process.nextTick(function() {
  console.log('6');
})
new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
})

setTimeout(function() {
  console.log('9');
  process.nextTick(function() {
    console.log('10');
  })
  new Promise(function(resolve) {
    console.log('11');
    resolve();
  }).then(function() {
    console.log('12')
  })
})

分析(以Node作为运行环境,因为process在node中才存在):

第一轮事件循环流程:

  • 整体的script代码作为第一个宏任务进入主线程,执行同步代码,遇到console.log(1),输出 1
  • 遇到setTimeout,其回调函数被分发到宏任务的 Event Queue 中,等待执行。这里标记为setTimeout1
  • 遇到process.nextTick,其回调函数被分发到微任务的 Event Queue 中,等待执行。
  • 遇到new Promise,这是一个构造函数,new Promise构造函数直接执行,遇到console.log(7),输出 7。接着Promise.then()函数被分发到微任务的 Event Queue 中,等待执行。 遇到setTimeout,其回调函数被分发到宏任务的 Event Queue 中,等待执行。这里标记为setTimeout2

将上面的统计一下

宏任务Event Queue

微任务Event Queue

setTimeout1

process.nextTick

setTimeout2

Promise.then()

第一轮事件循环同步代码执行完成,接下来执行微任务

微任务有两个,一个是 process.nextTick ,里一个是 Promise.then()

前面说了,process.nextTick优先于其他的微任务执行,所以

  • 执行process.nextTick:输出 6
  • 执行Promise.then():输出 8

到此,第一轮事件循环结束,最终第一轮事件的输出为 1,7,6,8。开始执行第二轮事件循环(setTimeout)。

第二轮事件循环分析

  • setTimeout1setTimeout2 中先找同步代码
  • setTimeout1 中遇到 console.log(2),输出2
  • setTimeout1 中遇到 process.nextTick,放在第二轮的微任务的Event Queue中,等待执行。这里标记为process_1
  • setTimeout1 中遇到 new Promise ,执行同步代码,输出 4, Promise.then() 放到微任务的Event Queue中,等待执行。这里标记为Promise_1
  • setTimeout2 中遇到 console.log(9),输出9
  • setTimeout2 中遇到 process.nextTick,放在第二轮的微任务的Event Queue中,等待执行。这里标记为process_2
  • setTimeout2 中遇到 new Promise ,执行同步代码,输出 11, Promise.then() 放到微任务的Event Queue中,等待执行。这里标记为Promise_2

第二轮的统计

第二轮宏任务Event Queue

第二轮微任务Event Queue

process_1

Promise_1

process_2

Promise_2

第二轮没有事件循环中没有宏任务,有四个微任务。

四个微任务中,有两个 process

  • 依次执行 process_1process_2。输出:3, 10
  • 一次执行 Promise_1Promise_2。输出:5, 12

所以第二轮输出:2,4,9,11,3,10,5,12

最终的输出为:1,7,6,8,2,4,9,11,3,10,5,12

如果是在浏览器中,排除掉process的输出,结果为:1,7,8,2,4,5,9,11,12

NodeJS中 setImmediate 与 setTimeout 的区别

在官方文档中的定义,setImmediate 为一次Event Loop执行完毕后调用。setTimeout 则是通过计算一个延迟时间后进行执行。

但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。因为如果主进程中先注册了两个任务,然后执行的代码耗时超过XXs,而这时定时器已经处于可执行回调的状态了。所以会先执行定时器,而执行完定时器以后才是结束了一次Event Loop,这时才会执行setImmediate。

setTimeout(() => console.log('setTimeout'))
setImmediate(() => console.log('setImmediate'))

node环境下执行上面的代码,可以看到如下结果

这两个console的结果是随机的。

我们可以通过一些处理,使得我们可以先执行 setTimeout 或者是 setImmediate

但是如果后续添加一些代码以后,就可以保证setTimeout一定会在setImmediate之前触发了:

setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))

let countdown = 1e9

while(countdonn--) { } // 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行`setTimeout`再结束这一轮循环,也就是说开始执行`setImmediate`

如果在另一个宏任务中,必然是setImmediate先执行:

require('fs').readFile(__dirname, _ => {
  setTimeout(_ => console.log('timeout'))
  setImmediate(_ => console.log('immediate'))
})

// 如果使用一个设置了延迟的setTimeout也可以实现相同的效果

上面的为什么有这样的解决方法,从上面的定义中就可以看出来。

关于 async/await 函数

因为,async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似

setTimeout(() => console.log(4))

async function main() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

main()

console.log(2)

输出的结果是:1,2,3,4

可以理解为,await 以前的代码,相当于与 new Promise 的同构代码,以后的代码相当于 Promise.then

总结

之前了解过JavaScript单线程,也了解过JavaScript代码的执行顺序,但是宏任务与微任务也是最近才听说的,这对于一个从事两年前端的开发者真的是,我自己的过失。或需又是因为我是转行的,没有过相关的基础,没有接触到这方面的只是。不过现在我很高兴,因为我对JavaScript的执行有了更多的了解,相比于之前的只是,真的是了解了很多。学习永远都不晚,就怕你从来都不想去了解。在了解EventLoop,宏任务与微任务,JavaScript单线程的时候,参考了一些文档

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:http://www.lyt007.cn复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • JavaScript进阶----宏任务与微任务

    接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务

    大数据爱好者
  • js运行机制同步与异步(宏任务与微任务)

    众所周知,javascript的最大特点就是单线程,同一时间追能做同一件事,所以为了防止主线程的阻塞,在代码执行时分为同步任务和异步任务,所有的同步任务在主线程...

    qiangzai
  • 浏览器中的 Event Loop,宏任务与微任务

      当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(...

    TimothyJia
  • JavaScript 事件循环机制 - 微任务和宏任务的关系

    JavaScript 是单线程的,同一时间只能做一件事情。如果碰到某个耗时长的任务(比如一个需要 3s 的网络请求),那么后续的任务都要等待,这种效果是无法接受...

    文渊同学
  • 引擎进阶(上):探究宏任务 & 微任务的运行机制

    首先分析宏任务和微任务的运行机制,并针对日常开发中遇到的各种宏任务&微任务的方法,结合一些例子来看看代码运行的顺序逻辑,把这部分知识点重新归纳和梳理。

    玖柒的小窝
  • JS中的同步异步编程,宏任务与微任务的执行顺序

    首先我们先看看同步与异步的定义,及浏览器的执行机制,方便我们更好地理解同步异步编程。

    TimothyJia
  • JS中的进程、线程、任务队列、事件循环、宏任务、微任务、执行栈等概念理解

    javascript中有很多需要知道的概念,尤其是标题中列出来的这些,今天就来过一下这些概念。

    伯约同学
  • 浏览器和Node.js的EventLoop事件循环机制知多少?

    无论是浏览器端还是服务端Node.js,都在使用EventLoop事件循环机制,都是基于Javascript语言的单线程和非阻塞IO的特点。在EventLoop...

    玖柒的小窝
  • 深入理解JS执行机制

    JavaScript是一门单线程的非阻塞脚本语言,同一时刻只允许一个代码段执行。在单线程的机制下,执行异步任务时,在等待结果返回的这个时间段,后面的代码就无法执...

    神奇的程序员
  • 我不知道的 Event Loop

    所以像setTimeOut定时任务、ajax请求都是需要一定的时间的,所以一般都是用异步方式,不会阻塞后边代码的执行,而是设置了定时时间之后、或发送了请求之后,...

    执行上下文
  • 「面试」- Vue nextTick实现原理

    熟悉 vue 的前端,想必对 vue 里的 nextTick 也很熟悉了,用的时候就知道他是延迟回调,有时候用起来甚至和setTimeout 看起来是同样的效果...

    用户10106350
  • 【前端进阶】深入浅出浏览器事件循环【内附练习题】

    我们看一个很经典的图,这张图基本可以概括了事件循环(该图来自演讲—— 菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014[1])...

    GopalFeng
  • 面试官:对于宏任务和微任务,你知道多少?

    宏任务(macroTask)和微任务(microTask),都是JavaScript中异步中的一些概念,如果你对其还一头雾水,那就跟着我再捋一遍,加深一下印象。

    是乃德也是Ned
  • 浏览器为契机贯穿前端知识点-大纲参考

    ps:动画不连贯,因为浏览器渲染有一个最小时间间隔(这块之前搜藏了) 涉及如下部分:

    love丁酥酥
  • 全方位理解JavaScript的Event Loop

    下面我们一个一个的来了解 Event Loop 相关的知识点,最后再一步一步分析出本段代码最后的输出顺序。

    laixiangran
  • JavaScript事件循环机制解析

    最近面试了很多家公司,这道题几乎是必被问到的一道题。之前总觉得自己了解得差不多,但是当第一次被问到的时候,却不知道该从哪里开始说起,涉及到的知识点很多。于是花时...

    前端迷
  • Js异步机制的实现

    JavaScript是一门单线程语言,所谓单线程,就是指一次只能完成一件任务,如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模...

    WindrunnerMax
  • 高频面试题:JavaScript事件循环机制解析

    最近面试了很多家公司,这道题几乎是必被问到的一道题。之前总觉得自己了解得差不多,但是当第一次被问到的时候,却不知道该从哪里开始说起,涉及到的知识点很多。于是花时...

    木子星兮

扫码关注腾讯云开发者

领取腾讯云代金券