JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
进程与线程
参考文章链接: www.cnblogs.com/qianqiannia…
进程
计算机核心是CPU,承担所有计算任务;操作系统是计算机的管理者,负责任务的调度,资源的分配管理,统领整个计算机的硬件;应用程序是具有某种功能的程序,程序都是运行在操作系统之上。
进程是一个程序在一个数据集上的一次动态执行过程,是操作系统对资源分配和调度的一个独立单位,是应用程序运行的载体。
进程一般由程序,数据集合和进程控制块三部分组成。
特点:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进行一起并发执行
- 独立性:进程是系统进行资源分配和调度的一个独立单位
- 结构性:进程由程序,数据和进程控制块三部分组成
线程
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。
一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。
两者区别
- 线程是程序执行的最小单位,进程是操作系统分配资源的最小单位。
- 一个进程是有一个或者多个线程组成,线程是一个进程中的不同执行路线
- 进程相互独立,但同一进程下的线程可以共享程序的内存空间及该进程的资源。某个进程内的线程在其他进程内不可见。
- 调度和切换:线程上下文切换比进程上下文切换要快得多
关系示意图
为何不使用多进程而是使用多线程?
线程廉价,线程启动、退出比较快,对系统资源的冲击也比较小。而且线程彼此分享了大部分核心对象(File Handle)的拥有权
如果使用多重进程,但是不可预期,且测试困难
这篇文章也讲的挺好理解的: www.ruanyifeng.com/blog/2013/0…
为什么JavaScript是单线程?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
任务队列
单线程的话,所有任务就需要排队,上一个任务执行完毕后再执行下一个任务。如果上一个任务耗时长,后面的任务就得等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是任务可以分为同步和异步任务。
同步任务指在主线程上排队执行的任务,上一个任务执行完成后才执行下一个任务。
异步任务指不进入主线程,而进入“任务队列”中的任务。只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
运行机制:
- 所有同步任务都在主线程上执行,形成一个执行栈(执行栈是一个存储函数调用的栈结构,遵循先进后出的原则)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
示意图:
只要主线程空了,就会去读取"任务队列",这就是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(事件循环)。
主线程运行的时候,产生堆(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自身发起。
宏任务和微任务之间的关系
| 标题 | 宏任务 | 微任务 |
|---|---|---|
| 谁发起 | 宿主(浏览器、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。