异步
异步编程的核心: 现在 与 稍后 的部分之间的关系。
例子: 去吃饭排队,先拿一个号,排到了手机会有消息收到。在这个时候,可以取买点喝的。
现在:排队拿号 -> 买水 。
稍后:短信通知排到了,开吃!
event loop 事件轮询
我们将事件轮询理解成一个永动机,它一直会去找某一个队列【就可以理解成一个数组】里面的是否有执行的任务,如果有,那么会去执行。
模拟代码:
var eventLoop = [];
var event;
// 永远执行
while (true) {
// 检查队列是否有数据
// 执行一个tick
if (eventLoop.length > 0) {
event = eventLoop.shift();
try {
event();
}
catch(err) {
reportError(err); // report Error stack info
}
}
}
js引擎 遇到异步代码块,会将它放到【不一定是马上】任务队列里面,当主线程执行完所有代码的时候,会从这个任务队列里面取任务,如果有就执行。
setTimeout 不会将回调放在事件轮询队列上,定时器超时的时候,环境才会把你的回调放进事件轮询,这样在未来的tick中将会被取出执行。
虽说js是单线程,但是在事件轮询 和 并发模型机制上,并非是单线程。 除了主线程callstack , 还有一个专门处理回调函数或者事件的线程(在浏览器里面就是webAPI,node里面就是线程 )。外面有一个task queue,这里就是回调等待的地方。
比如遇到了A , A 有一个回调,A函数执行了,A的回调放到了 另外的线程,另外的线程在满足一定条件(可能是setTimeout 或者response响应)之后就会将这个回调放到 task queue 里面, callstack [必须]clear之后会马上去task queue 里面一个一个执行这个队列的代码,执行完一个,task queue的这个就回收。
cool video: https://www.youtube.com/watch?v=8aGhZQkoFbQ
[大白话]: 主线程的代码执行完毕之后,然后会去task queue(任务队列)去执行队列中的任务。
所以
console.log("one")
setTimeout(()=>{
console.log("two")
}, 0)
console.log("three")
结果会是:
"one"
"three"
"two"
而不是one two three
。
但是ES6 引入了promise generator async 等异步概念,他们是不是也是被放到了task queque呢。。答案是否定的。
在任意特定的时刻
,一次只有一个队列中的一个事件可以被处理,当事件执行的时候,它可以间接的或者直接的引发(导致)一个或者更多的后续事件。
一个task queue 引发了后续的task queue的例子
(function () {
console.log('main stack')
setTimeout(() => {
console.log("1 in task queue one")
setTimeout(() => {
console.log("11 in task queue two")
}, 0)
}, 0)
setTimeout(() => {
console.log("2 in task queue one")
setTimeout(() => {
console.log("22 in task queue two")
}, 0)
}, 0)
})()
es6引入了新的概念:job queque(工作队列)
, Promise的异步行为是基于job的。
实际上这个概念,就是和微任务队列microqueue
是类似的。
宏观任务队列、微观任务队列
常见的macro-task宏观任务有: setTimeout 、 setInterval、script、I/O操作、UI渲染
常见的micro-task微观任务有:promise的then回调函数、async函数里面await表达式后面的代码。
上面我们知道了 setTimeout 的回调函数,必须等待callstack主线程的同步代码执行完毕之后,才去执行,这一步已经理解了,主线程也是第一个宏任务。
但是我们看下面的代码:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
执行结果是
script start
script end
promise1
promise2
setTimeout
promise的构造函数为同步代码,then回调函数会被放到微任务队列里面。
settimeout的回调函数会被放到task queue 宏任务队列里面。
callstack 主线程的同步代码[第一个宏任务]执行完毕---->> 微任务队列 ----> 第二个宏任务 settimeout —>>微任务队列
宏任务是执行的基本单元,微任务队列是从属于一个宏任务的。
还有一种情况是async 函数:
async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。
如果后面的代码有return 语句,比如return xxx,会立即返回一个promise:
Promise.resolve( xxx )
,如果没有显示的声明return语句,那么返回的是
Promise.resolve(undefined)
, 返回的promise对象会等待await表达式完成之后,才会从pending -> resolved 或者 rejected
小测试
下面这段代码的打印结果是啥呢?
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')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
还有这个呢?
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
}).then(function(){
console.log('promise11')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
}).then(function(){
console.log('promise22')
})
}, 0)
参考:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/33
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
《you dont know javascript》第五章 异步与性能
补充-- 2020 09-28
requestAnimationFrame
是属于宏任务