对EventLoop的理解

返回主页
Eden
关注前端,关注web

博客园
首页
管理

聊聊JavaScript异步中的macrotask和microtask
前言

首先来看一个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任务的操作包括:

    script(整体代码)

    setTimeout、setInterval、setImmediate

    I/O、UI交互事件

    postMessage、MessageChannel

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

具体的可视化操作演示可以参考Tasks, microtasks, queues and schedules。

上面代码运行到最后一句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是按照html标准定义来实现,具体的实现留给各浏览器厂商

NodeJS中的Event loop是基于libuv实现

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

根据HTML Standard - event loop processing model对Event loop规范描述来简单说明事件循环模型:

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

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

进栈运行所选的任务

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

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

执行microtask:perform a microtask checkpoint,具体执行步骤参考这里

更新并进行UI渲染

返回第一步执行

microtask的应用

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

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

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

{{test}}

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

handleClick () {
this.test = ‘end’;
this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …nsole.log(this.refs.test.innerText);//打印"end"
});
}

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

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

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

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

microtask跨浏览器实现

从Vue的nextTick方法的实现以及immediate的实现可以看出,怎么实现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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值