1.进程与线程
进程是CPU资源分配的最小单位,而线程是CPU调度的最小单位
进程(工厂)
有单独的属于自己的工厂资源
线程(工人)
多个工人在一个工厂中协作工作,工厂与工人是1:n的关系。也就是说一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
工厂的空间是工人们共享的,这象征一个进程的内存空间是共享的,每个线程都可用这些共享内存
多个工厂之间独立存在
多进程:
在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。
例:浏览器每开一个Tab页,都是创建了一个进程
多线程:
程序中包含多个执行流,即在一个程序中可以运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务
例:一个进程中可以有多个线程,比如:渲染线程、JS引擎线程、HTTP请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
2.浏览器内核
简单来说浏览器内核是通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成
GUI渲染线程
JS引擎线程
定时触发器线程
事件触发线程
异步HTTP请求线程
GUI渲染线程
主要负责页面的渲染,解析HTM、CSS、构建DOM树、布局和绘制等。
当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,主线程才会去执行GUI渲染。
JS引擎线程
该线程主要负责处理JS脚本,执行代码
主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待JS引擎线程的执行
该线程与GUI渲染线程互斥,当JS引擎线程执行JS脚本时间过长,将导致页面渲染的阻塞
定时触发器线程
负责执行异步定时器一类的函数的线程,如:setTimeout、setInterval
主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。
事件触发线程
主要负责将准备好的事件交给JS引擎线程执行
比如setTimeout定时器计数结束、AJAX等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待JS引擎线程的执行
异步HTTP请求线程
负责执行异步请求一类的函数的线程,如:Promise、fetch、AJAX等
主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。
3.浏览器中的事件循环
宏任务和微任务
事件循环中的异步队列有两种:宏任务队列和微任务队列
一个宏任务队列和一个微任务队列
先执行微任务队列,微任务队列执行完毕后执行宏任务(每执行一次宏任务都需要把微任务队列清空)
常见的宏任务有:setTimeout、setInterval、requestAnimationFrame、script等
常见的微任务有:new Promise().then(回调)、MutationObserver等
事件循环流程
1.一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。微任务队列空,宏任务队列有且只有一个script脚本(整体代码)
2.全局上下文(script标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的宏任务与微任务,它们会分别被推入各自的任务队列里。同步代码执行完了,script脚本会被移出宏任务队列,这个过程本质上是队列的宏任务的执行和出队的过程
3.上一步我们出队的是一个宏任务,这一步我们处理的是微任务。但需要注意的是:当一个宏任务执行完毕后,会执行所有的微任务,也就是将整个微任务队列清空。
4.执行渲染操作,更新界面
5.检查是否存在Web worker任务,如果有,则对其进行处理
6.上述过程循环往复,查到两个队列都清空
总结:
当某个宏任务执行完毕后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推
4.Node.js中的事件循环
介绍
Node,js中的事件循环和浏览器中的是完全不相同的东西。
Node.js采用V8作为JS的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现
多个宏任务队列,多个微任务队列
Node.js的事件循环比浏览器端复杂很多。
Node.js的运行机制:
V8引擎解析JS脚本
解析后的代码,调用NodeAPI
libuv库负责NodeAPI的执行。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给V8引擎
V8引擎再将结果返回给用户
事件循环的6个阶段
外部输入数据 -> 轮询阶段(poll) -> 检查阶段(check) ->
关闭事件回调阶段(close callback) -> 定时器检测阶段(timer) ->
I/O事件回调阶段(I/O callback) -> 闲置阶段(idle、prepare)->
轮询阶段(poll) -> .......
poll阶段: 获取新的I/O事件,适当的条件下Node.js将阻塞在这里
check阶段: 执行setImmediate()的回调
close callbacks阶段: 执行socket的close事件回调
timers阶段: 这个阶段执行timer(setTimeout、setInterval)的回调
I/O callbacks阶段: 处理一些上一轮循环中的少数未执行的I/O回调
idle、prepare阶段: 仅Node.js内部使用
注意:上面六个阶段都不包括process.nextTick()
timer阶段
timers阶段会执行setTimeout和setInterval回调,并且是由poll阶段控制的。同样,在Node.js中定时器指定的时间也不是准确时间,只能是尽快执行
poll阶段
poll是一个至关重要的阶段,这一阶段中,系统会做两件事情:
回到timer阶段执行回调
执行I/O回调
并且在进入该阶段时如果没有设定了timer的话,会发生以下两件事情:
如果poll队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
如果poll队列为空时,会有两件事发生:
如果有settimmediate回调需要执行,poll阶段会停止并且进入到check阶段执行回调
如果没有settimmediate回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
当然设定了timer的话且poll队列为空,则会判断是否有timer超时,如果有的话会回到timer阶段执行回调
假设poll被堵塞,那么即使timer已经到时间了也只能等着,这也是为什么上面说定时器指定的时间并不是准确的时间
check阶段
settimmediate()回调会被加入check队列中,check阶段的执行顺序在poll阶段之后
setTimeout和setImmediate区别
二者非常相似,区别主要在于调用时机不同
setTimeout: 设计在poll阶段为空闲时,且设定时间到达后执行,但它在timer阶段执行
setImmediate:设计在poll阶段完成时执行,即check阶段
执行时间
进入事件循环也是需要成本的,如果在准备时候花费了大于1ms的时间,那么在timer阶段就会直接执行setTimeout回调。如果准备时间小于1ms,那么就是setImmediate回调先执行
但当二者在异步I/O callback内部调用时,总是先执行setImmediate,再执行setTimeout
process.nextTick
这个函数其实是独立于事件循环之外的,它有一个自己的队列。当每个阶段完成后,如果存在nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。
Promise.then
Promise.then也是独立于事件循环之外的,有一个自己的队列,但是优先级要比process.nextTick要低,所以当微任务中同时存在process.nextTick和Promise.then时,会优先执行前者。
Node.js于浏览器的事件队列的差异
浏览器环境下,就两个队列,一个宏任务队列,一个微任务队列。微任务的任务队列是每个宏任务执行完后执行
Node.js中,微任务会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行微任务队列的任务,每个任务队列的每个任务执行完毕之后,就会清空这个微任务队列
总结:
浏览器端:微任务在事件循环的宏任务执行完之后执行
Node.js端:微任务在事件循环的各个阶段之间执行