前言:
JS是单线程的,那么,它如何处理异步操作?
答:使用事件循环,执行过程如下:
-
所有同步任务都在主线程上执行,形成一个执行栈。
-
主线程之外,会存在一个任务队列,只要异步任务有了结果(如:setTimeout的等待时间到了),就在任务队列中放置一个事件(所以,也叫事件队列),进行排队(处于等待状态)。
-
当执行栈中的所有同步任务执行完后,就会读取任务队列(事件队列)中的任务(事件)。即:任务队列中的任务就结束了等待状态,进入执行栈。
-
主线程不断重复第三步。直到任务队列和执行栈里的代码执行完毕。
1、执行栈
注意,执行栈不是内存的栈区。要了解堆栈和内存的分区,请查看js的基本类型(值类型)和引用类型的区别。
在js中,当很多函数被依次调用的时候,因为js是单线程的,同一时间只能执行一个函数,怎么办?不能同时来,得排个队,排个一子队,按照顺序来,那这个一子队的顺序,即哪个函数在前,哪个函数在后,谁来记录呢?js为此专门开辟了一个内存区域,起名叫做执行栈。在执行栈里,保存着即将要执行的函数。就像大家在学校里排队打饭一样,只有一个人给你打饭时,你就老老实实地排队,一个打完饭了,下一个再打。
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个函数,那么js会向执行栈中添加这个函数的执行环境(当我们调用一个函数的时候,js会生成一个与这个函数对应的执行环境(context),又叫执行上下文。这个执行环境中保存着这个函数的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。),然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。
2、事件队列
以上说的是同步执行的情况,如果出现了异步(如发送ajax请求数据)执行,就需要用到事件队列,或者叫做任务队列(Task Queue)。
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中下面的任务。执行过程中,当这个异步事件被触发时(异步任务有了结果,如setTimeout的等待世间到了),把该事件加入一个队列里,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
即:执行栈存放所有要执行的函数(代码),事件队列中存放所有的异步任务,如果执行栈中的代码执行完毕后,才会把事件队列中的事件(所有异步的代码)放入执行栈,接着执行。
这也是为什么如下代码的执行,与你想象的不一样的原因所在。
这个代码
setTimeout(function(){
console.log(1);
}, 0)
console.log(2);
的执行结果是在后台分别打印出: 2 1
3、事件队列里分为:宏任务队列和微任务队列
1)、宏任务(macrotask )
宏任务一般包括: setTimeout,setInterval,I/O 操作(包括AJAX请求),即上面的示例中setTimeout是宏任务。
2)、微任务(microtask )
微任务一般包括:promise.then() 里的操作
4、当宏任务碰到了微任务时
那么问题来了,当执行栈里的代码执行完毕,去事件队列里取任务时,先去微任务队列里的任务还是先去宏任务里的任务呢?
答:先取微任务里的任务。因为,微任务耗时少,快。
5、示例:当宏任务碰到了微任务时
console.log(1); //主线程
setTimeout(function(){
console.log(2);//这个是宏任务,不管时间是多少(就算是0),都会进入到宏任务
},0);
new Promise(function(resolve,reject){
resolve();//这个是then的回调函数,这个会进入到微任务
})
.then(function(){
console.log(3);
});
console.log(4);//主线程
// 以上打印结果是:1,4,3,2
6、示例:当宏任务碰到了微任务时
多啰嗦一句,注意:在Promise的回调函数里的代码是同步,then里的回调才是异步的(resolve())
console.log(1); //主线程
setTimeout(function(){
console.log(2);//这个是宏任务,不管时间是多少(就算是0),也会进入到宏任务
},0);
new Promise(function(resolve,reject){
console.log(3); //Promise里的代码是同步的 (这行代码是新加的),所以,一开始在主线程执行
resolve();//这个是then的回调函数,这个是进入到微任务
})
.then(function(){
console.log(4);
});
console.log(5);
// 以上打印结果是:1,3,5,4,2
7、示例:Promise,async,await,setTimeout
如果不懂 async和await,请查看 什么是async,什么是await,async和await的区别,async和await的理解
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
</html>
<script>
console.log(0); //主线程
setTimeout(function(){
console.log(1);//宏任务
},1000);
setTimeout(function(){
console.log(2);//宏任务
},0);
new Promise(function(resolve,reject){
console.log(3);//主线程
resolve(); //微任务
}).then(()=>{
console.log(4);//微任务,这个then里的回调代码就是resolve
});
async function async1(){
console.log(5); //同步,主线程
let temp = await async2(); //async2函数里有promise
console.log(temp);
console.log(6);
return "async1";
}
async function async2(){
console.log(7); //同步
return "async2";
}
async1();
console.log(8);//主线程,
// 以上打印结果是:0,3,5,7,8,4,async2里的promise.then(), 6, async1里的promise.then(),2,1
</script>