Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

彻底理解 JS Event Loop(浏览器环境) #2

Open
daihere1993 opened this issue Apr 5, 2018 · 0 comments
Open

彻底理解 JS Event Loop(浏览器环境) #2

daihere1993 opened this issue Apr 5, 2018 · 0 comments

Comments

@daihere1993
Copy link
Owner

daihere1993 commented Apr 5, 2018

最近罗列了一些软件开发基础知识点,计划逐一的、彻底的理解每一个知识点,并为每个知识点写一篇详细的,图文并茂的文章。这篇是关于浏览器环境下 JS 的 Event Loop 机制(如有错误,欢迎指出)。

浏览器线程

我们常说 JS 是单线程语言,但是别忘了常见的浏览器内核可都是多线程的,多个线程间会进行不断通讯,通常会有如下几个线程:

  • GUI 渲染线程
  • JS 引擎线程
  • 定时器线程
  • 事件触发线程
  • 异步 HTTP 请求线程

Microtask 与 Macrotask

在大多数解释 JS Event Loop 的文章中,鲜有谈及 Miscrotask 和 Macrotask 这两个概念,但这两个概念却是非常的重要,我在翻阅 Zone.js Primer 时,里面就经常会提及这两个概念,当时也是看的云里雾里的,这也是我写这篇文章的原因之一。

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 的实际运用了,如果回答不出,那可能还得接着往下看。

问题:是先执行 then( ) 中的回调函数呢,还是 setTimeout( ) 中的回调函数呢?

答案:先执行前者。因为 Promise.prototype.then( ) 是 Microtask ,而 setTimeout( ) 是 Macrotask 。至于为什么先执行 Miscrotask ?继续往后看~

在 JS 线程中程序的每一个调用都被看成是一个任务(task) ,所有的任务被分成许多类型且存放在对应类型的队列中,为了方便理解,我把这些任务队列分成三类:

  • Micro-task queue: 存放 microtask 的回调函数。

  • Macro-task queue: 存放 macrotask 的回调函数 。

  • Other-task queue: 这是一个我个人抽象出来队列,实际并不存在,假设该队列用来存放除了 microtask 和 macrotask 外的所有任务。

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 的实现

js_event_loop

如上,根据个人的理解,我画了一个浏览器环境下 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 为例):

  1. JS 线程判断某个 macrotask 是一个定时器,将这个定时器同步给定时器线程。
  2. 定时器线程启动从 JS 线程收到的定时器。
  3. JS 线程在某个时候(可能是执行上述步骤 1 的时候)会通过定时器和 http 请求等一些线程来更新 macro-task queue ,即如果以上的定时结束了,JS 线程就可以将对应定时器的回调函数存放到 macro-task queue 中。

如何理解 JS 中的异步

目前普遍对异步的解释可能是:执行调用,如果立即得到结果就是同步调用,否则为异步调用。

在 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.👊

参考链接

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant