JavaScript:再谈Tasks和Microtasks

Stella981
• 阅读 833

JavaScript是单线程,也就是说JS的堆栈中只允许有一类任务在执行,不可以同时执行多类任务。在读js文件时,所有的同步任务是一条task,当然了,每一条task都是一个队列,按顺序执行。而如果在中途遇到了setTimeout这种异步任务,就会将它挂起,放到任务队列中去执行,等执行完毕后,如果有callback,就把callback推入到Tasks中去,注意,是把异步任务的完成时的callback推进去,等待执行,而microtasks什么时候执行呢?只要JS stack栈清了,它就执行,它和异步任务不一样的是,它不会新开一个任务队列,就是新开一个task。常见的microtask有promise事件,MutationObserver对象。

这里我补充一下MutationObserver对象:就是监控某个范围内的DOM树如果发生变化时就会触发一些相应的事件,这个是DOM4里面定义的,用来替换DOM3里的Mutation事件,兼容性移动端没什么问题,安卓4.4,ios 6/7,但是IE就比较惨了,只能11以上。

常见的用法如下:

// Firefox和Chrome早期版本中带有前缀
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver

// 选择目标节点
var target = document.querySelector('#some-id');
 
// 创建观察者对象
var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log(mutation.type);
  });    
});
 
// 配置观察选项:
var config = { attributes: true, childList: true, characterData: true }
 
// 传入目标节点和观察选项
observer.observe(target, config);
 
// 随后,你还可以停止观察
observer.disconnect();

这里其实只要关注几点:

第一是构造函数的第一个参数是一个函数,就是回调,但是这个回调要注意的是,它不是立即回调,而是等所有的变化都结束的时候才会统一回调,这里比较坑的就是这个,所谓的所有变化都结束实际上就是指一个task里面,

第二是如果前一个mutation还没有触发回调,那么后续的mutation也不会触发。

回到上面。

所谓的event loop之所以说是一个循环,是因为每次JavaScript执行完Tasks队列中的一个task之后,它都会回过头去看一下是否有待执行的task了,如果有那就继续执行,这是一个不断循环重复的过程,因为你同步任务task执行完成后,挂起的异步任务可能还在任务队列里执行,暂时还没有callback,而异步任务执行完成后,callback开一个新的task,然后进入到Tasks队列中等候,而JS却不知道你已经在那儿排队了,所以没办法,只能通过不断循环的方式来确保每一个待执行的task都能被及时执行。它的逻辑大概如下,具体的下篇再讲:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

这里只是做一个类比。

其实通过上面这个机制,我们可以看出一些问题:

比如:

1,setTimeout一定准时吗,不一定,如果它前面的task执行时间超过了它设置的时间,那它必须得等那个task执行完成之后才能执行,第二个参数的时间值并不代表该时间以后执行setTimeout callback,而是该时间以后将callback这个新的task推入到Tasks里面去等待执行。所以不要写什么setTimeout 0这种以为能立即执行的了,而且W3C规范,setTimeout最小值只能为4。

2,如果我有一个异步队列callback特别长,要执行好久好久,而此时我又触发了一个新的事件,那怎么办?没办法啊,只能等它这个callback执行完才能执行啊,就比如你是一个click事件,你触发了click对应的function,将这个新的task推入到Tasks中去,而此时并不会立即执行,因为前面还有没执行完的任务,所以会造成点击没效果,因为它还在等待前一个异步callback执行完。所以不要以为异步任务是异步的,就可以随意写一堆逻辑,如果太复杂了,也会造成用户操作没反应这种问题,虽然比较少。

3,关于click事件,这里单独说一下,看一个例子:

<div class='outer'>
    <div class='inner'></div>
</div>

// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

JavaScript:再谈Tasks和Microtasks

如果此时我点击里面那个小块儿inner,控制台会打印什么?

click
promise
mutate
click
promise
mutate
timeout
timeout

 这里为什么会触发两次这个函数,很简单,因为冒泡,但是我们能看出一个问题,promise和mutate是微任务,只有JS主线程栈空了它们才会执行,说明每一次冒泡执行完后都会空一下,但是这里要注意的是,虽然主线程空了,但是不代表这里的click事件因为冒泡而被分成了两个task,实际上还是一个task(叫Dispatch click),而timeout虽然很快,但还是排在了两次冒泡的Dispatch click后面,因为前面是一个task,它是新开的一个task,必须等前面一个task执行完毕。那这里也能看出,微任务不一定是在当前task结束的时候才会执行,这里来看个图

JavaScript:再谈Tasks和Microtasks

这里可以看出,所有的微任务都在Microtasks这个队列中等待执行,只要JS stack一清空,它就立即执行,而是否一定是某个task执行完成呢,不一定,只要空了,它就执行。上面那个click冒泡就是很好的证明。但是可以确定的是,微任务一定是在一次事件循环event loop的结尾处执行。

下面来看一个坑爹的,还是上面那些代码,如果不是人为主动点击触发,而是改用js主动触发事件,就比如inner.click(),会是什么结果呢?来看:

click
click
promise
mutate
promise
timeout
timeout

 意不意外?惊不惊喜?timeout就不解释了,同上,但是为什么click是连续执行了,为什么mutate只执行了一次,而且还是夹在了promise中间,针对这几个点,解释一下:

1,为什么click连续执行两次:先来看为什么上面那个不是连续执行,原因很简单,因为js栈空了,所以先执行了微任务,那这次为什么没有先执行微任务,那就是说明JS stack没有空嘛,这里作者给出的解释是,click()会导致事件同步分派,所以调用的脚本.click()仍然处于回调之间的堆栈中。上述规则确保微任务不会中断正在执行的JavaScript。这意味着我们不会在两者之间处理Microtasks队列,而是在它们之后。。。。总之就是如果是js调用的函数,JS堆栈不会空。

2,为什么mutate只执行了一次:当第一次执行到mutate的时候,它被直接插进了Microtasks队列中等待执行,而刚刚说到了MutationObserver这个对象的实例有个特点,当前一个挂起的这个对象还没解决的时候,后续的是不会处理的,所以只有一次,因此只有一个mutate被夹到了两个promise之间。

以上就是整个Tasks,Microtasks,任务队列等等专业知识的解释。

关于event loop的详解,请看后续。

end

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
2个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
菜园前端 菜园前端
1年前
什么是JavaScript 调用栈?
原文链接:什么是调用栈?我们写的JS代码大多数都是同步模式,也就是从上往下依次执行。后一个任务必须要等前一个任务结束才能开始执行,程序的执行顺序和我们代码的编写顺序是完全一致的。程序执行中每遇到一个任务都会先入栈,当前入栈的任务执行完毕后就会出栈。本来栈的
菜园前端 菜园前端
1年前
什么是宏任务与微任务?
原文链接:事件循环机制在事件循环中,每进行一次循环操作称为tick,每一次tick的任务处理是比较复杂的。关键步骤如下:1.执行一个宏任务2.执行过程中如果遇到微任务,就将它添加到微任务的任务队列中3.宏任务执行完毕后,立即执行当前微任务队列中的所有微任务
Stella981 Stella981
2年前
ES6 Promise
Promisepromise是异步编程的一种解决方案1什么是异步?异步模式,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。"异步模式"非常重要。在浏
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
8个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这