我们知道JS是单线程脚步语言,设计为单线程是有好处的,如果为多线程,当两个人同时操作了同一个资源,这样会造成同步问题,不知以谁为准。
同时,单线程也存在一些问题,比如:
for(var i = 0;i<1000;i++){
console.log(1)
}
console.log(2)
结果就是,2将会等待1全部输出完毕后在执行,浪费了大量的时间
我们希望在等待的时间去做别的事,所以,js诞生了异步。
js 的异步有很多,像事件绑定、ajax请求,promise、回调、订阅监听、async等等。
异步与同步不同,当JS解析执行时,会被js引擎分为两类任务,同步任务(synchronous) 和 异步任务(asynchronous)。
对于同步任务来说,会被推到执行栈按顺序去执行这些任务。
对于异步任务来说,当其可以被执行时,会被放到一个 任务队列(task queue) 里等待JS引擎去执行。
当执行栈中的所有同步任务完成后,JS引擎才会去任务队列里查看是否有任务存在,并将任务放到执行栈中去执行,执行完了又会去任务队列里查看是否有已经可以执行的任务。这种循环检查的机制,就叫做事件循环(Event Loop)。
对于任务队列,其实是有更细的分类。其被分为 微任务(microtask)队列 & 宏任务(macrotask)队列
宏任务: 整体js代码,setTimeout、setInterval等,会被放在宏任务(macrotask)队列。
微任务: Promise的then、Mutation Observer等,会被放在微任务(microtask)队列。
当js开始执行的时候,先执行主执行栈的代码,遇到异步任务,将任务放入异步任务队列里(微任务和宏任务分别放入各自的任务队列),
然后开始执行异步的任务,先执行微任务,后执行宏任务。
如下图所示
下图为一次Eventloop(事件循环)
下面一个简单的例子,看一下输出什么
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
分析:
从上到下开始执行,主线程宏任务 输出 " script start" ,
遇到settimeout,放入宏任务队列Event Queue H["timeout1"],继续,
new promise,直接打印"promise1" ,["script start","promise1"],
然后promise.then,放入微任务队列Event Queue W["then1"]
settimeout,放入宏任务队列 H["timeout1","timeout2"],
然后console,直接打印"script end",["script start","promise1","script end"]
现在主栈执行完毕,开始执行异步,先执行微任务队列,结果["script start","promise1","script end","then1"],
至此,该次事件循环已经结束,开启下一次事件循环,从宏任务队列开始打印"timeout1",继续查找微任务队列,没有微任务,结束第二次事件循环
开启第三次执行宏任务"timeout2",结果就是["script start","promise1","script end","then1","timeout1","timeout2"]
tips:setimeout 定时w3c标准最小为4ms,即使设置时间为0,系统会默认为4ms
然后看一个复杂的例子,先自己分析一下输出什么,结果最后我会分析。
setTimeout(function() {
console.log('timeout1');
},200)
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
async1();
new Promise(function (resolve) {
console.log('111');
resolve();
new Promise(function(resolve){
console.log('222')
setTimeout(function(){
console.log('333')
})
resolve()
}).then(function(){
console.log('444')
setTimeout(function(){
console.log('555')
})
})
}).then(function (resolve) {
console.log('666')
setTimeout(function(){
console.log('777')
},200)
});
setTimeout(function(){
console.log('timeout2')
},100)
结果分析
开始,从上到下,遇到settimeout,放入宏任务H["timeout1(200)"],注意时间是200ms,
然后遇到两个async函数,执行async1(),主栈输出["async1 start"],继续,执行await async2,注意这里,我们要分析await,
实际上async是promise的语法糖,我们要将其转换为promise,async会返回一个隐式的promise [async function MDN的解释](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function)
将async1和async2转换如下,这样就容易理解了
function async1(){
Promise.resolve(console.log('async1 start'))
Promise.resolve(console.log('async2'))
}
function async2(){
Promise.resolve(console.log('async2'))
}
现在执行async2 Promise.resolve(console.log('async2')),主栈输出["async1 start","async2"],
将 console.log('async1 end')
放入微任务栈, 这里用P1代替["P1"],继续,输出“111”,主栈["async1 start","async2","111"],然后resolve,
将 console.log('666')
setTimeout(function(){
console.log('777')
},200),
放入微任务,用P2表示,W["P1","P2"]
我们发现在promise内部又有一个promise,继续执行,主栈["async1 start","async2","111","222"],
宏任务“333”,时间为0,放入第一位,H["333","timeout1(200)",],继续resolve,
将console.log('444')
setTimeout(function(){
console.log('555')
})
放入微任务,用P3表示,因为在函数内部,先执行 W["P1","P3","P2"],
继续,遇到宏任务timeout2,时间为100ms,所以排第二H["333","timeout2(100)","timeout1(200)"]
OK ,主任务结束,目前,主栈["async1 start","async2","111","222"],微任务W["P1","P3","P2"],
宏任务H["333","timeout2(100)","timeout1(200)","777"],接下来执行微任务P1,
输出"async1 end",
目前W["P3","P2"],执行P3,P2,目前微任务W["444","666"],
宏任务H["333","555","timeout2(100)","timeout1(200)"],目前为止,所有任务队列已经理清楚,先执行微任务队列,
结束事件循环,继续宏任务的新的事件循环,注意:每一个宏任务都是一次新的事件循环
最终结果:
["async1 start","async2","111","222","async1 end","444","666","333","555","timeout2",
"timeout1","777"]
相信这应该是最详细的式例分析了,这篇文章花了我两天的时间,研究的同时,也发现了自己的很多不足。
异步的方式有很多,例子中仅仅使用了三种,实际上,js的异步是浏览器开起的不同线程决定的。比如事件,http,定时器线程等等,我将会在下一篇解释浏览器多线程的事情。