博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
聊聊JavaScript异步中的macrotask和microtask
阅读量:5149 次
发布时间:2019-06-13

本文共 6491 字,大约阅读时间需要 21 分钟。

前言

首先来看一个JavaScript的代码片段:

console.log(1);setTimeout(() => {  console.log(2);  Promise.resolve().then(() => {    console.log(3)  });}, 0);new Promise((resolve, reject) => {  console.log(4)  resolve(5)}).then((data) => {  console.log(data);})setTimeout(() => {  console.log(6);}, 0)console.log(7);

如果你能知道正确的答案,那么后续的内容可以略过了;如果不能建议看看下面有关js异步的内容,百利无一害,??。

任务队列

js的一大特点是单线程,即同一个时间只能做一件事,这样设计主要与其作为浏览器脚本语言有关,js主要用途是用户交互以及操作dom,这决定其是单线程设计,否则会带来复杂的同步问题。比如一个线程删除一个节点,而另一个线程要操作该节点,浏览器不知以哪个线程为准。

单线程意味着任务需要排队,如果前一个任务耗时长,那么就会阻塞后续任务的执行。为此js出现了同步和异步任务,二者都需要在主线程执行栈中执行;其中异步任务需要进入任务队列(task queue)进行排队,其具体运行机制如下:

  • 同步任务在主线程上执行,形成一个执行栈

  • js会将主线程执行栈中的异步任务置于任务队列排队

  • 一旦主线程执行栈同步任务执行完毕处于空闲状态时,就会将任务队列中任务入栈开始执行

还是先来看一个js片段:

console.log('script start')setTimeout(function() {    console.log('timeout')}, 0)console.log('script end')

这段代码在进入主线程执行时,当执行到setTimeout时会将其放置到异步任务队列中,即使设置时间为0也不会马上执行,必须等到主线程执行栈空闲时(执行完console.log('script end')语句后)才会读取异步队列的任务执行。

macrotask与microtask

二者任务都会被放置于任务队列中等待某个时机被主线程入栈执行,其实任务队列分为宏任务队列和微任务队列,其中放置的分别为宏任务和微任务。

  • macrotask(宏任务) 在浏览器端,其可以理解为该任务执行完后,在下一个macrotask执行开始前,浏览器可以进行页面渲染。触发macrotask任务的操作包括:

    • scrip(整体代码)

    • setTimeoutsetIntervalsetImmediate

    • I/OUI交互事件

    • postMessageMessageChannel

  • microtask(微任务)可以理解为在macrotask任务执行后,页面渲染前立即执行的任务。触发microtask任务的操作包括:

    • Promise.then

    • MutationObserver

    • process.nextTick(Node环境)

下面通过例子来看看二者的不同:

console.log('script start');setTimeout(function() {  console.log('timeout');}, 0);Promise.resolve().then(function() {  console.log('promise1');}).then(function() {  console.log('promise2');});console.log('script end');

上面一段代码输出结果为:

script start > script end > promise1 > promise2 > timeout

具体的可视化操作演示可以参考。

上面代码运行到最后一句console后,生成的任务队列:

macrotasks:【setTimeout回调】

microtasks:【Promise.then回调1, Promise.then回调2】

两种不同的任务队列,为啥microtask的任务会先执行呢,这就要说说macrotask与microtask的运行机制[3]如下:

  • 执行一个macrotask(包括整体script代码),若js执行栈空闲则从任务队列中取

  • 执行过程中遇到microtask,则将其添加到micro task queue中;同样遇到macrotask则添加到macro task queue中

  • macrotask执行完毕后,立即按序执行micro task queue中的所有microtask;如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行

  • 所有microtask执行完毕后,浏览器开始渲染,GUI线程接管渲染

  • 渲染完毕,从macro task queue中取下一个macrotask开始执行

Event loop

在主线程执行栈空闲的情况下,从任务队列中读取任务入执行栈执行,这个过程是循环不断进行的,所以又称Event loop(事件循环)。

Event loop是一个js实现异步的规范,在不同环境下有不同的实现机制,例如浏览器和NodeJS实现机制不同:

  • 浏览器的Event loop是按照定义来实现,具体的实现留给各浏览器厂商

  • NodeJS中的Event loop是基于libuv实现

下面来说说浏览器环境下的Event loop,首先借用一幅图:

408483-20190912104534185-1914866236.png

根据对Event loop规范描述来简单说明事件循环模型:

  1. 按先进先出原则选择最新进入Event loop任务队列的一个macrotask,若没有则直接进入第6步的microtask

  2. 设置Event loop的当前任务为上面一步选择的任务

  3. 进栈运行所选的任务

  4. 运行完毕设置Event loop的当前任务为null

  5. 将第一步选择的任务从任务队列中删除

  6. 执行microtask:perform a microtask checkpoint,具体执行步骤参考

  7. 更新并进行UI渲染

  8. 返回第一步执行

microtask的应用

根据Event loop机制,macrotask的一个任务执行完后就进行UI渲染,然后进行另一个macrotask任务执行,macrotask任务的应用就不做过多介绍。下面来说说microtask任务的应用场景,我们以vue的异步更新DOM来做说明,先看官网的说明:

Vue异步执行DOM更新,只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

也就是说,Vue绑定的数据发生变化时,页面视图不会立即重新更新,需要等到当前任务执行完毕时进行更新。例如下面代码:

export default { data () { return { test: 'begin' }; }, methods () { handleClick () { this.test = 'end'; console.log(this.$refs.test.innerText);//打印“begin” } }}

上面代码在执行this.test = 'end'后,页面视图绑定数据test发生变化,若按照同步执行代码,视图应该能马上获取到对应dom的内容,但是并没有获取到。这是因为Vue采用异步视图更新的。具体来说就是Vue在侦听到数据变化时,异步更新视图最终是通过nextTick来完成的,而该方法默认采用microtask任务来实现异步任务,具体的可以参考;这样在 microtask 中就完成数据更新,task 结束就可以得到最新的 UI 了。上面代码如下:

handleClick () { this.test = 'end'; this.$nextTick(() => {  console.log(this.$refs.test.innerText);//打印"end" });}

按照描述,macrotask、microtask和UI 渲染的执行顺序:

一个macrotask任务 --> 所有microtask任务 --> UI 渲染

既然nextTick是按照microtask来实现异步的,那么microtask任务应该是在UI渲染前执行的,为什么表现的是microtask在UI 渲染之后执行的呢?可能有人对上面提出过质疑。猜测原因如下,具体原因可以参考。

JS更新dom是同步完成的,但是UI渲染是异步的。

microtask跨浏览器实现

从Vue的nextTick方法的实现以及的实现可以看出,怎么实现Event loop中的microtask实现呢?那就是借助js原生支持的Promise、MutationObserver(浏览器)、process.nextTick(nodejs环境)来实现,均不支持时使用setTimeout(fn, 0)来兜底降级实现。下面就来简单说说microtask的实现思路:

  • 浏览器是否原生实现Promise,有则使用Promise类似如下实现,否则走下一步。

    if (typeof Promise !== 'undefined' && isNative(Promise)) {  const p = Promise.resolve()  microTimerFunc = () => {    p.then(handle)  }
  • 浏览器环境是否原生支持MutationObserver,支持可以这么实现,否则走下一步。

    function microFun(handle) { var observer = new MutationObserver(handle); var element = document.createTextNode(''); observer.observe(element, {   characterData: true }); return function () {   element.data = blabla; };}
  • 浏览器是否支持onreadystatechange事件,支持则创建一个空的script标签,一旦插入到document中,其onreadystatechange事件将会异步地触发,比setTimeout(fn,0)快,否则走下一步

    function microFun(handle) {  return function () {    var scriptEl = document.createElement('script');    scriptEl.onreadystatechange = function () {      handle();      scriptEl.onreadystatechange = null;      scriptEl.parentNode.removeChild(scriptEl);      scriptEl = null;    };    document.documentElement.appendChild(scriptEl);    return handle;  };};
  • 使用setTimeout(fn, 0)来兜底实现

下面看一下core-js模块中Promise中对microtask的模拟实现,具体可以参考:

module.exports = function () {  var head, last, notify;  var flush = function () {    var parent, fn;    if (isNode && (parent = process.domain)) parent.exit();    while (head) {      fn = head.fn;      head = head.next;      try {        fn();      } catch (e) {        if (head) notify();        else last = undefined;        throw e;      }    } last = undefined;    if (parent) parent.enter();  };  // Node.js  if (isNode) {    notify = function () {      process.nextTick(flush);    };  // browsers with MutationObserver  } else if (Observer) {    var toggle = true;    var node = document.createTextNode('');    new Observer(flush).observe(node, { characterData: true }); // eslint-disable-line no-new    notify = function () {      node.data = toggle = !toggle;    };  // environments with maybe non-completely correct, but existent Promise  } else if (Promise && Promise.resolve) {    var promise = Promise.resolve();    notify = function () {      promise.then(flush);    };  // for other environments - macrotask based on:  // - setImmediate  // - MessageChannel  // - window.postMessag  // - onreadystatechange  // - setTimeout  } else {    notify = function () {      // strange IE + webpack dev server bug - use .call(global)      macrotask.call(global, flush);    };  }  return function (fn) {    var task = { fn: fn, next: undefined };    if (last) last.next = task;    if (!head) {      head = task;      notify();    } last = task;  };};

问题答案

对于文章开头的js代码,其最终输出内容为:

1 -> 4 -> 7 -> 5 -> 2 -> 3 -> 6

可以从以下几个步骤来简单分析,具体执行步骤如下图所示:

408483-20190913122454767-2038890726.png

参考文献

转载于:https://www.cnblogs.com/wonyun/p/11510848.html

你可能感兴趣的文章
Real-Time Rendering 笔记
查看>>
如何理解HTML结构的语义化
查看>>
Activity之间的跳转:
查看>>
实验四2
查看>>
多路复用
查看>>
Python数据可视化之Pygal(雷达图)
查看>>
Java学习笔记--字符串和文件IO
查看>>
转 Silverlight开发历程—(画刷与着色之线性渐变画刷)
查看>>
SQL语法(3)
查看>>
在js在添版本号
查看>>
sublime3
查看>>
Exception Type: IntegrityError 数据完整性错误
查看>>
Nuget:Newtonsoft.Json
查看>>
Hdu - 1002 - A + B Problem II
查看>>
Android设置Gmail邮箱
查看>>
js编写时间选择框
查看>>
JIRA
查看>>
小技巧——直接在目录中输入cmd然后就打开cmd命令窗口
查看>>
深浅拷贝(十四)
查看>>
HDU 6370(并查集)
查看>>