JavaScrpit 基础系列之运行机制 Event Loop

385 阅读7分钟

JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。

进程与线程

参考文章链接: www.cnblogs.com/qianqiannia…

进程

计算机核心是CPU,承担所有计算任务;操作系统是计算机的管理者,负责任务的调度,资源的分配管理,统领整个计算机的硬件;应用程序是具有某种功能的程序,程序都是运行在操作系统之上。

进程是一个程序在一个数据集上的一次动态执行过程,是操作系统对资源分配和调度的一个独立单位,是应用程序运行的载体。

进程一般由程序,数据集合和进程控制块三部分组成。

特点:

  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的
  • 并发性:任何进程都可以同其他进行一起并发执行
  • 独立性:进程是系统进行资源分配和调度的一个独立单位
  • 结构性:进程由程序,数据和进程控制块三部分组成

线程

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。

一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。

一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

两者区别
  • 线程是程序执行的最小单位,进程是操作系统分配资源的最小单位。
  • 一个进程是有一个或者多个线程组成,线程是一个进程中的不同执行路线
  • 进程相互独立,但同一进程下的线程可以共享程序的内存空间及该进程的资源。某个进程内的线程在其他进程内不可见。
  • 调度和切换:线程上下文切换比进程上下文切换要快得多
关系示意图

image.png

image.png

为何不使用多进程而是使用多线程?

线程廉价,线程启动、退出比较快,对系统资源的冲击也比较小。而且线程彼此分享了大部分核心对象(File Handle)的拥有权

如果使用多重进程,但是不可预期,且测试困难

这篇文章也讲的挺好理解的: www.ruanyifeng.com/blog/2013/0…

为什么JavaScript是单线程?

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

任务队列

单线程的话,所有任务就需要排队,上一个任务执行完毕后再执行下一个任务。如果上一个任务耗时长,后面的任务就得等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是任务可以分为同步和异步任务。

同步任务指在主线程上排队执行的任务,上一个任务执行完成后才执行下一个任务。

异步任务指不进入主线程而进入“任务队列”中的任务。只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

运行机制:

  • 所有同步任务都在主线程上执行,形成一个执行栈(执行栈是一个存储函数调用的栈结构,遵循先进后出的原则)。
  • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面的第三步。

示意图:

image.png

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

console.log('1')
setTimeout(() => {
    console.log('2')
}, 500)
console.log('3')
// 执行结果:
// 1
// 3
// 2

console.log('1') 是同步任务进入主线程执行,打印1,
setTimeout() 是异步任务进入“任务队列”, 500ms后执行。
console.log('3') 是同步任务进入主线程执行,打印3。
主线程执行完成后,主线程从“任务队列”读取 setTimeout() 可以执行,打印2。
得出结果:1、3、2

事件和回调函数

"任务队列" 是一个事件的队列(也可以看出消息队列)。主线程读取“任务队列”就是读取里面的事件。

任务队列" 的事件包括IO设备事件、用户产生的事件。

只要指定过回调函数,这些事件发生时就会进入“任务队列”。

回调函数,就是那些会被主线程挂起来的代码。

异步任务必须要有回调函数,主线程执行异步任务就是执行对应的回调函数。

“任务队列” 先进先出的顺序。如果有定时器,主线程会先检查执行时间,到了规定时间后才返回主线程。

Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

image.png

主线程运行的时候,产生堆(heap)和栈(stack)。

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。

var req = new XMLHttpRequest();
req.open('GET', url);    
req.onload = function (){};    
req.onerror = function (){};    
req.send();

Ajax请求是一个异步任务,当前代码执行完后才获取读取“任务队列”中的事件。上面的代码等同:

var req = new XMLHttpRequest();
req.open('GET', url); 
req.send();
req.onload = function (){};    
req.onerror = function (){};

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。

宏任务与微任务

参考文章: www.cnblogs.com/wangziye/p/…

宏任务(macrotask )和微任务(microtask )表示异步任务的两种分类。

宏任务是由宿主发起的,而微任务由JavaScript自身发起。

image.png

宏任务和微任务之间的关系

image.png

标题宏任务微任务
谁发起宿主(浏览器、Node)js引擎
具体事件1. script (可以理解为外层同步代码) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js)1. Promise.then catch finally 2. MutaionObserver((html5新特性) 3. Object.observe(已废弃;Proxy 对象替代) 4. process.nextTick(Node.js)
谁先运行后运行先运行
会触发新一轮的tick吗不会

看下面的例子

console.log('1')

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

process.nextTick(function() {
    console.log('6')
})

new Promise(function(resolve) {
    console.log('7')
    resolve()
}).then(function() {
    console.log('8')
})

首先第一个宏任务进入主线程 console.log('1') 打印:1;
setTimeout() 会放到宏任务队列中去;
process.nextTick() 放到微任务队列中;
Promise,new Promise()直接打印:7,Promise.then加入到微任务队列;
第一轮宏任务执行完成,开始执行第一轮微任务;
process.nextTick() 打印:6;
Promise.then() 打印: 8;
第一轮微任务执行完成,开始第二轮宏任务;
setTimeout() 中 console.log('2') 打印出:2;
process.nextTick() 加入微任务队列;
Promise,new Promise()直接打印:4,Promise.then加入到微任务队列;
第二轮宏任务执行完成,开始第二轮微任务 process.nextTick() 打印:3;
Promise.then() 打印: 5。
打印出: 1、7、6、8、2、4、3、5。