浏览器和Node中的Event Loop

  • 为什么javascript是单线程的?

    • 因为javascript最初设计是运行在浏览器的,首先系统分配给浏览器的内存不会很多,其次javascript运行在浏览器的目的是为了与用户进行交互,假设javascript支持多线程,那么线程A要新增一个节点,线程B要删除一个节点,浏览器就不知道到底要干嘛了。

    • 随着CPU的发展,HTML5提出了web worker。允许web worker创建子线程,但是子线程得受控于主线程,且不能操作DOM。

  • Event loop是一个执行模型,在不同的地方有不同的实现。在浏览器中遵循的是HTML的标准,在node中是基于libuv库来实现的。

浏览器中的Event Loop

HTML标准这样定义事件循环:

为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。

有两类事件循环:一种针对浏览上下文(browsing context),还有一种针对worker(web worker)。

浏览器和Node中的Event Loop_第1张图片
图片来自演讲《Help,I`m stuck in an event loop》

在主线程执行的过程中会产生堆和栈。执行栈中的代码会依次去调用外部的API。这时候就会分同步代码和异步代码。同步方法就去执行栈执行,调用到异步方法的时候就会产生任务队列,异步方法的回调就去排队。执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行,在同步代码执行完了,空闲的时候才去看有没有异步任务要执行,如果这个时候任务队列正好有排队的任务,就去执行。

  • 任务队列分为macro-task和micro-task:

    • macro-task:setTimeout,setInterval,setImmediate,I/O,UI rendering

    • micro-task:process.nextTick,Promise,Object.observer,MutationObserver

  • 在执行任务队列中的方法的时候,会先去执行micro-task队列,再循环执行macro-task队列。

举个例子来说,在浏览器执行以下代码:

setTimeout(()=>{
 console.log(1);
})
console.log(2);
new Promise((resolve,reject)=>{
 console.log(3);
 resolve();
}).then(()=>{
 console.log(4)
});
console.log('end');

这样一段代码,执行结果是:2,3,end,4,1

分析一下:

1、在遇到setTimeout的时候,这个方法会去macro-task排队。

2、显而易见,同步方法,输出2。

3、输出3在resolve()之前,不用等待resolve()执行完,所以是同步的,输出3;输出4在then()中,需要等待执行,属于Promise,在micro-task排队。

4、显而易见,同步方法,输出end。

5、自此同步代码已经执行完了。

6、开始执行micro-task队列,所以输出4。

9、开始执行macro-task队列,所以输出1。

node中的Event Loop

nodejs的event loop分为6个阶段,每个阶段的作用如下:

  • timers:执行setTimeout()setInterval()中到期的callback。

  • I/O callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行

  • idle, prepare:仅内部使用(这个存疑,我也不是很懂内部在干嘛)

  • poll:最为重要的阶段,执行I/O callback,这个指的就是比如去读数据库啊,读文件之类的。在适当的条件下会阻塞在这个阶段(所谓适当的条件我要去查一查,现在不确定具体是什么样的条件叫适当)

  • check:执行setImmediate的callback

  • close callbacks:执行close事件的callback,例如socket.on("close",func)

当然还是需要注意的就是,在每一个阶段执行之前还是会先把micro-task里的任务执行掉。

              |-----------------micro tasks
   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
|             |             <----- micro tasks
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘ 
|             |            <----- micro tasks
|             |
|             |                   ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
|             |                   └───────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐      
│  │        check          │
│  └──────────┬────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

举个例子,在node下执行代码:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then((resolve,reject)=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then((resolve,reject)=>{
 console.log('promise2')
 })
})

根据node下的Event Loop执行顺序来看,输出结果应该是:

timer1,timer2,promise1,promise2

分析一下:

首先,micro-task是空的,执行timer阶段,执行setTimeout,输出timer1,timer2。promise1的then和promise2的then去micro-task排队。

然后,micro-task队列里有方法,输出promise1,promise2

再举个例子:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then(()=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then(()=>{
 console.log('promise2')
 })
})
Promise.resolve().then(()=>{
 console.log('promise3')
})

这段的输出是:promise3,timer1,timer2,promise1,promise2

分析一下:因为在执行timer阶段之前,会先把micro-task队列里的方法执行一下,所以promise3虽然写在后面了,也得先输出来。

再再举个栗子:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then(()=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then(()=>{
 console.log('promise2')
 })
})
Promise.resolve().then(()=>{
 console.log('promise3')
})
process.nextTick(()=>{
 console.log('nexttick')
})

这段的输出是:nexttick,promise3,timer1,timer2,promise1,promise2

分析一下:因为在micro-task队列里的优先级,process.nextTick一定比promise先执行

需要注意的是:是异步方法的回调在任务队列排队。

/////setTimeout和setImmediate的执行顺序有待商榷,主要是会涉及到代码执行的时间,和HTML规定setTimeout默认的4ms问题,不足4ms补足4ms

参考文档:

  • JavaScript 运行机制详解:再谈Event Loop

  • 不要混淆nodejs和浏览器中的eventloop

  • 理解node的EventLoop

  • 并发模型与事件循环

  • 什么是浏览器的事件循环

你可能感兴趣的:(浏览器和Node中的Event Loop)