最近罗列了一些软件开发基础知识点,计划逐一的、完全的理解每个知识点,并为每一个知识点写一篇详细的,图文并茂的文章。这篇是关于浏览器环境下 JS 的 Event Loop 机制(若有错误,欢迎指出)。html
咱们常说 JS 是单线程语言,可是别忘了常见的浏览器内核可都是多线程的,多个线程间会进行不断通信,一般会有以下几个线程:html5
在大多数解释 JS Event Loop 的文章中,鲜有谈及 Miscrotask 和 Macrotask 这两个概念,但这两个概念倒是很是的重要,我在翻阅 Zone.js Primer 时,里面就常常会说起这两个概念,当时也是看的云里雾里的,这也是我写这篇文章的缘由之一。web
setTimeout(function () {
console.log('timeout1');
}, 0);
console.log('start');
Promise.resolve().then(function () {
console.log('promise1');
Promise.resolve().then(function () {
console.log('promise2');
});
setTimeout(function () {
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('timeout2')
}, 0);
});
console.log('done');
复制代码
以上代码最后会输出什么呢?若是你能很快的回答出来,你大概就已经掌握了 Event Loop 的实际运用了,若是回答不出,那可能还得接着往下看。api
问题:是先执行 then( ) 中的回调函数呢,仍是 setTimeout( ) 中的回调函数呢?promise
答案:先执行前者。由于 Promise.prototype.then( ) 是 Microtask ,而 setTimeout( ) 是 Macrotask 。至于为何先执行 Miscrotask ?继续日后看~浏览器
在 JS 线程中程序的每个调用都被当作是一个任务(task) ,全部的任务被分红许多类型且存放在对应类型的队列中,为了方便理解,我把这些任务队列分红三类:服务器
Micro-task queue: 存放 microtask 的回调函数。多线程
Macro-task queue: 存放 macrotask 的回调函数 。app
Other-task queue: 这是一个我我的抽象出来队列,实际并不存在,假设该队列用来存放除了 microtask 和 macrotask 外的全部任务。webapp
Microtask 和 Macrotask 的区别就是执行顺序上的区别。简单的说,JS 线程会先处理 other-task queue 上的任务,处理完了以后,再去处理 micro-task queue 上的任务,最后才处理 macro-task queue 上的任务。至于 JS 线程具体的执行细节,后面会详细的进行描述。
如下是常见的 Microtask 和 Macrotask:
Microtask :Promise.prototype.then( )、MutationObserver.prototype.observe( ) 等 。
Macrotask :setTimeout( )、setImmediate( )、XMLHttpRequest.prototype.onload( ) 等。
如上,根据我的的理解,我画了一个浏览器环境下 JS 实现 Event Loop 大体模型图,具体含义以下:
1 获取执行的任务,执行步骤 1.1
1.1 判断 other-task queue 中是否有任务,若是有,获取最先的任务而后执行步骤 2 ,不然执行步骤 1.2 。
1.2 判断 micro-task queue 中是否有任务,若是有,获取最先的任务而后执行步骤 2 ,不然执行步骤 1.3 。
1.3 判断 macro-task queue 中是否有任务,若是有,获取最先的任务而后任何执行步骤 2 ,不然执行步骤 3 。
2 将取到的任务放到 call stack 并执行,执行完以后再执行步骤 1 (值得注意的是,在执行的过程当中,是会不断的更新全部的 task queue ,由于 call stack 中正在执行的任务内部也可能存在普通任务、microtask 和 macrotask ,执行任务的过程能够理解为一个递归过程,若是无限递归,call stack 上待执行的任务就会不断累积而溢出,这也就是常见的 Maximum call stack size exceeded 错误)。
3 线程会处理其余工做,例如:不断同步「事件触发线程」的状态,一旦有事件触发,即查看触发事件「target」有没有对应事件的监听器任务,若是有,则选中该任务并执行步骤 2 。须要注意的是,并非只有执行了步骤 1.3 后才会执行当前步骤,JS 线程确定还会在的某个时候去同步其余线程的状态的。
接下来,若是仔细想,可能会产生一个疑问:JS 进程是如何更新 micro-task queue 和 macro-task queue 这两个队列的呢 ?
根据个人理解,micro-task queue 和 other-task queue 都是“同步”更新的,而 macro-task queue 是“异步”更新。如下是 macro-task queue 更新的具体流程(以 setTimeout 为例):
目前广泛对异步的解释多是:执行调用,若是当即获得结果就是同步调用,不然为异步调用。
在 JS 环境中,我我的实际上是不一样意这个解释的。
首先,根据以上的解释,setTimeout( )、Promise.prototype.then( ) 、http 请求和各种浏览器事件,这些都被认为是异步的。但我却不这么认为,我认为浏览器事件不是异步的。如下代码即是理由:
// html: <button id="btn">click</button>
// js
var btn = document.getElementById('btn');
setTimeout(function () {
console.log('timeout')
}, 0);
Promise.resolve().then(function () {
console.log('promise');
});
btn.addEventListener('click', function () {
console.log('click');
});
btn.click();
console.log('done');
复制代码
若是浏览器事件是异步的,无论后续会打印出什么,第一个打印的必然是 done ,而实际的打印结果为:click done promise timeout
。
也就是说,JS 认为浏览器事件并不是异步。
由此,我我的对异步的解释是:在知足调用所需的外在条件的状况下,执行调用,当即得到结果的就为同步调用,不然为异步调用。
根据这个理解,当咱们发起的一个 http 请求时,假设服务器以光速返回请求结果,XMLHttpRequest 对象的 onload 方法会当即执行吗?,显然不会,因此 http 请求为异步调用。这也是为何我在以上分析 Event Loop 中的任务队列时并无将 event-task queue 拎出来的缘由。所以,对于异步调用的判断能够是这样:若是某个调用属于 microtask 或是 macrotask 中的其中一个,那么这个调用就是异步调用。
有人可能会注意到,这篇文章常常出现「我认为」和「我理解」,这并不是是我对本身不自信,而是我想表达一个见解:在翻阅别人的技术文章的时候,务必保持独立思考的能力,就算文章的做者是业界有名的大牛,也不能没原因的「深信不疑」,对对应的技术点务必在自个脑中里创建一个能够自圆其说的模型。至于我为何会表达这个见解,是由于我找翻阅大量的过程当中,发现大多数关于 JS Event Loop 的文章或多或少都有一些粗糙或是错误,若是我只看其中的某一篇,我很大的几率会有创建一个错误的 Event Loop 模型。固然,就我当前的理解,仍是可能会有些许错误。Anyway ,仍是那句话:保持独立思考,与各位共勉。
Done.👊