1、基础知识
- 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务:不进入主线程、而进入"任务队列"(event queue)的任务,只有"任务队列"通知主线程某个异步任务可以执行了,该任务才会进入主线程执行。js中的异步任务有:setTimeout、setInterval、ajax、promise 等,需要注意的是promise本身是同步的,但其 .then()、.catch()、.finally() 却都是异步的。
- 所有同步任务都在主线程上执行,形成一个执行栈。js引擎存在monitoring process进程,会持续不断的检查执行栈是否为空,一旦为空,就会去 task queue(任务队列) 检查是否有等待被调用的函数。这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环机制)。
- setTimeout(fn,0)
setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。需要注意的是,setTimeout()只是将事件插入了"任务队列"的尾部,因此要等到同步任务和"任务队列"现有的事件都处理完,事件才会得到执行。但是同步任务或"任务队列"现有的事件有可能耗时很长,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
2、几个问题
2.1、JS为什么是单线程的?
- js作为浏览器脚本语言,它的主要用途是操作DOM,这也就决定js必须是单线程(避免几个线程同时操作同一个DOM);单线程就意味着,所有任务需要排队,前一个任务结束才能执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
- 为了利用多核CPU的计算能力,H5的Web Worker实现的“多线程”实际上指的是“多子线程”,完全受控于主线程,且不允许操作DOM;所以这个新标准并没有改变JavaScript单线程的本质。
2.2、JS为什么需要异步?
如果 js 中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。 对于用户而言,阻塞就以为着“卡死”,这样就导致了很差的用户体验。比如在进行ajax请求的时候如果没有返回数据后面的代码就没办法执行。
2.3、js 单线程如何实现的异步?
js 中的异步以及多线程都可以理解成为一种“假象”,就拿h5的WebWorker来说,子线程有诸多限制,不能控制DOM,不能修改全局对象等等,通常只用来做数据处理。JS异步的执行机制其实就是事件循环(event loop)。
3、Event Loop事件循环
- 宏任务(macro-task):整体代码script、setTimeout、setInterval
- 微任务(mincro-task):Promise.then(catch、finally)、process.nextTick()、async
微任务中 process.nextTick() 是 node.js 中的,作用是在下一次 Event Loop(主线程读取"任务队列")之前触发 process 指定的回调函数(本次 Event Loop 不会执行)。
①事件循环图
(1)、整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
(2)、同步任务会直接进入主线程依次执行;
(3)、异步任务会再分为宏任务(进入宏任务队列) 和 微任务(进入微任务队列)。
(4)、当主线程内的任务执行完毕(主线程为空时),会检查微任务的任务队列,如果有任务,就进入主线程全部执行,如果没有就从宏任务队列读取下一个宏任务执行;
(5)、每执行完一个宏任务就清空一次微任务队列,此过程会不断重复,这就是Event Loop;
4、代码示例
4.1 同步任务
console.log(1);
console.log(2);
console.log(3);
//执行结果:1、2、3 ,按顺序一步步执行
4.2、同步任务混合异步任务
console.log(1);
setTimeout(function() {
console.log(2);
},1000)
setTimeout(function() {
console.log(3);
},0)
console.log(4);
/*
执行结果:1、4、3、2
分析:
同步任务按照顺序一步一步执行,所以先输出1、4
异步任务:当执行到异步任务(如上的 setTimeout 宏任务)时,先将其加入 Event
Table(事件表格) 中,如果 Event Table 中的任务满足某种条件(如达到定时器的时间设定)
后就将其加入 Event Queue(任务队列) 中去(谁先满足条件谁先加入;队列特点:先入先出)。
当同步任务完成后,便从Event Queue 中读取事件到主线程执行,并按顺序输出3、2。
*/
for (var i = 1;i < 6;i ++) {
setTimeout(function fn() {
console.log(i)
},i * 1000)
}
/*
打印5个6,因为 for 循环作为同步任务先执行完毕,此时 i 为 6,再按顺序执行5个加入
Event Queue 的异步 setTimeout(),所以打印5个6。将var改为let或使用闭包,可以输出正
常值。
*/
4.3、微任务和宏任务练习1
console.log(1);
setTimeout(function() {
console.log(2)
},1000);
new Promise(function(resolve) {
console.log(3);
resolve();
}).then(function() {
console.log(4)
});
console.log(5);
/*
执行结果是:1、3、5、4、2
1、3、5是同步任务,所以先输出,.then()是微任务所以加入微任务队列,setTimeout()
是宏任务所以加入宏任务队列,主线程清空后先执行微任务在执行宏任务,所以输出4、2
*/
4.4、微任务与宏任务练习2
console.log(1);
setTimeout(function() {
console.log(2);
setTimeout(function() {
console.log(3);
new Promise(function(resolve) {
console.log(4);
resolve();
}).then(function() {
console.log(5);
});
}, 1000)
new Promise(function(resolve) {
console.log(6);
resolve();
}).then(function() {
console.log(7);
});
}, 1000)
setTimeout(function() {
console.log(8);
}, 1000)
console.log(9);
//执行结果:1、9、2、6、7、8、3、4、5
4.5、微任务和宏任务练习3 (这个比较绕 !可以用笔写一下 )
new Promise(resolve => {
console.log(1);
setTimeout(() => {//①
resolve(); //②
Promise.resolve().then(() => {//③
console.log(2);
setTimeout(() => console.log(3));//④
Promise.resolve().then(() => console.log(4));//⑤
});
});
Promise.resolve().then(() => console.log(5));//⑥
}).then(() => { //⑦
console.log(6);
Promise.resolve().then(() => console.log(7));//⑧
setTimeout(() => console.log(8));//⑨
});
console.log(9);
//执行结果:1、9、5、6、2、7、4、8、3
参考与:
JavaScript执行机制