【Event Loop】浏览器事件循环 vs Node事件循环

JS两大特点:单线程和非阻塞

单线程: JS引擎是基于单线程(Single-threaded)事件循环的概念构建的。同一时刻只运行一个代码块在执行,与之相反的是像JAVA和C++等语言,它们允许多个不同的代码块同时执行。对于基于线程的软件而言,当多个代码块同时访问并改变状态时,程序很难维护并保证状态不出错。
非阻塞: 当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。非阻塞通过事件循环机制Event Loop实现。

Event Loop是什么?

Event Loop是一个实现主线程不阻塞的执行模型,在不同的环境有不同的实现。浏览器和Node.js基于不同的技术实现了各自的Event Loop。

  • 浏览器的Event Loop是在html5的规范中明确定义
  • Node.js 的Event Loop是基于libuv实现的,可以参考Node的官方文档以及libuv的官方文档。
  • libuv 已经对Event Loop做出了实现,而html5规范中只是定义了浏览器中Event
    Loop的模型,具体实现由浏览器厂商处理。

宏任务与微任务

宏任务macrotask,以下异步任务的回调会依次进入宏任务队列等待后续被调用:

  • setTimeout
  • setInterval
  • setImmediate(node独有)
  • requireAnimationFrame请求动画帧(浏览器独有)
  • UI rendering(浏览器独有)
  • I/O
    微任务microtask,以下异步任务的回调会依次进入微任务队列等待后续被调用:
  • Process.nextTick(node独有,同步任务执行完就会执行process.nextTick的任务队列,process.tick优于Promise.then)
  • Promise.then()
  • Object.observe
  • MutationObserve

浏览器端的Event Loop

主要包含三个部分:执行栈、事件循环、任务队列
在这里插入图片描述
图中调用栈中遇到DOM操作、ajax请求以及setTimeout等WebAPIs的时候就会交给浏览器内核的其他模块进行处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。等到这些模块处理完这些操作的时候将回调函数放入任务队列中,等执行栈中的task执行完之后再去执行任务队列之中的回调函数。

浏览器的Event Loop执行过程

  1. 执行全局Script的同步代码;
  2. 检查Microtask queues是否存在执行回调,有就执行microtask任务,直至全部执行完成,任务队列执行栈清空后进入下一步,例如peomise.then().then()两个微任务是依次执行的;
  3. 开始执行macrotask宏任务,Task Queues中按顺序取task执行,每执行完一个task都会检查Microtask队列是否为空(执行完一个Task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有Microtask,然后再进入下一个循环从Task Queue中取下一个Task执行,以此类推。
    具体实现请查看Philip Roberts的演讲中的一个例子《Help, I’m stuck in an event-loop》
	console.log('start')
	setTimeout(function cb(){
		console.log('定时器')
	}1000)
	console.log('end')

这段代码的执行:

  1. 首先main()函数的执行上下文入栈,执行这段Script整体代码
  2. log(‘start’) 执行入栈,打印start
  3. setTimeout(cb) 入栈,setTimeout是宏任务不直接执行,cb会交给浏览器的timer 模块进行延时1s,然后加入到任务队列中等待执行
  4. log(‘end’)执行入栈,打印end
  5. 这时候执行栈中的所有任务已经执行完了,执行引擎会检查回调队列中是否有需要执行的回调,有则依次执行回调。发现存在cb回调,执行log(‘定时器’)

Node.js中的Event Loop

Nodejs的Event Loop分为6个阶段,按照顺序反复运行,执行阶段分析:

  • timers
    定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。时间范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1ms。
  • I/O callbacks
    执行网络、流、tcp错误等callback。除了以下操作的回调函数,其他的回调函数都在这个阶段执行。
  1. setTimeout()和setInterval()的回调函数
  2. setImmediate()的回调函数
  3. 用于关闭请求的回调函数,比如socket.on(‘close’, …)
  • idle, prepare
    仅系统内部使用。
  • poll
    轮询阶段:检查定时器是否到时。用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等。这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
  • check检测
    -执行存放的setImmediate() 回调函数。
  • close callbacks关闭的回调函数
    执行close的回调函数,如:socket.on(‘close’, …)。
    在这里插入图片描述

Node.js 的Event Loop过程

  1. 执行全局Script的同步代码
  2. 执行microtask微任务,这里会先执行所有Next tick queue中的微任务,在执行Other Microtask Queue 中的所有任务
  3. 开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行,每个阶段必须执行完macrotask中的所有任务,再执行相应的微任务队列,然后在进入下一个阶段。
  4. 循环:Timers Queue所有宏任务 -> 步骤2 -> I/O Queue所有宏任务 -> 步骤2 -> Check Queue所有宏任务 -> 步骤2->Close Callback Queue所有宏任务 -> 步骤2 -> Timers Queue

浏览器的Timer 与Node的Timer

浏览器和node的Timer对0ms和1ms的延时效果是一致的,都是1ms。
浏览器的timer:

// https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp#93
double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);
这里interval就是传入的数值,可以看出传入0和传入1结果都是oneMillisecond,即1ms。

另外:HTML规范第11条提到Timer在嵌套层级超过5级时,不能少于4ms的延时限制。(为了给CPU留下休息时间)

  1. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

Node的timer:

// https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456
if (!(after >= 1 && after <= TIMEOUT_MAX))
after = 1; // schedule on next tick, follows browser behavior
代码中的注释直接说明了,设置最低1ms的行为是为了向浏览器行为看齐。

setTimeout(() => {
	console.log(2)
}, 2)

setTimeout(() => {
	console.log(1)
}, 1)

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

如代码设置的延时应该打印0,1,2,但是因为timer 的最低延迟限制,0ms和1ms都只能延时1ms,所有最终应该打印1,0,2
在chrome浏览器内运行如预期结果表现一致,但是在node环境下打印出的结果竟然无法确定。
经过多次试验发现:

  1. 在node中多个定时器顺序设置相差1ms的延迟(1ms, 2ms, 3ms;10ms, 11ms, 12ms),能保证执行顺序。
  2. 在node中多个定时器倒序设置相差1ms的延迟(3ms, 2ms, 1ms;12ms, 11ms, 10ms),将产生不确定的执行顺序。

实例分析

实例1:浏览器与Node的执行顺序的不同

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

分析:浏览器输出: time1 promise1 time2 promise2;Node输出: time1 time2 promise1 promise2

实例2:Node的阶段执行顺序

const fs = require('fs');
fs.readFile('demo.js', () => {
  console.log(0)
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
  console.log(3)
});

分析:fs读取文件处于node事件循环的I/O callback 阶段,回调中存在I/O callback阶段之前的timers阶段的setTimeout,所以setTimeout的回调将在下一个事件循环执行;然后继续执行check阶段的setImmediate回调,所以执行打印:0,3,2,1

实例3:Node的执行优先级

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

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

分析:这里检测到存在两个microTask(promise.then, process.nextTick),同步代码执行完毕就会执行process.nextTick的回调,然后执行promise.then的回调,所以优先打印4,3。这里setTimeout没有设置延时,进行最小延时1ms处理,当我们的系统进入事件循环小于1ms时,延时条件未满足,将跳过timer阶段,先执行setImmediate的回调打印2,在下一次循环在执行setTimeout的回调打印1;反之当系统进入事件循环大于1ms时,满足延时的条件执行setTimeout的回调打印1,在执行setImmediate的回调打印2;。所以结果:4,3,1,2或者4,3,2,1。

实例4:

      console.log('1')
      setTimeout(()=>{
          console.log('2');
          new Promise(resolve=>{
            console.log('3')
            resolve();
          }).then(()=>{
            console.log('4')
          })
      },0)
      new Promise(resolve=>{
            console.log('5')
            resolve();
          }).then(()=>{
            console.log('6')
          }) 
      setTimeout(()=>{
          console.log('7');
      },0)   
      setTimeout(()=>{
          console.log('8');
          new Promise(resolve=>{
            console.log('9')
            resolve();
          }).then(()=>{
            console.log('10')
          })
      },0)
      new Promise(resolve=>{
            console.log('11')
            resolve();
          }).then(()=>{
            console.log('12')
          })
      console.log('13');

分析:1 5 11 13 6 12 2 3 4 7 8 9 10

注意

在以往的Node版本中,也就是11.0 之前, JS的执行栈的顺序是
执行同类型的所有宏任务 -> 在间隙时间执行微任务 ->event loop 完毕执行下一个event loop
在最新版本的11.0之后, NodeJS为了向浏览器靠齐,对底部进行了修改,最新的执行栈顺序和浏览器的执行栈顺序已经是一样了
执行首个宏任务 -> 执行宏任务中的微任务 -> event loop执行完毕执行下一个eventloop

相关文章

Node 定时器详解
JavaScript 运行机制详解:再谈Event Loop
Event Loop的规范和实现
深入浅出Javascript事件循环机制(上)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值