问:JS 是单线程的。那么,异步怎么处理。也就是说,如何用单线程解决“多线程的场景”。
答:用事件轮训机制,也叫事件循环(event loop)。
事件轮询 (eventloop) 是"一个解决和处理外部事件时将它们转换为回调函数的调用的实体(entity)"
JS事件轮询
1、js 是一门单线程语言,从上往下执行的,首先,主线程读取js代码,形成一个执行栈,此时是同步的环境。
2、当主线程检测到异步操作,就会交给其他异步线程处理,然后继续执行主线程的的任务。
3、异步任务执行完毕之后,判断异步任务的类型,异步任务可分成宏任务和微任务,像setTimeout、setInterval属于宏任务,promise.then属于微任务,不同的任务进入不同的队列,等待主线程空闲时候调用。
4、当主线程的的同步任务执行完毕之后,开始执行微任务队列里面的所有微任务,执行完微任务,就执行宏任务队列里面所有的宏任务。
5、执行完成之后,主线程开始询问任务队列里面是否还有等待的任务,如果有则进入主线程继续执行。
1.执行栈
在js中,当很多函数被依次调用的时候,因为js是单线程的,同一时间只能执行一个函数,怎么办?不能同时来,得排个队,排个一子队,按照顺序来,那这个一子队的顺序,即哪个函数在前,哪个函数在后,谁来记录呢?js为此专门开辟了一个内存区域,起名叫做执行栈。在执行栈里,保存着即将要执行的函数。
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个函数,那么js会向执行栈中添加这个函数的执行环境(当我们调用一个函数的时候,js会生成一个与这个函数对应的执行环境(context),又叫执行上下文。这个执行环境中保存着这个函数的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。),然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。
2.任务队列
以上说的是同步执行的情况,如果出现了异步(如发送ajax请求数据)执行,就需要用到事件队列,或者叫做任务队列(Task Queue)。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务指的是,不进入主线程、而进入 " 任务队列 "(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
“ 任务队列 " 是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空," 任务队列 " 上第一位的事件就会自动进入主线程。
3.宏观任务队列和微观任务队列
1)宏观任务队列
宏任务:(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
包括:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)。
2)微观任务队列
微任务:microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
包括:Promise.then、Object.observe、MutationObserver、process.nextTick(Node.js 环境)。
注意:当宏任务碰到了微任务,先取微任务里的任务。因为,微任务耗时少,快。
一些示例
1.当只有宏任务
console.log(1);
// // 碰到setTimeout代码,会先把异步任务交给web api(计时,并把任务扔到任务队列)
setTimeout(function(){
console.log(2);
},1000)
// 碰到setTimeout代码,会先把异步任务交给web api(计时,并把任务扔到任务队列)
setTimeout(function(){
console.log(3);
},500)
console.log(4);
// 以上代码的执行顺序是:1,4,3,2
2.当只有微任务
console.log(1);
(new Promise(function(resolve,reject){
console.log(2);//同步
resolve();//也是同步,但是调用then里的方法时,会把then里的方法扔给webapi,webapi再扔给微任务队列。
})).then(function(){
console.log(3);
})
console.log(4);
// 以上代码打印结果: 1,2,4,3
3.当宏任务碰到了微任务时
console.log(1);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve,reject){
console.log(3);
resolve();
}).then(function(){
console.log(4);
});
console.log(5);
// 以上代码打印结果: 1,3,5,4,2
4.当宏任务碰到多个微任务
//当微任务里的所有任务都执行完毕时,才执行宏任务里的任务
console.log(1);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve,reject){
console.log(3);
resolve();
}).then(function(){
console.log(4);
});
Promise.resolve(5).then(function(n){
console.log(n);
})
console.log(6);
// 以上代码打印结果: 1,3,6,4,5,2
当宏任务碰到了多个微任务时。
宏任务队列和微任务队列执行思路和轮询机制不一样:
1、微任务队列执行时,会把所有的任务全部执行完毕,才会轮询宏任务
2、宏任务队列执行时:当执行完一个任务后,就会去轮询微任务队列。