Event Loop 总结

date
Nov 11, 2018
slug
event-loop-note
status
Published
tags
Javascript
summary
Javascript 中的事件循环,即 Event Loop
type
Post
之前也有 event loop 的概念,但一直都是琐碎的。直到阅读以下三篇文章后,琐碎的点串在了一起。

进程与线程的区别

什么是进程?

在计算机操作系统中,只有进程才能在系统中运行。所以要使程序运行就必须为其创建进程。当我们打开浏览器时就已经创建了进程。创建进程之后,系统会为其分配资源(内存等)供其使用。
进程是程序(指令和数据)的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。

什么是线程?

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总的来说进程是资源分配的最小单位,线程是计算机系统调度和分派的基本单位。线程基本上不拥有资源(也有一点必不可少的资源)它只专注于调度和分派,提高系统并发程度,但是它可以访问其隶属进程的资源。

浏览器是多进程

进入浏览器(chrome等)打开多个标签页,然后打开浏览器的 task manager。如下图:
notion image
从图上来看,我们首先可以确定的是我打开的4个页面都是进程,因为它们有不同的 process id(进程标识符),这个是存在 PCB 中的,系统分配且唯一的数字标识符。PCB 是进程控制块,具体可搜索计算机操作系统-进程。
浏览器的主要进程有四个,分别为:主进程、GPU进程、浏览器渲染(render)进程(每个标签页都会一个渲染进程)、第三方插件进程。

主进程

  • 用户的前进、后退操作
  • 每个标签页的创建与销毁
  • 将渲染进程执行后生成的位图(bitmap)绘制在对应标签页上,呈现给用户
  • 书签、下载等功能的管理

浏览器渲染进程

  • 页面渲染
  • 脚本执行(事件、task)

GPU进程:用于3D绘制等

第三方插件进程:使用插件时创建

 
主要来说下浏览器渲染进程,这个进程是当前标签的控制者。js 代码的执行、事件的触发、页面的展示等都由它负责。那么这一个进程可以同时干这么多事情吗?它一个当然是可以的,但是如果一个人来做就会浪费资源而且一旦执行脚本文件后会不断阻塞页面执行。至此,引入了线程来解决这个问题。

Javascript 的单线程

在渲染进程中划分了很多个线程,这里介绍一些常驻线程:

JS 线程(引擎线程,即常说的那个js单线程)

  • 运行环境(v8),负责处理 js 脚本
  • 每个标签页只有一个 js 线程在执行代码
  • js 线程与 Gui 渲染线程互斥,js 执行时间过长会导致页面渲染的阻塞(互斥原因下面阐述)

GUI 渲染线程

  • 负责渲染页面(构建dom 树 & cssparser 树 & renderobject 树)
  • 当页面出现重绘或回流(repaint/reflow)时,会调用该线程
  • 与 js 线程互斥

事件触发线程:遵循先进先出的原则在异步队列中等待 js 线程处理。(异步队列下面会解释)

定时器触发线程

  • 定时器并不是在某段时间后执行,而是在某时间后被添加至异步队列,等 js 线程空闲后执行
  • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms

http 请求线程:请求成功后会把回调放入异步队列中

备注:上述执行环境是在浏览器中,如果执行环境换为 node 的话,还有 I/O 线程与 setImmediate

为什么说 JS 线程与 GUI 渲染线程互斥(js 代码会阻塞渲染)?

GUI 渲染线程第一个构建的是 dom 树,js 代码也有可能会修改 dom 结构,一旦修改就会引起 重绘或回流,这就会导致 GUI 线程的再次从头开始。为了避免这样的事情发生所以采用了这种互斥的策略。俩者并不能在同一时间进行。

异步的实现方案 — Event Loop

针对于 js 引擎单线程这种情况,为了不让程序(被 http 请求、定时器等)阻塞,所以有了异步的概念。但是在现代 web 程序开发中,代码量越来越大。为了能让越来越多的定时器、事件、请求有序且规范的执行,event loop 方案出现了。
console.log('1');

setTimeout(function() {
  console.log('2');
}, 0);

console.log('3');

Promise.resolve().then(function() {
  console.log('4');
}).then(function() {
  console.log('5');
});

console.log('6');
上述代码的执行结果是:1 3 6 4 5 2
首先介绍 task & microtask:
  • task 主要包括:setTimeout、setInterval、setImmediate、I/O、UI交互事件
task & microtask 在当前标签页中拥有俩个队列来分别存储 task & microtask除了 js & gui 线程,其他每个线程执行时都会按照 task都有一个异步队列来控制顺序。如下图
notion image
Event Loop 过程如下:
  1. 整个script代码作为 task 执行时先判断是同步任务还是异步任务
  1. 同步任务依次进入 js 线程,依次执行
  1. 异步任务**先在 table 中注册**,然后**当任务完成时**按照 task/microtask 区分开来,依次进入不同的异步队列
  1. js 线程执行完毕后(执行栈为空)先读取 microtask 的异步队列,全部执行完毕(队列为空后),再去读取 task 的异步队列,**task 异步队列中的任务若包含 microtask 则再次去读取 microtask 异步队列**
  1. 上述过程不断重复,直至 task 异步队列为空。整个过程即为 Event Loop。
还是直接来看代码:
console.log('1');

setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
详细过程请参考:
Event Loop的规范和实现
作者简介:nekron 蚂蚁金服·数据体验技术团队 一直以来,我对 Event Loop的认知界定都是可知可不知的分级,因此仅仅保留浅显的概念,从未真正学习过,直到看了这篇文章-- 《这一次,彻底弄懂 JavaScript 执行机制》。该文作者写的非常友好,从最小的例子展开,让我获益匪浅,但最后的示例牵扯出了 chrome和 Node 下的运行结果迥异,我很好奇,我觉得有必要对这一块知识进行学习。 由于上述原因,本文诞生,原本我计划全文共分3部分来展开:规范、实现、应用。但遗憾的是由于自己的认知尚浅,在如何根据 Event Loop 的特性来设想应用场景时,实在没有什么产出,导致有关应用的篇幅过小,故不在标题中作体现了。 (本文所有代码运行环境仅包含Node v8.9.4以及 Chrome v63) 因为Javascript设计之初就是一门单线程语言,因此为了实现主线程的不阻塞, Event Loop 这样的方案应运而生。 先来看一段代码,打印结果会是? console.log(1) setTimeout(() => { console.log(2) }, 0) Promise.resolve().then(() => { console.log(3) }).then(() => { console.log(4) }) console.log(5) 不熟悉Event Loop的我尝试进行如下分析: 首先,我们先排除异步代码,先把同步执行的代码找出,可以知道先打印的一定是1、5 但是,setTimeout和Promise是否有优先级?还是看执行顺序? 还有,Promise的多级then之间是否会插入setTimeout? 带着困惑,我试着运行了一下代码,正确结果是: 1、5、3、4、2 。 那这到底是为什么呢?
Event Loop的规范和实现
输出:1 7 8 2 4 5 9 11 12
那如果执行环境切换为 node,node 的 Event loop的实现与浏览器稍有差异。具体如下:
notion image
需要注意的是:
  • expired timers and intervals queue 这个异步队列专门来存储所有的 setTimeout/setInterval
  • immediates queue 这个异步队列专门来存储所有的 setImmediate
  • next tick queue 专门来存储所有的 process.nextTick
  • node 清空 queue 时会执行所有 task(不论时 task 还是 microtask)
那么以下代码的结果呢?
console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
    process.nextTick(() => {
        console.log(3)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

process.nextTick(() => {
    console.log(6)
})

setTimeout(() => {
    console.log(9)
    process.nextTick(() => {
        console.log(10)
    })
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})
js 执行栈为空,microtask queue 为空时,去检查 task queue,发现俩个 settimeout 这时依次执行。这里与浏览器环境不同,浏览器环境是从 task queue 读出一个 task 先执行,执行完后去检查 microtask queue,而 node 中是一次性执行完 task queue,再去检查 micrtask queue(比如 process.nextTick、promise 等)
输出:1 7 6 8 2 4 9 11 3 10 5 12
分析过程请参考:
Event Loop的规范和实现
作者简介:nekron 蚂蚁金服·数据体验技术团队 一直以来,我对 Event Loop的认知界定都是可知可不知的分级,因此仅仅保留浅显的概念,从未真正学习过,直到看了这篇文章-- 《这一次,彻底弄懂 JavaScript 执行机制》。该文作者写的非常友好,从最小的例子展开,让我获益匪浅,但最后的示例牵扯出了 chrome和 Node 下的运行结果迥异,我很好奇,我觉得有必要对这一块知识进行学习。 由于上述原因,本文诞生,原本我计划全文共分3部分来展开:规范、实现、应用。但遗憾的是由于自己的认知尚浅,在如何根据 Event Loop 的特性来设想应用场景时,实在没有什么产出,导致有关应用的篇幅过小,故不在标题中作体现了。 (本文所有代码运行环境仅包含Node v8.9.4以及 Chrome v63) 因为Javascript设计之初就是一门单线程语言,因此为了实现主线程的不阻塞, Event Loop 这样的方案应运而生。 先来看一段代码,打印结果会是? console.log(1) setTimeout(() => { console.log(2) }, 0) Promise.resolve().then(() => { console.log(3) }).then(() => { console.log(4) }) console.log(5) 不熟悉Event Loop的我尝试进行如下分析: 首先,我们先排除异步代码,先把同步执行的代码找出,可以知道先打印的一定是1、5 但是,setTimeout和Promise是否有优先级?还是看执行顺序? 还有,Promise的多级then之间是否会插入setTimeout? 带着困惑,我试着运行了一下代码,正确结果是: 1、5、3、4、2 。 那这到底是为什么呢?
Event Loop的规范和实现

总结

  • js 是一门单线程语言
  • event loop 是 js 的执行机制

参考资料

 

© i7eo 2017 - 2022