EventLoop 分享

什么是Event loop?

ECMAScript规范没有提到事件循环。相反,事件循环在HTML规范中有详细说明,尽管事件循环是在HTML规范中定义的,可是在其余环境中,好比Nodejs,也使用了它。javascript

HTML Standard中的定义

为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用本节所述的event loop。 Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation 事件,用户交互,脚本,渲染,网络这些都是咱们所熟悉的东西,他们都是由event loop协调的。触发一个click事件,进行一次ajax请求,背后都有event loop在运做。html

浏览器的event loop

An event loop has one or more task queues. A task queue is a set of tasks.vue

一个 event loop有一个或多个任务队列,一个任务队列是一个多任务的集合。java

Task queues are sets, not queues, because step one of the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.node

任务队列是集合,而不是队列,由于事件循环处理模型的第一步从选择的队列中获取第一个可运行的任务,而不是退出第一个任务。web

The microtask queue is not a task queue.ajax

微任务队列不是任务队列。segmentfault

在规范的Processing model定义了event loop的循环过程:api

  • 一个event loop只要存在,就会不断执行下边的步骤:
1.在tasks队列中选择最老的一个task,用户代理能够选择任何task队列,
若是没有可选的任务,则跳到下边的microtasks步骤。
2.设置oldestTask成为任务队列中第一个可运行的任务,并将其从任务队列中删除。
5.运行oldestTask里面的一系列脚本
7.从其任务队列中删除oldestTask
8.Microtasks: 执行microtasks任务检查点。(也就是执行microtasks队列里的任务)
11.更新渲染(Update the rendering)...

复制代码

event loop会不断循环上面的步骤,归纳说来:

  1. event loop会不断循环的去取tasks队列的中oldestTask(可执行的)一个任务推入栈中执行。
  2. 执行栈中的oldestTask一步一步执行完后,出栈。
  3. 当执行栈为空时,检查微任务(microtask checkpoint),不为空执行微任务
  4. 当微任务队列最终为空时,事件循环检查是否须要UI呈现更新,若是须要,则从新呈现UI。
  5. 这将结束事件循环的当前迭代,该循环将回到开始并再次检查宏任务队列。

图片出处: livebook.manning.com/book/secret…

microtask checkpoint

所作的就是执行microtask队列里的任务。何时会调用microtask checkpoint呢?promise

  • 每次回调以后,只要没有其余JavaScript处于执行中
  • 在event loop的第8 步(Microtasks: Perform a microtask checkpoint)执行checkpoint,也就是在每一个任务结束时,更新渲染以前。

演示

<div class="outer">
	<div class="inner"></div>
</div>
<script>

	// Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the
	// outer element
	new MutationObserver(function() {
		console.log('mutate');
	}).observe(outer, {
		attributes: true
	});

	// Here's a click listener… function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements
	inner.addEventListener('click', onClick);
	outer.addEventListener('click', onClick);

</script>
复制代码

点击打印结果

click
promise
mutate
click
promise
mutate
timeout
timeout
复制代码

此代码出处和解释> jakearchibald.com/2015/tasks-…

在node里面的解释

事件循环容许Node.js执行非阻塞的I/O操做(尽管JavaScript是单线程的),方法是尽量地将操做卸载到系统内核。

node 官网的 EventLoop

下图简要的概述了event loop的操做顺序:

注:每个框表明event loop中的一个阶段

每一个阶段都有一个FIFO(先进先出)的回调队列等待执行。虽然每一个阶段都有其独特之处,但整体而言,当event loop进入到指定阶段后,它会执行该阶段的任何操做,并执行对应的回调直到队列中没有可执行回调或者达到回调执行上限,然后event loop会进入下一阶段。

因为任何这些阶段的操做可能产生更多操做,内核也会将新的事件推入到poll阶段的队列中,因此新的poll事件被容许在处理poll事件时继续加入队,这也意味着长时间运行的回调能够容许poll阶段运行的时间比计时器的阈值要长

阶段概览

翻译:

  • timers:执行的是setTimeout()和setInterval()的回调
  • pending callbacks:执行延迟到下一个循环迭代的I/O回调
  • idle, prepare:仅内部使用
  • poll:检索新的I/O事件;执行与I/O相关的回调(除了close回调、计时器调度的回调和setimmediation()以外,几乎全部回调都执行);节点将在适当的时候在这里阻塞
  • check:setImmediate回调在这里触发
  • close callbacks:好比socket.on('close', ...)

Between each run of the event loop, Node.js checks if it is waiting for any asynchronous I/O or timers and shuts down cleanly if there are not any.

在每次运行事件循环之间,Node.js检查它是否在等待任何异步I/O或计时器,若是没有,则干净地关闭。

setImmediate() vs setTimeout()

两者的调用顺序取决于它们的执行上下文。若是二者都在主模块被调用,那么其回调被执行的时间点就取决于处理过程的性能(这可能被运行在同一台机器上的其余应用影响)

好比说,若是下列脚本不是在I/O循环中运行,这两种定时器运行的顺序是不必定的

// timeout_vs_immediate.js
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});
复制代码
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout
复制代码

可是若是你把上面的代码置于I/O循环中,setImmediate回调会被优先执行:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
复制代码
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout
复制代码

使用setImmediate()而不是setTimeout()的主要好处是:若是代码是在I/O循环中调用,那么setImmediate()老是优先于其余定时器(不管有多少定时器存在)

setTimeout 的时延

When delay is larger than 2147483647 or less than 1, the delay will be set to 1.

在node下运行

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

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

# output 能够看到1在前 0在后
1 2
0 4
复制代码

在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是4ms,这一般是因为函数嵌套致使(嵌套层级达到必定深度),或者是因为已经执行的setInterval的回调函数阻塞致使的。例如:

let i = 0
	let now = Date.now()

	function f(){
		let newD = Date.now()
		console.log(newD-now)
		now = newD

	}

	function cb() {
		f();
		i++
		if(i>10) return
	setTimeout(cb, 0);
	}

	setTimeout(cb, 0)
复制代码

从输出结果看也有小于4ms的,缘由就不知道了。

process.nextTick()

你可能已经注意到process.nextTick()不在上面的图表中,即便它也是异步api。这是由于严格意义上来讲process.nextTick()不属于event loop中的一部分,它会忽略event loop当前正在执行的阶段,而直接处理nextTickQueue中的内容。

回过头看一下图表,你在任何给定阶段调用process.nextTick(),在继续event loop以前,全部传入process.nextTick()的回调都会被执行。这可能会致使一些很差的状况,由于它容许你递归调用process.nextTick()从而使得event loop没法进入poll阶段,致使没法接收到新的 I/O事件

为何要使用process.nextTick()?

这里有两个主要的缘由

  1. 让开发者处理错误、清除无用的资源或者在event
  2. loop继续以前再次尝试从新请求资源 有时须要容许回调在调用栈展开以后但在事件循环继续以前运行

process.nextTick() 和 setImmediate()

如下截图来源:《深刻浅出node.js》

参考

Q&A

  1. 深刻浅出讲到的idle观察者 和 node讲到的idle阶段有什么关系?
  2. vue.nexttick 优先使用微任务promise实现的好处?
相关文章
相关标签/搜索