阅读前言
js世界里的进程、线程、协程和事件循环
WebWorker与事件循环

随着时间的推移,知识的积累,自己的认知总是在一步步变化着,有些变得深刻、有些得到翻新、有些走向湮灭。

1 本文主要内容

2 实例与performance分析

function con1() {
  console.log('sync1')
}

function con2() {
  console.log('sync2')
}

function con3() {
  console.log('sync3')
}

con1()

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

const channel = new MessageChannel()
channel.port1.onmessage = () => {
  console.log('postMessage')
}

Promise.resolve().then(() => {
  console.log('promise')
  channel.port2.postMessage('')
  con2()
  setTimeout(() => {
    console.log('timer3')
  })
})

con3()

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

打开chrome的隐身模式再打开这个页面,利用performance去分析,这一步是避免chrome插件影响你的分析。

完整的performance

⚠️注意:电脑不同的性能绘制的performance可能不一样。

2.1 summary色块含义

我们需要了解浏览器渲染网页的大致过程

浏览器渲染网页的大致过程

从performance上可以看到,色块含义

此处引用从event loop规范探究javaScript异步及浏览器更新渲染时机

我们分析代码时一般看紫色render的时间,和黄色script的执行时间

以坐标轴来看,x代表执行时间,从左到右一段一段的,我把它理解为一个个的事件循环,主线程不断的在执行这个操作

y代表调用栈call stack,从上到下代表从一个执行环境到另一个执行环境

2.2 从调用栈的角度看事件队列

我还清楚的记得第一次看eventloop相关时,有这样的分类

严格来讲,UI rendering是否属于task有待验证,但它本身是由浏览器的另一个线程去处理的,并且与主线程互相阻塞。

执行环境分为全局执行环境、函数执行环境、eval

如果从调用栈的角度考虑它,结合我们上面的performance分析图可以这样理解

从上到下,从左到右,调用栈的顺序依次是

从中我们可以看到render是穿插进行的,唯一可以确定的是render是在microtasks执行完成后运行的,因为可以理解为在浏览器的js世界中,主线程外的其它线程的调用,都是需要task去处理的,那必然是在microtasks之后了。

2.3 总结

一个eventloop的顺序可以理解为这样

最后一个例子来加深理解

new Promise(resolve => {
    resolve(1);
    console.log(4)
    Promise.resolve(2).then(e => console.log(e))
    Promise.resolve(5).then(e => console.log(e))
}).then(v => console.log(v))

console.log(3)
// 4
// 3
// 2
// 5
// 1

值得阅读的文档


2018-10-16更新

在清楚了代码执行顺序后,我们可以这样理解,在不同的执行环境中会分别产生task和microtask

在同一个执行环境中产生的task会放到相同的tasks queen中,因为栈是后进先出,所以我们看到的task调用顺序也是这样的

tasks queen在浏览器中有很多,它们之间有一个默认执行顺序机制(浏览器厂商推出的游戏模式?视频模式?),比如Message的postMessage方法,浏览器会将它放入优先级最高的队列中,在microtasks执行完后第一时间就去执行

另外,在每个task执行完后,还会去做checkpoint检查,是否存在microtask,存在就会优先去执行。

microtasks queen同样也是如此,不同的执行环境下队列是不一样的,会按照调用栈的顺序依次执行这些队列

时机:在调用栈执行完成之后,或task执行完之后,就会去执行microtasks queen

所以,你讲task和microtask谁先谁后有意义吗?

3 node中事件循环模型

这里也只是对网络的文章查阅总结

在浏览器中,由浏览器本身去管理进程与线程,但在node中,只有v8解释器,需要node的底层模块来处理这些线程和事件模型,这个模块就是libuv

很重要的一点就是在node中,不存在taskmicrotask这个概念。

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

事件模型的各个阶段,起始由外部发起请求开始

3.1 nextTick 与 setImmediate

当事件进入下个阶段前,会检查next queen中是否为空,不为空则执行process.nextTick,也就是说不能循环递归去调用process.nextTick,因为这样会造成死循环。

setImmediate在check阶段执行

3.2 阶段详解

执行setTimeout、setInterval的回调

并不是所有I/O的回调都在这里执行,官网上举的例子是

For example if a TCP socket receives ECONNREFUSED when attempting to connect, some unix systems want to wait to report the error. This will be queued to execute in the I/O callbacks phase

意思是对于TCP socketECONNREFUSED错误,在unix系统中会被放到这个阶段去执行回调

而用户操作的有关I/O的回调其实都是EventEmitter的实例,也就是events事件。

这个阶段做2件事情

1、判断有没有timer 2、执行events回调,这里的回调事件就包括很多了,只要是EventEmitter的实例都会在此处执行

通常上来讲,随着代码的执行,eventloop最终会停留在poll阶段,等待新的事件。

这个阶段被专门用来执行setImmediate的回调,当poll阶段空闲且存在setImmediate时,会立刻进入check阶段

3.3 总结

poll阶段是处理绝大部分events回调的阶段,除了setImmediatetimer,此外,poll阶段的循环也是有限制的。

值得阅读的文档