资讯专栏INFORMATION COLUMN

js异步从入门到放弃(三)- 异步任务队列(task queues)

wuaiqiu / 2962人阅读

摘要:前言本文是对于异步系列第一篇里提到的模型中,所提到的任务队列的展开分析正文说明以下代码均使用浏览器运行关于浏览器表现的差异在最后做补充。的函数执行过程,重复前面的步骤,因此输出此时执行栈和均为空,存储着两个的回调函数。

前言

本文是对于异步系列第一篇里提到的evenloop模型中,所提到的任务队列(task queues)的展开分析

正文

说明:以下代码均使用chrome浏览器运行 关于浏览器表现的差异在最后做补充。

引子-奇怪的执行顺序

先看一个典型的例子:

console.log("script start")
// 第一个异步任务
setTimeout(()=>{
    console.log("setTimeout")
},0)

// 第二个异步任务
Promise.resolve().then(()=>{
    console.log("promise1")
}).then(()=>{ 
  console.log("promise2");
})
console.log("script end")
// 实际输出结果: 
// script start
// script end
// promise1
// promise2
// setTimeout

根据之前说过的evenloop模型,首先输出script startscript end没有什么问题;
但是接下来却发现:
先执行了Promise指定的callback而不是setTimeoutcallback-- Why?

两种任务队列(microtask queue&macrotask queue)

在之前讨论evenloop模型时,粗略提到了任务队列有2种类型:microtask queuemacrotask queue,他们的区别在于:

macrotask的执行:是在evenloop的每次循环过程,取出macrotask queue中可执行的第一个(注意不一定是第一个,因为我们说过例如setTimeout可以指定任务被执行的最少延迟时间,当前macrotask queue的首位保存的任务可能还没有到执行时间,所以queue只是代表callback插入的顺序,不代表执行时也要按照这个顺序)。

microtask的执行:在evenloop的每次循环过程之后,如果当前的执行栈(call stack)为空,那么执行microtask queue中所有可执行的任务

(某些文献内容中 直接把macrotask称为task,或者某些中文文章中把它们翻译成"微任务"和"宏任务",含义都是相似的:macrotask或者task代表相对多带带占据evenloop过程一次循环的任务,而microtask有可能在一次循环中执行多个)

现在回头来解析前面的例子:

第一次执行主函数,输出script start

遇到setTimeout,将对应的callback插入macrotask queue

遇到promise,将对应的callback插入microtask queue

输出script end,主函数运行结束,执行栈清空,此时开始检查microtask queue,发现里面有可运行的任务,因此按顺序输出promise1promise2

microtask queue执行完,开始新一轮循环,从macrotask queue取出setTimeout任务并执行,输出setTimeout

结束,呈现上面的输出结果。

常见异步操作对应的回调函数任务类型如下:

macrotask: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering

microtask: process.nextTick, Promises, Object.observe, MutationObserver

大概可以这样区分:和html交互密切相关的异步操作,一般是macrotasks;由emcascript的相关接口返回的异步操作,一般是microtasks

如何判断执行顺序

接下来看一个更复杂的例子,帮助理解不同异步任务的执行顺序




    
outer
inner

点击inner部分,打开chrome的调试器,可以看到console打出的结果是:

click

promise

mutate

click

promise

mutate

timeout

timeout

接下来分析运行过程 (建议打开chrome单步调试,进行观察分析):

点击inner,触发对应的onClick事件,此时inner对应的onClick函数进入执行栈;

运行console.log("click"),输出(1)click

运行setTimeout,macrotask queue添加对应的console函数

运行Promise,此时microtask queue添加对应的console函数

运行outer.setAttribute,触发MutationObserver,microtask queue添加对应的console函数(前面注明了MutationObserver创建的回调任务类型是microtask)

当前函数执行完毕,由于执行栈清空,此时开始调度microtask queue,因此依次输出(2)promise(3)mutate,此时当前执行栈call stackmicrotask queue均为空,但是macrotask queue里依然存储着两个东西--inner的Click触发的任务,以及先前setTimeout的回调函数。

inner的onclick函数虽然执行完毕,但是由于事件冒泡,紧接着要触发outeronClick的执行函数,因此setTimeout的回调暂时还无法执行。

outeronClick函数执行过程,重复前面的2-5步骤,因此输出(4)click (5)promise (6)mutate

此时执行栈call stackmicrotask queue均为空,macrotask queue存储着两个setTimeout的回调函数。,根据evenloop模型,开始分别执行这两个task,于是输出了两个(7)和(8)timeout

结束。

再次建议在调试器查看上面的步骤,尤其要注意观察call stackmicrotask queue macrotask queue的变化,会更加直观

在充分理解上面例子的基础上,我们把点击inner部分的这个操作,改成直接在js代码的末尾加上innner.click(),请问结果是否一致呢?

先说最终结果:

click

click

promise

mutate

promise

timeout

timeout

与前一次的结果完全不同!
接下来再次进入调试分析:

由于是直接执行inner.click(),这次进入inner绑定的onclick函数时,与前面是有所不同的:

通过chrome调试器可以看到,此时的call stack有两层--除了onClick函数之外,还有一层匿名函数,这层函数其实就是最外层的script,相当于window.onload绑定的处理函数。

这是很关键的一点!!!就是这一个区别,导致了整个执行结果的差异。
因为前面的例子的执行顺序是:

页面加载后先运行了整个匿名函数

函数出栈

点击时触发inner的onclcik

此时onClick对应的函数进栈。

两次执行到onclick时的callstck区别如图:

第一次,通过点击inner触发click

第二次,通过代码直接触发click

接下来分析本次的输出顺序:

重复前面例子中,步骤2-5,输出一个(1)click

inneronClick函数执行完毕,但是这次执行栈并未清空,因为当前匿名函数还在执行栈里,因此无法开始调度microtask queue!!!(前面说过,microtask queue的调度必须在当前执行栈为空的情况下),因此,这时候会先进入冒泡事件触发的onClick

类似的,输出(2)clcik之后,promise的回调函数进入microtask queue

运行outer.setAttribute,触发MutationObserver,但是此时microtask queue无法再次添加对应的回调函数了,因为已经有一个存在的监听函数在pengding

两个onclick执行完毕,执行栈清空,接下来开始调度microtask queue,输出(3)promise (4)mutate (5)promise

此时当前执行栈call stackmicrotask queue均为空,macrotask queue存储着两个setTimeout的回调函数。根据evenloop模型,开始分别执行这两个task,于是输出了两个(6)和(7)timeout

结束

这两个例子的对比,着重说明了一点:
--microtask queue存储的任务,必须要在当前函数执行栈为空时才会开始调度。
完整内容可参见html标准中的8.1.4部分

结论

macrotask会按顺序执行,并且有可能被中途插入浏览器render,例如上面的冒泡事件

microtask的执行有两个条件:

在每个macrotask结束之后

当前call stack为空

ps:浏览器差异

上述代码在chrome的浏览器下测试结果,可能和在某些版本的firefox和ie浏览器下不一致,在某些浏览器中可能会把promise的回调函数当做mascrotask,但是:

普遍的共识把 Promise当做是miscrotask,并且有比较充分的理由:如果把promose当做是task(即mascrotask)将会导致一些性能问题--因为task的调度是可以被其他task相关的任务如Render打断,还会因为与其他任务源的交互导致不确定性。
参考文献

Tasks, microtasks, queues and schedules

HTML Living Standard

如果觉得写得不好/有错误/表述不明确,都欢迎指出
如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处。如果有问题也欢迎私信交流,主页有邮箱地址
如果觉得作者很辛苦,也欢迎打赏~

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

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

相关文章

  • js异步入门放弃(实践篇) — 常见写法&面试题解析

    摘要:前文该系列下的前几篇文章分别对不同的几种异步方案原理进行解析,本文将介绍一些实际场景和一些常见的面试题。流程调度里比较常见的一种错误是看似串行的写法,可以感受一下这个例子判断以下几种写法的输出结果辨别输出顺序这类题目一般出现在面试题里。 前文 该系列下的前几篇文章分别对不同的几种异步方案原理进行解析,本文将介绍一些实际场景和一些常见的面试题。(积累不太够,后面想到再补) 正文 流程调度...

    Awbeci 评论0 收藏0
  • 大话javascript 4期:事件循环(2)

    摘要:只要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取。三主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环。 一、任务队列 同步任务与异步任务的由来 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。 如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候C...

    李昌杰 评论0 收藏0
  • Event Loop - JS执行机制

    摘要:心塞塞根据规范,事件循环是通过任务队列的机制来进行协调的。等便是任务源,而进入任务队列的是他们指定的具体执行任务回调函数。然后当前本轮的结束,主线程可以继续取下一个执行。 依然是:经济基础决定上层建筑。 说明 首先,旨在搞清常用的同步异步执行机制 其次,暂时不讨论node.js的Event Loop执行机制,以下关于浏览器的Event Loop执行机制 最后,借鉴了很多前辈的研究文...

    muddyway 评论0 收藏0
  • javascript引擎执行的过程的理解--执行阶段

    摘要:如果对语法分析和预编译,还有疑问引擎执行的过程的理解语法分析和预编译阶段。参与执行过程的线程分别是引擎线程也称为内核,负责解析执行脚本程序的主线程例如引擎。以上便是引擎执行宏任务的整个过程。 一、概述 js引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段,上篇文章我们介绍了语法分析和预编译阶段,那么我们先做个简单概括,如下: 1、语法分析: 分别对加载完成的代码块进行语法...

    SnaiLiu 评论0 收藏0
  • javascript引擎执行的过程的理解--执行阶段

    摘要:如果对语法分析和预编译,还有疑问引擎执行的过程的理解语法分析和预编译阶段。参与执行过程的线程分别是引擎线程也称为内核,负责解析执行脚本程序的主线程例如引擎。以上便是引擎执行宏任务的整个过程。一、概述 js引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段,上篇文章我们介绍了语法分析和预编译阶段,那么我们先做个简单概括,如下: 1、语法分析: 分别对加载完成的代码块进行语法检验,语...

    Achilles 评论0 收藏0

发表评论

0条评论

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