Event Loop

2019-07-07  本文已影响0人  lyp82nkl

JS单线程

JS是一门单线程的非阻塞的脚本语言,只有一个主线程来处理所有任务。当JS执行一系列任务时,由于JS是单线程的,同一时间只能处理一个任务,于是这些任务就在执行栈中排队,JS会依次执行这些任务。当JS执行一项异步任务(如I/O)时,主线程不会一直等待其返回结果,而是挂起(pending)这个任务,继续执行执行栈中的其他任务,等异步任务返回结果时再根据一定规则去执行相应的回调。当异步任务返回结果时,JS会将这个异步任务加入与当前执行栈不同的另一个队列--事件队列。被加入事件队列的异步任务的回调不会立即被执行,而是等待当前执行栈中的任务执行完毕,主线程闲置时,JS从事件队列中,取出排在第一位的任务,将对应回调放入执行栈,并执行其中的同步代码,如此反复,形成一个无限循环,称为事件循环(Event Loop)。

浏览器的事件循环


上图所示,js中的基本数据与对象都会储存在栈内存中,其中复杂类型数据对象会在堆内存储存其数据结构,栈内存储存的是对这个数据结构的引用。

执行栈

javaScript是单线程,也就是说只有一个主线程,主线程有一个栈。当JS代码执行时,代码会被推入执行栈中进行运行,运行代码的过程中,同步事件会立即执行,其中Dom、Ajax以及SetTimeout等异步事件会注册回调函数,放入事件回调队列中,等同步代码执行完之后执行。这样一个循环便是浏览器的Event Loop。


Macro Task 和 Micro Task

在栈内存中代码执行完后,浏览器空闲,立即处理回调队列,将回调队列中的宏任务队列中的事件推入执行栈中执行。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

let promise = new Promise((resolve, reject)=>{
    console.log(1)
    resolve()
})
promise.then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

执行结果:



循环1:

  1. 首先整个代码被推到执行栈中执行,这是一个宏任务(整个script代码)
  2. 运行中,同步代码立即执行,new Promise中的fn是立即执行的。setTimeout被放在宏任务队列中,promise1、promise2被放在微任务队列中。
  1. 宏任务script执行完后,执行微任务队列,取出microtask队列,推入执行栈执行,第一次循环到此结束。

循环2:

  1. 取出宏任务中的setTimeout推入执行栈执行,如果有微任务则,则被放在微任务队列(这里没有)。
  2. 宏任务执行完,去微任务队列执行(微任务队列为空)。
  1. 宏任务队列为空,循环至此结束。

Node.js 的 Event Loop

当 Node.js 启动时,会做这几件事

  1. 初始化 event loop
  2. 开始执行脚本(或者进入 REPL,本文不涉及 REPL)。这些脚本有可能会调用一些异步 API、设定计时器或者调用 process.nextTick()
  3. 开始处理 event loop
    nodejs的Event Loop 一共有6个阶段:


各阶段详解

1. timers 此阶段执行由setTimeout()和setInterval()的回调。
2. poll 轮训阶段执行 文件、网络请求等 除了timers 以外的事情
const fs = require('fs');

function someAsyncOperation(callback) {
  // 假设需要95ms才能完成
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(function() {

  const delay = Date.now() - timeoutScheduled;

  console.log(delay + 'ms have passed since I was scheduled');
}, 100);
// 100ms执行

// 执行一个耗时95ms的异步操作
someAsyncOperation(function() {

  const startCallback = Date.now();

  // 执行一个耗时 10ms 的同步操作
  while (Date.now() - startCallback < 10) {
    // 什么也不做
  }
});
// 在 95ms 的时候执行同步操作 需要 10ms
// 在 100ms 的时候正在执行 同步操作 ,执行不了 settimeout 这个定时器
// 等同步操作执行完了 在执行 settimeout 这个定时器,需要 105ms 才能执行

当 event loop 进入 poll 阶段,发现 poll 队列为空(因为文件还没读完),event loop 检查了一下最近的计时器,大概还有 100 毫秒时间,于是 event loop 决定这段时间就停在 poll 阶段。在 poll 阶段停了 95 毫秒之后,fs.readFile 操作完成,一个耗时 10 毫秒的回调函数被系统放入 poll 队列,于是 event loop 执行了这个回调函数。执行完毕后,poll 队列为空,于是 event loop 去看了一眼最近的计时器(译注:event loop 发现卧槽,已经超时 95 + 10 - 100 = 5 毫秒了),于是经由 check 阶段、close callbacks 阶段绕回到 timers 阶段,执行 timers 队列里的那个回调函数。这个例子中,100 毫秒的计时器实际上是在 105 毫秒后才执行的。

check 处理setImmediate()的回调。
// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

//$ node timeout_vs_immediate.js
//timeout
//immediate

//$ node timeout_vs_immediate.js
//immediate
//timeout
nextTick() 不属于Event Loop的一部分,nextTick()会先于所有的回调执行。
setTimeout(()=>{
    console.log('setTiomeout')
},0)

setInmediate(()=>{
    console.log('setInmediate')
})

proces.nextTick(()=>{
    console.log('nextTick')
})

上述代码中nextTick先于其它两个执行,Vue中有Vue.nextTick()方法就是类似的思想。
参考:
Event Loop、计时器、nextTick

上一篇 下一篇

猜你喜欢

热点阅读