宏任务:
当前调用栈执行的代码成为宏任务,(主代码块和定时器)也或者宿主环境提供的叫宏任务
这些任务包括:
- 渲染事件
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
- JavaScript 脚本执行事件;
- 网络请求完成、文件读写完成事件
微任务:
当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件:promise.then,proness.nextTick等等。 由语言标准提供的叫微任务.
执行顺序:
在挂起任务的时候, JS 引擎会把任务按照类别分到两个队伍当中。 首先在宏任务(macrotask) 队伍中取出第一个任务,执行完毕后。取出 microtask 队列中的所有任务顺序执行;周而复始,循环。
总结上面:
可以举个例子:就像银行柜台办理业务,每个来办理业务的人就像一个一个宏任务,当前用户业务办理完成然后 接待下一个客户,就像开始了下一个宏任务一样。但是一个客户可能要办理多项业务(修改密码,存款,转账等)这些业务就像微任务,只有微任务执行完成,才能执行下一个宏任务(总不能一个客户业务没有办理完,就让他去重新取号排队,估计要打人了!!!)
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
//马上执行for循环啦
//代码执行结束
//执行then函数啦
//定时器开始啦
- 这段代码作为宏任务,进入主线程。
- 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
- 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
- 遇到console.log(),立即执行。
- 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
- ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
- 结束
看下demo:
Promise 在前 setTimeout在后面
new Promise((resolve) => {
console.log('外层宏事件2');
resolve()
}).then(() => {
console.log('微事件1');
}).then(()=>{
console.log('微事件2')
})
console.log('外层宏事件1');
setTimeout(() => {
//执行后 回调一个宏事件
console.log('内层宏事件3')
}, 0)
// 执行结果:
外层宏事件2
外层宏事件1
微事件1
微事件2
内层宏事件3
- Promise 在前,Promise.then则是具有代表性的微任务,所有会进入的异步都是指事件的回调。所以说 new Promise 在实例化的过程中所执行的代码都是同步进行的,而then 中的才是异步执行的
- setTimeout就是作为宏任务来存在的
- 在同步执行完成之后,检查是否有异步任务,微任务会在下一个宏任务前面全部完成
- 所以 结果是 外2-外1-微1-微2-内3
setTimeout 在前面 Promise 在后面
setTimeout(() => {
//执行后 回调一个宏事件
console.log('内层宏事件3')
}, 0)
console.log('外层宏事件1');
new Promise((resolve) => {
console.log('外层宏事件2');
resolve()
}).then(() => {
console.log('微事件1');
}).then(()=>{
console.log('微事件2')
})
// 执行结果:
外层宏事件1
外层宏事件2
微事件1
微事件2
内层宏事件3
- setTimeout 设定了时间,相当于取号了,在排队过程。
- 然后在当前进程中又添加了一些Promise的处理(临时添加的业务)
- 同步的外1 和外2 执行完成,开始执行微任务,微任务执行完成之后才执行下一个异步宏任务,所以结果如上;
前面提到宿主环境:能够使js 完美运行的环境,目前常见的环境就是两种宿主环境有浏览器和node
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')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 1-7 -6 -8 -2- 4- 3- 5- 9 -11- 10 -12
- 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
- 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
- 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
- 遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
- 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
- 同步输出1-7 执行微任务 process1-then1 输出6 - 8
- 好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:
- 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
- 第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。 输出 3和5
- 第二轮事件循环结束,第二轮输出2,4,3,5。
- 第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 输出9,11,10,12。
node 环境和浏览器环境 又有什么区别呢?
-
宏任务
requestAnimationFrame
姑且也算是宏任务吧,requestAnimationFrame
在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行微任务
JS 是单线程,所以同一个时间不能处理多个任务,所以每次办理完一个业务,都会询问当前客户是否还有其他要办理的业务(检查是否有未完成的微任务),当前用户办理完成,结束这个宏任务开始下一个宏任务,这样操作持续进行,而这样的操作就被称为Event Loop
什么是Event Loop?
event loop 顾名思义 就是事件循环。因为V8是单线程的,即同一时间只能干一件事情,但是呢文件的读取,网络的IO处理是很缓慢的,并且是不确定的,如果同步等待它们响应,那么用户就起飞了。于是我们就把这个事件加入到一个 事件队列里(task),等到事件完成时,event loop再执行另一个事件队列。
1、 update_time
在事件循环的开头,这一步的作用实际上是为了获取一下系统时间
2、timers
事件循环跑到这个阶段的时候,要检查是否有到期的timer,其实也就是setTimeout和setInterval这种类型的timer,到期了,就会执行他们的回调。
3、I/O callbacks
处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都结束的时候,在这个阶段会触发它们的回调。
4、idle, prepare
这个阶段内部做一些动作,与理解事件循环没啥关系
5、I/O poll阶段
这个阶段相当有意思,也是事件循环设计的一个有趣的点。这个阶段是选择运行的。选择运行的意思就是不一定会运行。
6、check
执行setImmediate操作
7、close callbacks
关闭I/O的动作,比如文件描述符的关闭,链接断开
除了task还有一个microtask,这一个概念是ES6提出Promise以后出现的。这个microtask queue只有一个。
并且会在且一定会在每一个task后执行,且执行是按顺序的。加入到microtask 的事件类型有Promise.resolve().then(), process.nextTick() 值得注意的是
event loop一定会在执行完micrtask以后才会寻找新的 可执行的task队列。而microtask事件内部又可以产生新的microtask
浏览器:
- 宏任务(macroTask):script 中代码、setTimeout、setInterval、I/O、UI render
- 微任务(microTask): Promise、Object.observe、MutationObserver。
- I/O 有点笼统,点击个btn 上传一个文件,与程序交互的这些都可以称为I/O
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')
function handler () {
console.log('click') // 直接输出
Promise.resolve().then(_ => console.log('promise')) // 注册微任务
setTimeout(_ => console.log('timeout')) // 注册宏任务
requestAnimationFrame(_ => console.log('animationFrame')) // 注册宏任务
$outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务
}
new MutationObserver(_ => {
console.log('observer')
}).observe($outer, {
attributes: true
})
$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)
1、因为一次I/O创建了一个宏任务,也就是说在这次任务中会去触发handler
2、在同步的代码已经执行完以后,这时就会去查看是否有微任务可以执行,然后发现了Promise和MutationObserver两个微任务,遂执行之
3、click事件会冒泡,所以对应的这次I/O会触发两次handler函数(一次在inner、一次在outer),所以会优先执行冒泡的事件(早于其他的宏任务),也就是说会重复上述的逻辑
4、在执行完同步代码与微任务以后,这时继续向后查找有木有宏任务
5、因为我们触发了setAttribute,实际上修改了DOM的属性,这会导致页面的重绘,而这个set的操作是同步执行的,也就是说requestAnimationFrame的回调会早于setTimeout所执行
所以上面 执行顺序是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout
node:
- Node也是单线程,但是在处理Event Loop上与浏览器稍微有些不同
setImmediate与setTimeout的区别:
在官方文档中的定义,setImmediate为一次Event Loop执行完毕后调用。
setTimeout则是通过计算一个延迟时间后进行执行。
microTask:微任务;
nextTick:process.nextTick;
timers:执行满足条件的 setTimeout 、setInterval 回调;
I/O callbacks:是否有已完成的 I/O 操作的回调函数,来自上一轮的 poll 残留;
poll:等待还没完成的 I/O 事件,会因 timers 和超时时间等结束等待;
check:执行 setImmediate 的回调;
close callbacks:关闭所有的 closing handles ,一些 onclose 事件;
idle/prepare 等等:可忽略。
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
进程和线程的区别:
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多
- 进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
- 内存分配方面:
系统在运行的时候会为每个进程分配不同的内存空间;
而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。 - 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
- 创建一个线程比进程开销小;
- 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
浏览器都有哪些进程?
1.Browser进程(即上篇文章截图里面的浏览器进程):浏览器的主进程(负责协调、主控),只有一个。主要作用:
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将渲染(Renderer)进程得到的内存中的Bitmap(位图),绘制到用户界面上
- 网络资源的管理,下载等
2、第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
3、GPU进程:最多一个,用于3D绘制等
4、浏览器渲染进程(即通常所说的浏览器内核)(Renderer进程,内部是多线程的):主要作用为页面渲染,脚本执行,事件处理等
浏览器内核
简单来说浏览器内核是通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。从上面我们可以知道,Chrome浏览器为每个tab页面单独启用进程,因此每个tab网页都有由其独立的渲染引擎实例
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成
- GUI 渲染线程
GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了.
- JavaScript引擎线程
Javascript引擎,也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。Javascript引擎线程理所当然是负责解析Javascript脚本
- 定时触发器线程
- 事件触发线程
- 异步http请求线程