JavaScript事件循环

2021-06-09  本文已影响0人  AShuiCoder

事件循环是什么?事实上我把事件循环理解成我们编写的JavaScript和浏览器或者Node之间的一个桥梁。

浏览器的事件循环是一个我们编写的JavaScript代码和浏览器API调用(setTimeout/AJAX/监听事件等)的一个桥梁, 桥梁之间他们通过回调函数进行沟通。

Node的事件循环是一个我们编写的JavaScript代码和系统调用(file system、network等)之间的一个桥梁, 桥梁之间他们通过回调函数进行沟通的。

1. 浏览器的事件循环

1.1. 进程和线程

线程和进程是操作系统中的两个概念:

听起来很抽象,我们直观一点解释:

再用一个形象的例子解释:

image.png

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

你可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程。

1.2. 浏览器和JavaScript

我们经常会说JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。

浏览器是一个进程吗,它里面只有一个线程吗?

但是JavaScript的代码执行是在一个单独的线程中执行的:

分析下面代码的执行过程:

const name = "coderwhy";

// 1.将该函数放入到调用栈中被执行
console.log(name);

// 2. 调用栈
function sum(num1, num2) {
  return num1 + num2;
}

function bar() {
  return sum(20, 30);
}

console.log(bar());

1.3. 浏览器的事件循环

如果在执行JavaScript代码的过程中,有异步操作呢?

const name = "coderwhy";

// 1.将该函数放入到调用栈中被执行
console.log(name);

// 2.调用栈
function sum(num1, num2) {
  return num1 + num2;
}

function bar() {
  return sum(20, 30);
}

setTimeout(() => {
  console.log("settimeout");
}, 1000);

const result = bar();

console.log(result);

那么,传入的一个函数(比如我们称之为timer函数),会在什么时候被执行呢?

image.png

1.4. 宏任务和微任务

但是事件循环中并非只维护着一个队列,事实上是有两个队列:

那么事件循环对于两个队列的优先级是怎么样的呢?

我们来看一个面试题:执行结果如何?

setTimeout(function () {
  console.log("set1");

  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});

new Promise(function (resolve) {
  console.log("pr1");
  resolve();
}).then(function () {
  console.log("then1");
});

setTimeout(function () {
  console.log("set2");
});

console.log(2);

queueMicrotask(() => {
  console.log("queueMicrotask1")
});

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});

执行结果:

pr1
2
then1
queueMicrotask1
then3
set1
then2
then4
set2

async、await是Promise的一个语法糖:

今日头条的面试题:

async function async1 () {
  console.log('async1 start')
  await async2();
  console.log('async1 end')
}
 
async function async2 () {
  console.log('async2')
}

console.log('script start')
 
setTimeout(function () {
  console.log('setTimeout')
}, 0)
 
async1();
 
new Promise (function (resolve) {
  console.log('promise1')
  resolve();
}).then (function () {
  console.log('promise2')
})

console.log('script end')

执行结果如下:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

2. Node的事件循环

2.1. Node的事件循环

浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。

我们来看在很早就给大家展示的Node架构图:

image.png

libuv到底是什么呢?

libuv到底帮助我们做了什么事情呢?

2.2. 阻塞IO和非阻塞IO

如果我们希望在程序中对一个文件进行操作,那么我们就需要打开这个文件:通过文件描述符。

操作系统为我们提供了阻塞式调用和非阻塞式调用:

所以我们开发中的很多耗时操作,都可以基于这样的 非阻塞式调用

但是非阻塞IO也会存在一定的问题:我们并没有获取到需要读取(我们以读取为例)的结果

那么这个轮训的工作由谁来完成呢?

libuv提供了一个线程池(Thread Pool):

阻塞和非阻塞,同步和异步有什么区别?

2.3. Node事件循环的阶段

我们最前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:

但是一次完整的事件循环Tick分成很多个阶段:

我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

但是,Node中的事件循环不只是 微任务队列和 宏任务队列:

所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:

2.4. Node代码执行面试

面试题一:

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

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

setTimeout(function () {
  console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
  console.log('promise1')
  resolve();
  console.log('promise2')
}).then(function () {
  console.log('promise3')
})

console.log('script end')

执行结果如下:

script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3

setTimeout0
setImmediate
setTimeout2

面试题二:

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

setImmediate(() => {
  console.log("setImmediate");
});

执行结果:

情况一:
setTimeout
setImmediate

情况二:
setImmediate
setTimeout

为什么会出现不同的情况呢?

int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  // 计算距离当前时间节点最小的计时器
  heap_node = heap_min(timer_heap(loop));
  // 如果为空, 那么返回-1,表示为阻塞状态
  if (heap_node == NULL)
    return -1; /* block indefinitely */

  // 如果计时器的时间小于当前loop的开始时间, 那么返回0
  // 继续执行后续阶段, 并且开启下一次tick
  handle = container_of(heap_node, uv_timer_t, heap_node);
  if (handle->timeout <= loop->time)
    return 0;

  // 如果不大于loop的开始时间, 那么会返回时间差
  diff = handle->timeout - loop->time;
  if (diff > INT_MAX)
    diff = INT_MAX;

  return (int) diff;
}

和上面有什么关系呢?

上一篇下一篇

猜你喜欢

热点阅读