再次理解是事件循环(Event Loop)

1. 事件循环

JavaScript 是单线程运行,异步操作特别重要。

只要用到引擎之外的功能,就需要跟外部交互,从而形成异步操作。由于异步操作实在太多,JavaScript 不得不提供很多异步语法。这就好比,有些人老是受打击, 他的抗打击能力必须变得很强,否则他就完蛋了。

Node 的异步语法比浏览器更复杂,因为它可以跟内核对话,不得不搞了一个专门的库 libuv 做这件事。这个库负责各种回调函数的执行时间,毕竟异步任务最后还是要回到主线程,一个个排队执行。

为了协调异步任务,Node 居然提供了四个定时器,让任务可以在指定的时间运行。

  • setTimeout()
  • setInterval()
  • setImmediate()
  • process.nextTick()

前两个是语言的标准,`后两个是 Node 独有的。它们的写法差不多,作用也差不多,不太容易区别

2.同步任务和异步任务

同步任务即正常业务代码,不含回调

// test.js
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
new promise(()=>{
    console.log(4)
})
Promise.resolve().then(() => console.log(5));
(() => console.log(6))();

/*结果
4
6
3
5
1
2
*/

同步任务总比异步任务任务先执行

上面代码中只有以下两行代码是同步代码,所有最先执行,输46

new Promise(()=>{
    console.log(4)
})
Promise.resolve().then(() => console.log(5));
(() => console.log(6))();

异步任务可以分成两种:

  • 追加在本轮循环的异步任务
  • 追加在次轮循环的异步任务

循环指的是事件循环(EventLoop),本轮循环一定比次轮循环先执行

Node 规定,process.nextTickPromise的回调函数,追加在本轮循环,同步任务执行结束完(优先级process.nextTick>promise.then()),即同步任务一旦执行完成,就开始执行它们。而setTimeoutsetIntervalsetImmediate的回调函数,追加在次轮循环。

// 下面两行,次轮循环执行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面两行,本轮循环执行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(5));

3.process.nextTick()

虽然名字看起来像是次轮循环,实际上是追加在本次循环。

Node执行完所有的同步任务后,就会立马执行process.nextTick的任务队列(nextTickQueue),所以下面这行代码是第三个输出结果。基本上,如果你希望任务尽可能快的执行,那就使用process.nextTick()

process.nextTick(() => console.log(3));

4.微任务

根据语言规定,Promise对象的回调函数,会进入异步任务里面的“微任务队列”(microtask)。
微任务队列追加在process.nextTick队列后面。也属于本轮循环,所以以下代码总是先输出3在输出5

process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(5));
// 3
// 5

注意:只有一个队列趣步清空了以后,才会执行下一个队列。

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

上面代码中,全部process.nextTick()的回调函数,都会早于Promise的。

综上,本轮循环的执行顺序为

  • 1.同步任务
  • 2.process.nextTick()
  • 微任务

5.事件循环的概念

Node文档官方定义如下

When Node.js starts, it initializes the event loop, processes the provided input

script which may make async API calls, schedule timers, or call process.nextTick(),

then begins processing the event loop.

当Node.js启动时,它将初始化事件循环,处理提供的输入脚本,该输入脚本可能进行

异步API调用,调度计时器或调用process.nextTick(),然后开始处理事件循环。

这段话很重重要,首先,有的人认为,除了主线程,还存在一个单独的事件循环线程。不是这样的,只有一个主线程,事件循环是在主线程上完成的。其次,Node开始执行脚本时,会先进行事件循环的初始化,但是此时事件循环还没有开始,会先完成以下工作

  • 同步任务
  • 发出异步请求
  • 规定定时器生效的时间
  • 执行process.nextTick()等等

最后,上面这些事情都干完了,事件循环就开始了。

6.事件循环的六个阶段

事件循环会无限次执行,一轮又一轮,只有异步任务的回调函数队列清空了,才会停止执行。
每一轮的事件循环都分成六个阶段,这些阶段会依次执行

1. timers
2. I/O callbacks
3. idle, prepare
4. poll
5. check
6. close callbacks

每一个阶段都有一个先进先出的回调函数队列,只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

下面简单介绍一下每个阶段的含义,详细介绍可以看官方文档,也可以参考 libuv 的源码解读

1. Timer

这个是定时器阶段,处理setTimeOut()setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段,进行下一阶段

2. I/O callbacks

除了以下操作的的回调函数,其他回调函数都在这个阶段执行。

  • setTimeOut()setInterval()的回调函数
  • setImmediate()的回调函数
  • 用于关闭请求的回调函数,比如socket.on('close',...)

3. idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

4. Poll

1)执行下限时间已经达到的timers的回调,

2)然后处理 poll 队列里的事件。
当event loop进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:

  • 如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
  • 如果 poll 队列为空,则发生以下两件事之一:
    • 如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里面的回调 callback)。
      如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。
    • 但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):
      event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。

快乐叮当

5. check

该阶段执行setImmediate()的回调函数。

6. close callbacks

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。

7.事件循环的例子

const fs = require('fs');

const timeoutScheduled = Date.now();

// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 异步任务二:文件读取后,有一个 200ms 的回调函数
fs.readFile('map.js', () => {
  const startCallback = Date.now();
  console.log(`read time: ${startCallback -timeoutScheduled}ms`)
  while (Date.now() - startCallback < 200) {
    // 什么也不做
  }
});

// read time: 2ms
// 202ms

上面代码有两个异步任务,一个是 100ms 后执行的定时器,一个是文件读取,它的回调函数需要 200ms。请问运行结果是什么?

脚本进入第一轮事件循环以后,没有到期的定时器,也没有已经可以执行的 I/O 回调函数,所以会进入 Poll 阶段,等待内核返回文件读取的结果。由于读取小文件一般不会超过 100ms,所以在定时器到期之前,Poll 阶段就会得到结果,因此就会继续往下执行。

第二轮事件循环,依然没有到期的定时器,但是已经有了可以执行的 I/O 回调函数,所以会进入 I/O callbacks 阶段,执行fs.readFile的回调函数。这个回调函数需要 200ms,也就是说,在它执行到一半的时候,100ms 的定时器就会到期。但是,必须等到这个回调函数执行完,才会离开这个阶段。

第三轮事件循环,已经有了到期的定时器,所以会在 timers 阶段执行定时器。最后输出结果大概是200多毫秒。

8. setTimeout 和 setImmediate

由于setTimeouttimers 阶段执行,而setImmediatecheck 阶段执行。所以,setTimeout会早于setImmediate完成。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上面代码应该先输出 1,再输出2,但是实际执行的时候,结果却是不确定,有时还会先输出2,再输出1

这是因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒2147483647毫秒(2^31 -1)之间。也就是说,setTimeout(f, 0)等同于setTimeout(f, 1)

实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段先执行setImmediate的回调函数

但是,下面的代码一定是先输出2,再输出1。

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

9.部分面试题

  1. process.nextTick + setImmediate
setImmediate(function () {
    console.log(1);
    process.nextTick(function () {
        console.log(2);
    });
});
process.nextTick(function () {
    console.log(3);
    setImmediate(function () {
        console.log(4);
    })
});

// 3 1 2 4

2

setTimeout(function () {
console.log(1)
Promise.resolve().then(() => console.log(3))
process.nextTick(() => console.log(4))
}, 0);
setImmediate(() => console.log(2))

// 1 4 3 2

这个是因为netxtTick任务见缝插针,每个阶段完成都会查询。promise.then只会每轮查询。

3

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('setTimeout3') 
},3)  
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
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
setTimeout3
setImmediate 
*/

4

setTimeout(() => {
    console.log('timeout0');
    new Promise((resolve, reject) => {
        resolve('resolved') }).
        then(res => console.log(res));
    new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('timeout resolved')
      })
    }).then(res => console.log(res));
    process.nextTick(() => {
        console.log('nextTick1');
        process.nextTick(() => {
            console.log('nextTick2');
        });
    });
    process.nextTick(() => {
        console.log('nextTick3');
    });
    console.log('sync');
    setTimeout(() => {
        console.log('timeout2');
    }, 0);
}, 0);

/*timeout0
sync
nextTick1
nextTick3
nextTick2
resolved
timeout resolved
timeout2
*/

5.process.nextTick()导致程序饿死

const fs = require('fs');

function addNextTickRecurs(count) {
    let self = this;
    if (self.id === undefined) {
        self.id = 0;
    }

    if (self.id === count) return;

    process.nextTick(() => {
        console.log(`process.nextTick call ${++self.id}`);
        addNextTickRecurs.call(self, count);
    });
}

addNextTickRecurs(Infinity);
setTimeout(console.log.bind(console, 'omg! setTimeout was called'), 10);
setImmediate(console.log.bind(console, 'omg! setImmediate also was called'));
fs.readFile(__filename, () => {
    console.log('omg! file read complete callback was called!');
});

console.log('started');

/*

process.nextTick call 1
process.nextTick call 2
process.nextTick call 3
process.nextTick call 4
process.nextTick call 5
...
*/
  1. process.nextTick()的nextTickQueue在每个阶段执行完都会检查执行一次,并且在nextTick里增加的nextTick会直接添加到nextTickQueue队列里
setImmediate(() => console.log('this is set immediate 1'));
setImmediate(() => console.log('this is set immediate 2'));
setImmediate(() => console.log('this is set immediate 3'));

setTimeout(() => console.log('this is set timeout 1'), 0);
setTimeout(() => {
    console.log('this is set timeout 2');
    process.nextTick(() => console.log('this is process.nextTick added inside setTimeout'));
}, 0);
setTimeout(() => console.log('this is set timeout 3'), 0);
setTimeout(() => console.log('this is set timeout 4'), 0);
setTimeout(() => console.log('this is set timeout 5'), 0);

process.nextTick(() => console.log('this is process.nextTick 1'));
process.nextTick(() => {
    process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick'));
});
process.nextTick(() => console.log('this is process.nextTick 2'));
process.nextTick(() => console.log('this is process.nextTick 3'));
process.nextTick(() => console.log('this is process.nextTick 4'));

/*
this is process.nextTick 1
this is process.nextTick 2
this is process.nextTick 3
this is process.nextTick 4
this is the inner next tick inside next tick
this is set timeout 1
this is set timeout 2
this is process.nextTick added inside setTimeout
this is set timeout 3
this is set timeout 4
this is set timeout 5
this is set immediate 1
this is set immediate 2
this is set immediate 3
*/

7.process.nextTick和Promise的回调函数

原文解释

Promise.resolve().then(() => console.log('promise1 resolved'));
Promise.resolve().then(() => console.log('promise2 resolved'));
Promise.resolve().then(() => {
    console.log('promise3 resolved');
    process.nextTick(() => console.log('next tick inside promise resolve handler'));
});
Promise.resolve().then(() => console.log('promise4 resolved'));
Promise.resolve().then(() => console.log('promise5 resolved'));
setImmediate(() => console.log('set immediate1'));
setImmediate(() => console.log('set immediate2'));

process.nextTick(() => console.log('next tick1'));
process.nextTick(() => console.log('next tick2'));
process.nextTick(() => console.log('next tick3'));

setTimeout(() => console.log('set timeout'), 0);
setImmediate(() => console.log('set immediate3'));
setImmediate(() => console.log('set immediate4'));

/*
next tick1
next tick2
next tick3
promise1 resolved
promise2 resolved
promise3 resolved
promise4 resolved
promise5 resolved
next tick inside promise resolve handler
set timeout
set immediate1
set immediate2
set immediate3
set immediate4
*/
posted @ 2020-11-02 17:19  yatolk  阅读(421)  评论(0编辑  收藏  举报