详解JS运行机制和Event Loop

1 JS运行机制详解

1.1 单线程的JS

javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。因此一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!javascript

1.2 Event Loop

既然js是单线程,后一个任务会等前一个任务执行完成后才会执行,若是前一个任务执行时间过长后面的任务一直得不到执行,就会引发阻塞。那么问题来了,假如咱们想浏览新闻,可是新闻包含的超清图片加载很慢,难道咱们的网页要一直卡着直到图片彻底显示出来?所以咱们会将任务分为两类:html

  • 同步任务
  • 异步任务

当咱们打开网站时,网页的渲染过程就是一大堆同步任务,好比页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。具体逻辑见下面的导图: java

js执行机制

文字描述node

  • 同步和异步任务分别进入不一样的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。准确的讲,event loop是实现异步的一种机制。

上图中Event Queue 包括 macro task queue 和 micro task queue,下一小节咱们会详细解释一下。 上代码咱们体会一下这个流程:网络

console.log('1');
setTimeout(function () {
    console.log('timeout');
});
console.log('2');
复制代码

上面的代码解释多线程

  • console.log('1');console.log('2');是同步任务会放到主线程中,setTimeout声明的回调函数会放到Event Table。主线程内的任务(console.log('1');console.log('2');)执行完毕为空,会去Event Queue读取console.log('timeout');,进入主线程执行。因此执行的结果为1 2 timeout

1.3 Evnet Loop 中的macro task 和 micro task

1.3.1 定义

  • macro-task(宏任务):包括总体代码script,setTimeout,setInterval, setImmediate(node环境下)。
  • micro-task(微任务):Promise,process.nextTick

下面两张图为Event Loop 和 macro-task 及 micro-task的关系异步

event loop & macro task & micro task1

event loop & macro task & micro task2

导图解释ide

  • 不一样类型的任务会进入对应的Event Queue,好比setTimeout和setInterval会进入macro task Queue, Promise 会进入 micro task Queue。
  • 事件循环的顺序,决定js代码的执行顺序。
  • 进入总体代码(宏任务)后,开始第一次循环。接着执行全部的微任务。而后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行全部的微任务。

1.3.2 e.g.

看到这么多的定义和导图,咱们来段代码屡一下:函数

console.log('1');

setTimeout(function () {
    console.log('2');
    process.nextTick(function () {
        console.log('3');
    });
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5')
    })
})
process.nextTick(function () {
    console.log('6');
})
new Promise(function (resolve) {
    console.log('7');
    resolve();
}).then(function () {
    console.log('8')
})
复制代码
  • 总体script做为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到 macro task Queue中。
  • 遇到process.nextTick(),其回调函数被分发到micro task Queue中。咱们记为process1。
  • 遇到Promise,new Promise直接执行,输出7。then被分发到micro task Queue中。咱们记为then1。
macro task Queue macro task Queue
setTimeout process1
- then1
  • 咱们发现了process1和then1两个微任务。
  • 执行process1,输出6。
  • 执行then1,输出8。
  • 好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout宏任务开始:
  • 遇到console.log,输出2。
  • 遇到process.nextTick(),一样将其分发到micro task Queue中,记为process2。new Promise当即执行输出4,then也分发到macro task Queue中,记为then2。
macro task Queue macro task Queue
- process2
- then2
  • 咱们发现了process2和then2两个微任务。
  • 执行process2,输出3。
  • 执行then2,输出5。
  • 好了,第一轮事件循环正式结束,这一轮的结果是输出2,4,3,5。循环结束。最终的结果为1 7 6 8 2 4 3 5

1.4 总结

  • javascript是一门单线程语言
  • 事件循环是js实现异步的一种方法,也是js的执行机制。

2 Node中的Event Loop

2.1 node中Event Loop执行顺序

2.1.1 node中Event Loop的执行顺序的简单介绍

下图为node中Event Loop的执行顺序的简略图 oop

node中Event Loop执行顺序的简略图

note

  • timers: 执行被setTimeout() 和 setInterval()注册的回调函数.
  • I/O callbacks: 执行除了 close事件的回调、 被 timers和setImmediate()注册的回调.
  • idle, prepare: node内部执行
  • poll: 轮询获取新的 I/O 事件; node有可能会在这个地方阻塞.
  • check: 在这里调用setImmediate() 注册的回调.
  • close: 执行close事件的回调

2.1.2 详解poll阶段

1.poll阶段的功能

  • 执行刚刚过时的计时器的脚本。
  • 在轮询队列中处理事件。

2.poll阶段的处理流程

下面我用if else的方式描述一下poll阶段的处理逻辑,以下:

if ('事件循环进入到 poll 阶段 ' && '没有timers注册的scripts') {
    if ('poll 队列 不为空') {
        console.log('循环遍历它的回调队列,以同步执行它们,直到队列耗尽,或者达到系统依赖的最大值');
    } else {
        if ('存在setImmediate()注册的scripts') {
            console.log('结束poll phase 进入到check phase 执行这些注册的scripts');
        } else {
            console.log('事件循环将等待被添加到队列中的回调,而后当即执行它们');
        }
    }
}
console.log('一旦轮询队列为空,事件循环将检查有无到期的计时器。若是有一个或多个计时器准备就绪,事件循环将返回到计时器阶段,以执行这些计时器的回调。');
复制代码

3.比较setImmediate() 和 setTimeout()

setImmediate()setTimeout()很类似的,它们什么时候被调用,决定了它们的行为方式的不一样。

  • setImmediate 用于在当前轮询阶段完成后执行脚本
  • setTimeout用于把注册的脚本在最小阈值结束后运行。

它们执行的顺序将根据调用它们的上下文而变化。若是两个都是从主模块中调用,那么它们将受到进程性能的约束(这可能会受到其余应用程序的影响)。

例如,若是咱们运行的脚本不是在I/O循环中(即主模块),那么执行两个定时器的顺序是不肯定的,由于它受过程性能的约束:

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

setImmediate(() => {
  console.log('immediate');
});
// 打印结果的前后顺序是不肯定的,有时`timeout`在前,有时'immediate'在前
复制代码

可是,若是把这段代码放到I/O循环的回调中,immediate老是先被打印出来,以下:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
// 在一个I/O周期内,在任何计时器的状况下,setImmediate的回调,由于在一个I/O周期内,I/O callback 的下一个阶段为setImmediate的回调。
复制代码

2.2 node的Event Loop实现

以下图:

node中的EventLoop

说明

    1. Node的Event Loop分阶段,阶段有前后,依次是:
    • expired timers and intervals,即到期的setTimeout/setInterval
    • I/O events,包含文件,网络等等
    • immediates,经过setImmediate注册的函数
    • close handlers,close事件的回调,好比TCP链接断开
    1. 同步任务及每一个阶段以后都会清空microtask队列
    • 优先清空next tick queue,即经过process.nextTick注册的函数
    • 再清空other queue,常见的如Promise
    1. node会清空当前所处阶段的队列,即执行全部task

咱们在回头看一下,下面的代码:

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

setImmediate(() => {
  console.log('immediate');
});
复制代码

能够看出因为两个setTimeout延时相同,被合并入了同一个expired timers queue,而一块儿执行了。因此,只要将第二个setTimeout的延时改为超过2ms(1ms无效,由于最小间隔为1s),就能够保证这两个setTimeout不会同时过时,也可以保证输出结果的一致性。

咱们在回头看一下,上面提到的另一段代码:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
复制代码

为什么这样的代码能保证setImmediate的回调优先于setTimeout的回调执行呢?由于当两个回调同时注册成功后,当前node的Event Loop正处于I/O queue阶段,而下一个阶段是immediates queue,因此可以保证即便setTimeout已经到期,也会在setImmediate的回调以后执行。

3 补充

因为水平有限,理解的程度可能会有误差,欢迎你们指正。

4 参考文章

相关文章
相关标签/搜索