据大家所知,js都是单线程执行的。那么就会接触到线程与进程,同步执行与异步执行,以及js单线程执行原理概念。
一、进程与线程的概念
1.1 进程
进程是CPU进行资源分配的基本单位,浏览器使用的是多进程,一个标签对应一个进程,此进程负责管理各个标签的创建与销毁,前进后退等操作。
1.2 线程
线程是CPU调度最小单位,多个线程可以对应到单一进程,而且可以它们可以共享进程的内存大小。
二、js单线程是如何实现同步与异步
2.1 js为什么是单线程
若js是多线程的话,同时执行会有线程冲突。
例如第一线程在DOM的node节点A对他进行新增内容,第二个线程则对此A节点进行了删除操作,那么最终是以哪个进程为主?
所以js是采用单线程,一次仅仅会执行一件同步任务。但不代表js引擎中只有一个线程,只是代表只有一个线程(通常称为主线程)是用来执行js代码,其它线程则是处理后台配合。
2.2 js执行同步任务
2.2.1 js执行同步任务的原理
js是单线程也就是说同时只能执行一个任务,而其它剩下的任务需要在后面等待。当前面的任务执行完了,才执行后面的任务。
2.2.2 js执行同步任务与栈的关系
js在执行同步任务时,所选用的数据结构是栈,保持先进后出,有序的概念。
例如执行一个任务函数A会调用函数B,那么函数B的栈帧变成当前帧,而函数A的栈帧会变成调用帧。当函数B执行完才回调到函数A继续把A执行完。
2.3 js执行异步任务
2.3.1 js执行异步任务的原理
当JS碰到异步执行的任务时,会先把它放到任务队列中,暂不处理此异步任务,而是继续往下执行同步任务,这样不会进行使JS单线程任务阻塞。
当js单线程空闲下来以及任务队列中的异步任务通知可以执行了,才会把此异步任务从任务队列中取出来,放到线程上执行。
2.3.2 什么是任务队列
任务队列是用来存放js单线程所遇到的异步任务。任务队列有多个。
2.3.3 js执行异步任务与队列的关系
我们知道任务队列是以队列的数据结构,保持先进先出,有序的概念。
也就是说,当js单线程的执行栈空了,就会从任务队列按顺序取出来依次执行(要是任务队列都通知队列中异步任务都能执行的情况下)
2.3.4 任务队列是如何运用
进行回调函数的形式通知执行栈可以执行。
基本以下情况会通过Web APIs按照一定的规则,再对上面的任务进行划分宏事件放入宏队列中,微事件放如微队列中(此概念会在2.3.5进行叙述),例如:
- DOM Binding模块处理一些onclick的函数
- network模块处理ajax请求
- 使用计时器setTimeout
- 使用promise
为了能够实时检查到任务队列中是否能够回调用到主线程中运行,使用的是事件循环Event Loop。
就是创建一个类似while(true)的循环过程,循环过程成为Tick。JS的单线程不停的一遍一遍循环,查看任务队列中是否有异步任务要执行,如果有则取出该相关事件,并通过回调函数放入单线程的执行栈中。
2.3.5 任务队列的宏任务,微任务
2.3.5.1 什么是宏任务,微任务
- 宏任务:js中全局的同步代码,setTimeout管理模块,AJAX请求管理模块,Dom事件管理模块等
- 微任务:Promise管理模块,mutation管理模块
2.3.5.2 宏任务与微任务的执行原理
在同步任务都执行完,执行栈为空的基础下(同步任务相当于第一个宏任务执行完),每当宏任务执行完之后,会先查看是否微队列中有任务。有则全部执行完再执行第二个宏任务;没有则直接执行第二个宏任务。以此类推。
2.3.5.3 宏任务与微任务的执行代码理解以及动图
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
具体代码执行栈以及对应任务队列操作如下:
在上述代码以及动图中可以看出,从上往下顺序执行。
我们先分成同步任务与异步任务两部分:
一、同步执行
1.遇到console.log(‘start’)属于同步任务,直接执行。
2.遇到setTimeout代码块属于异步任务中的宏任务,把它放入任务队列中的宏队列macrotask挂起。
3.遇到Promise代码块属于异步任务中的微任务,把它放入任务队列中的微队列microtask中挂起。
4.遇到console.log(‘end’)属于同步任务,直接执行,至此同步任务已全部执行完成。
二、异步执行
1.同步任务完成之后,全局代码属于macrotask宏任务,此宏任务执行完之后就开始检查任务队列中的微队列是否有执行。
2.把微任务的Promise代码块回调,放入执行栈中执行,执行其中的console.log(‘promise1’)。
3.微任务的Promise代码块回调函数返回undefined。接着,primose的状态会从pending转换成fulfilled,继续放入microtask微队列之中。
4.进入event loop事件循环,此时执行.then()回调执行console.log(‘promise2’),至此异步任务的微任务队列完成。
5.再次进入event loop事件循环,就执行异步任务中的宏任务(此时微任务已没有需要执行,所以直接执行下一个宏任务)。执行setTimeout(),延时0之后,console.log(‘setTimeout’)。
6.至此任务队列全部执行完成,event loop还在循环等待新一轮需要处理的代码。
2.3.6 异步任务中的async-await
线程的执行顺序同时还会牵扯到async-await,promise,setTimeOut影响。现在主要重点关注async-await。
2.3.6.1 async-await是什么?
async是个函数且返回一个Promise对象,和await搭配使用。当在async函数中碰到await时,await就像是让出线程的操作,右边内容完成时让出线程,并把async剩下的代码挂起,等待剩下同步任务完成后才执行。
2.3.6.1 async-await,promise,setTimeOut的代码运行
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
运行后代码打印为
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
可以看出function async2()返回是一个Promise,await async2()相当function async2()的,then()执行成功返回会的值。此时awiat会让出线程,跳出function async1()的异步执行,继续执行下面的同步任务,等同步任务全部执行完毕,再执行function async1()剩下的部分。