JavaScript的任务跟微任务

缘由

前端团队在执行 code review 时候,我们发现早期的代码中有很多滥用了 async await 的代码。虽然在执行中,虽然在同步的代码中乱写 async await 并没有导致明显的bug,但加了await跟不加await的结果是否会有区别?今天我们先看一下以下几行示例代码的执行结果

async function fn() {
  console.log('fn start')
  await fn2()
  console.log('fn end')
}

function fn2() {
  console.log('fn2')
}

console.log('script start')
fn()
console.log('script end')

结果对比

  • 加await输出的结果是 script start -> fn start -> 2 -> script end -> fn end

  • 不加await输出结果是 script start -> fn start -> 2 -> fn end -> script end

在文章开始之前,本文我所说的任务,即为宏任务。只是我个人更倾向于mdn对其的称呼,是“任务”而不是“宏任务”,具体后面会解释

题目1

  1. 想一想,如果我们执行 foo ,是否会报栈溢出的错误?为什么?
function foo() { 
  setTimeout(foo, 0)
}

题目1 答案

不会

事件循环 Eventloop

JavaScript 并发模型基于“事件循环”,浏览器提供了运行时环境来执行我们的 JavaScript 代码。主要组成包括调用栈、事件循环、任务队列和 Web API 等。JavaScript 环境的可视化表示大致如下所示。(Promise应该是js的内置方法而不是WebAPIs)

image.png

JavaScript调用栈是后进先出的,引擎每次从调用栈中取出一个函数,然后按顺序从上到下运行代码。每当遇到一些异步代码的时候,比如setTimeout,他会将其丢给WebAPI,也就是箭头指向的1。

WebAPI会等到合适的时机,再把回调函数,发送到任务队列,也就是箭头2的操作。

Eventloop会不断监视调用栈是否为空,等到调用栈的任务为空的时候,他就会取出回调函数放入调用栈中。(在调用栈不为空的情况下,eventloop不会把任何回调放到调用栈中去,因此也解释了setTimeout的时间不一定是准确的)

从事件循环机制来看这个方法的执行顺序如下:

  1. 调用 foo()将把 foo 函数放到调用栈中
  1. 在处理内部代码时, JS 引擎遇到 setTimeout
  1. 将回调函数foo交给 webAPI,函数执行结束,出栈;调用栈又是空的
  1. 由于定时器设置为 0,时间一到回调函数 foo 将被发送到任务队列
  1. 进程再次重复,调用栈永远不会溢出

题目2

想一想,如果我们执行下方的foo,再点击页面的按钮,会有响应吗?为什么?

function foo() {
  return Promise.resolve().then(foo)
}

题目2答案

不会有响应

虽然他们都是异步,但跟setTimeout不一样,Promise.then是微任务。他们很相像,但是执行时机上是不一样的。

每当一个任务退出且执行上下文堆栈为空时,将逐个执行微任务队列中的每个微任务,与任务的不同之处在于,微任务的执行会一直持续到微任务队列为空——即使在此期间计划了新任务。换句话说,微任务可以将新的微任务编入队列,这些微任务将在下一个任务开始运行之前和当前事件循环迭代结束之前执行。

什么时候会产生任务

mdn上给了我们答案

  • A new JavaScript program or subprogram is executed (such as from a console, or by running the code in a <u>[<script>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)</u> element) directly.
  • An event fires, adding the event's callback function to the task queue.
  • A timeout or interval created with <u>[setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout)</u> or <u>[setInterval()](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)</u> is reached, causing the corresponding callback to be added to the task queue.

常见的微任务

  • promise.then

  • process.nextTick

  • ...

点击事件/滚动事件均会向任务队列中加入一个任务,但是由于foo函数递归增加微任务,因此使得事件循环在无尽处理微任务,所以页面会直接失去响应。

总结

微任务是任务的一个步骤,所以是一个任务先执行,接着当前所有微任务逐条执行;

然后是下一个任务执行, 然后执行当前任务所有的微任务,以此重复下去。如下图所示:

image.png

解释一下我个人在本文中称呼其为“任务”而不是“宏任务”的原因,宏任务听起来总是跟微任务的对立的关系。甚至有不少的社区讨论者给出来的结论的【先执行宏任务,再执行微任务】。这种是片面的,微任务是JavaScript级别的,宏任务是宿主级别的,他们之间是包含关系,不是先后关系,也不是对立关系。

思考:在有任务的情况下,我们为什么还需要微任务

这种设计是为了给紧急任务一个插队的机会,否则新入队的任务永远被放在队尾。区分了微任务和宏任务后,本轮循环中的微任务实际上就是在插队,这样微任务中所做的状态修改,在下一轮事件循环中也能得到同步。

这个操作,确保任务的顺序一致,即使结果或数据是同步的,但同时减少用户可识别的操作延迟的风险。

例子就不搬了,自己去看mdn吧,链接在文末

本文只是粗略介绍了任务与微任务的差异,实际上关联的知识点非常多,包括事件循环,js的单线程,浏览器的多进程,浏览器每个tab是多线程等细节都是每一个前端开发都需要了解的。可点击文末的链接进行了解

来做道面试题吧

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function() {
  console.log('settimeout')
})

async1()

new Promise(function(resolve) {
  console.log('promise1')
  resolve()
}).then(function() {
  console.log('promise2')
})

console.log('script end')

参考:

Tasks, microtasks, queues and schedules

In depth: Microtasks and the JavaScript runtime environment - Web APIs | MDN

Using microtasks in JavaScript with queueMicrotask() - Web APIs | MDN

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336