微任务/宏任务和同步/异步之间的关系

·  阅读 568
微任务/宏任务和同步/异步之间的关系

前言

(最后梳理比较简洁,在慢慢完善)

网上已有很多相关的js执行机制的文章了,那为啥还要写这个?

原因是其中一个机制大家有两套说法,不多BB直接上争议点 ↓ (不想看或已了解的直接跳到最后总结)

一.争议点

1.认为宏任务包含所有script代码的

所以,我个人的理解是:宏任务便是 JavaScript 与宿主环境产生的回调,需要宿主环境配合处理并且会被放入回调队列的任务都是宏任务

作者:Reed 链接:https://juejin.cn/post/6844903814508773383 来源:掘金

这里可能有小伙伴不清楚宿主环境的概念,我简单描述下: 宿主环境是作为js运行的一个载体,常见的有浏览器、node.js等

我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

作者:ssssyoki 链接:https://juejin.cn/post/6844903512845860872 来源:掘金

这个是笔者认同的,即:宏任务包含script主代码块

2.宏任务不包含所有script代码

我觉得不能这样理解,首先宏任务和微任务的定义,都是异步的js语句。console.log明显是同步语句。一个js文件的执行,应该是主执行栈先执行同步语句,遇到异步语句,放入任务队列。之后执行微任务队列,然后从宏任务队列取出头部的宏任务执行,执行过程中会产生新的微任务队列。这样循环执行,直到宏任务和微任务全部执行完。eventloop 作者:爱吃橘子 链接:https://segmentfault.com/q/1010000023206213?utm_source=tag-newest 来源:segmentfault思否

因为代码不是任务,所以 console.log()这句代码也不是任务,更不是宏任务

虽然社区有人总喜欢列举 Promise.then、MutationObserver 是微任务(这里没列举完全),但是没捋清事件循环机制的前提下,死记硬背这些个“微任务”的话,面试官很容易借此挖坑请你跳。 所谓任务,浅显来说就是代码块开始执行的入口(确切地说,是函数栈的入口,但是栈的概念较为复杂,不表)。而在 JS 里,除了“script整体代码块”之外,所有代码块的入口都是“回调函数”,回调函数被注册到事件后不会马上被执行,而是保存在一个神秘的的地方,保存起来待执行的才能算“任务”,然后才有宏/微任务之分。 “script整体代码块”的特殊之处,在于它的入口不是回调函数,但是我们可以想象它被装在一个隐形的函数里,作为回调函数被注册到某个事件里(大概是它解析完成之后会触发的一个事件),这时候这个隐形的函数就成为了一个任务。

作者:madRain 链接:https://segmentfault.com/q/1010000023206213?utm_source=tag-newest 来源:segmentfault思否

简单总结就是:异步代码才区分宏任务微任务

3.界限不清的

微任务和宏任务皆为异步任务,它们都属于一个队列,主要区别在于他们的执行顺序,Event Loop的走向和取值。那么他们之间到底有什么区别呢? ... 而宏任务一般是:包括整体代码script,setTimeout,setInterval、setImmediate。

微任务:原生Promise(有些实现的promise将then方法放到了宏任务中)、process.nextTick、Object.observe(已废弃)、 MutationObserver 记住就行了。 作者:张倩qianniuer 链接:https://juejin.cn/post/6844903638238756878 来源:掘金

二.(宏)任务の真相

首先要说明宏任务其实一开始就是任务(task),为什么这么说呢?因为ES6新引入了Promise标准,同时浏览器实现上多了一个microtask微任务概念,作为对照才称宏任务

贴上MDN文档定义

一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。 除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务。

任务队列和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务

由此可以得出结论: 宏任务是包含JS主代码块的。

从区别上还可以很明显看出js事件的循环机制,每次宏任务迭代开始后加入到任务队列中的宏任务要等到下一次任务迭代开始执行。 而微任务中途加入的也会在这次事件循环结束前执行完。

三.知识点

先介绍定义,熟悉的可以直接跳过,看目录中的 完整关系总结

同步和异步

首先要知道js是单线程脚本语言。// 尽管h5提出了worker,但他也是基于单线程实现的

同步是js任务进入任务栈按顺序等待主线程执行的过程,简单来讲就是js从上到下代码的运行的过程,如果中间有个同步任务出现了死循环,那么浏览器可能就会出现js无响应...意思就是无法继续向下执行。

但是我们读取本地文件数据,或者获取服务器接口数据时,花费的时间无法确定,难道之后的js代码就要一直等待吗?这样会导致页面看起来非常卡顿,严重影响用户体验。这个时候就出现了异步,即不进入主线程任务栈,而是先进入event Table并注册回调函数,然后按顺序进入异步事件队列,在主线程的任务栈执行完后调用(队列先进先出,所以按注册函数进入队列的顺序来调用)。

// 文字看的累没关系,可以配合流程图食用 盗的大佬的图

宏任务和微任务

宏任务(macrotask)

进入任务栈等待主线程执行的主代码块,包括从异步队列里加入到栈的,如setTimeout()、setInterval()的回调,其中不含异步队列中的微任务如Promise.then回调。

此时注意事件循环中(event loop)从异步队列加入到栈的宏任务是作为下一个事件来执行的,由于GUI渲染线程机制,每次事件循环后都会进行页面渲染,如下图:

第一次宏任务完成 → 页面渲染 → 第一次宏任务完成(包含上一次宏任务事件时,异步队列中加入的宏任务) → 页面渲染....

常见宏任务有:

  • 主代码块
  • setTimeout
  • setInterval

微任务(microtask)

是异步队列中,在当前这一次宏任务执行完后,页面渲染之前要执行的任务。

此时注意,即使当前微任务执行过程中,产生了新的微任务,也会在下一个宏任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

第一次宏任务 → 第一次所有微任务 → 页面渲染 → 第二次宏任务(包含上一次宏任务事件时,异步队列中加入的宏任务)→ 第二次所有微任务 → 页面渲染....

常见微任务有:

  • process.nextTick ()
  • Promise
  • Object.observe

四.完整关系总结

盗的大佬的图

1.主体代码(第一次事件循环开始,所有的script代码)作为宏任务进入任务执行栈,但在主线程执行之前要做一系列操作判断。

2.判断当前任务是同步还是异步,同步的由主线程在任务栈中按先进后出顺序(先局部上下文,再全局上下文)执行,异步判断是宏任务还是微任务。

3.异步中的宏任务放入异步的宏任务event Table(异步队列分两种,宏任务队列和微任务队列,event Table也一样),微任务进入微任务event Table,在回调函数注册之后,再次进入它们对应的队列。

4.当主线程的任务执行完后,会检查微任务队列是否有任务,如果有就执行,如此循环,知道微任务队列没有任务。

5.当前事件的微任务执行完后,开始执行下一次事件,即会执行宏任务队列中的宏任务,如此循环下去,直到没有任务。

一点心里话

写的不咋样,哪里有问题请随意指出,或者有什么想讨论都可以留言,如果看不下去,点击 「硬核JS」一次搞懂JS运行机制大佬的文章。觉得害行的点个赞鼓励一下呗。

辛苦各位看官,谢谢!

后续还会继续完善。

分类:
前端
标签:
分类:
前端
标签: