任务队列
我在上一篇的文章中说到,在JavaScript单线程语言的神奇操作之下,我们的同步任务先予以执行,而我们的异步任务则会进入“任务队列”之中,而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。说白了,任务队列就是安排函数执行顺序的一个队列。
那么,在js中粗略的事件执行步骤是什么呢?
1. 首先先执行主线程,其实就是在调用调用栈里面的同步代码,系统会先予以执行
2. 等到主线程将调用栈里面的全部同步任务执行完毕后,事件循环此时开始执行
3. 主线程发现调用栈为空后,会进行事件循环来观察要执行的事件回调。这个时候会进入任务队列当中,而事件循环检测到任务队列当中有事件,就进行我们最上面说到的操作,然后就取出相关事件任务放入调用栈中,由主线程执行。
那么我们就要来说说这个所谓的事件循环,那么这是个什么东西呢?
事件循环
我们都知道,当js代码在执行时,也就是往调用栈中放进去函数,然后再执行,但是,如果遇到异步函数,异步函数就会被挂起,进入所谓的任务队列中,并在调用栈为空的时候拿出来执行 流程如下:
step1:主线程读取JS代码,此时为同步环境,形成相应的执行上下文,也就是调用栈;
step2: 主线程遇到异步任务,将异步任务挂起;
step3: 将相应的异步任务推入任务队列;
step4: 主线程之中的任务执行完毕,查询任务队列,如果队列中存在任务,则取出一个任务推入主线程处理;
step5: 重复执行step2、3、4;称为事件循环。
具体流程如下图:
接下来我们又要来说说任务队列当中的猫腻了
宏任务与微任务
从任务队列中提取出来执行的函数分两种情况:
-
微任务(microtask) :如process.nextTick, promise.then, MutationObserver
-
宏任务(macrotask) :如script(最早执行), setTimeout, setInterval, setImmediate, I/O, UI-rendering
event-loop 五部曲:
1. 首先执行同步代码,这属于宏任务(至少包含script)
2. 当执行完所有同步代码,执行栈为空,查询是否有异步代码要执行
3. 执行所有的微任务
4. 当所有的微任务执行完毕后,有需要的话会渲染页面
5. 开启下一次的Event-Loop,执行宏任务中的异步代码
async 与 await
那么在此之前我们有需要来聊聊async 与 await。
如果在某个函数前加了async这个关键字,则表示当前这个函数内部可以存在异步,但是不会影响函数本身,async当中,也可以利用关键字,也就是await,而加了await的代码会立即执行,且后面的代码会被阻塞,导致后面的代码去到下一次的微任务队列。
我们看个例子
function getJSON() {
return new Promise((resolve,reject) =>{
setTimeout( () => {
console.log('json');
},500)
})
}
// async表示当前这个函数内部可以存在异步
async function testASync() {
await getJSON()//加了await的代码会立即执行,且后面的代码会被阻塞
console.log('数据已经拿到了')
}
testASync()
执行结果
json
数据已经拿到了
代码分析
上述代码当中,我们定义了一个getJSON( )函数,并在其中设置了异步函数,打印‘json’,而在另外一个函数当中,我们利用了async 与 await,并在其中执行getJSON( )函数,此时代码立即执行,阻塞了后一步的同步代码console.log('数据已经拿到了')
,那么此时就会先执行getJSON( )函数,打印了‘json’,之后才打印‘数据已经拿到了’。
看完这个,我们来看终极代码
例题
console.log('script start')
async function async1() {//同步
await async2() // await会导致后面的代码去到下一次的微任务队列
console.log('async1 end')//同步 v8正在违反规定,将await后面的代码提上一个循环
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () { //异步 宏任务
console.log('setTimeout')
}, 0) //异步函数
new Promise(resolve => {//同步代码
console.log('Promise')
resolve()
})
.then(function () {//异步 ,微任务
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
代码分析
我们就严格按照事件五部曲来代替js,执行以下这个代码。
-
首先执行同步代码,那么我们从上往下寻找同步代码。我们发现,第一行
console.log('script start')
就是一个同步代码,所以直接打印出结果script start
;之后我们继续执行,发现了async function async1()
这个函数,但是我们却需要卸掉他的伪装,虽然它前面有关键字async,但是有整个关键字存在并不会影响函数本身,所以它还是一个同步函数,这个时候在里面有个await async2()
,发现await,代码立即执行,打印出结果async2 end
,将console.log('async1 end')
挤到下一次的微任务队列。之后又找到了Promise( )
,打印出结果Promise
,最后才遇到了最后一个同步代码console.log('script end')
,直接打印. -
当执行完所有同步代码后,执行栈为空,查询是否有异步代码要执行,这里很明显有异步函数,但是其中又包括了宏任务与微任务,所以直接去第三步
-
执行所有的微任务,我们从上到下,所以发现了微任务为两个
.then( )
函数,所以需要打印结果promise1,``promise2
。 -
当所有的微任务执行完毕后,有需要的话会渲染页面,这里似乎没有。
-
最后,执行宏任务
setTimeout
,需要打印结果setTimeout
并开启下一次的Event-Loop,执行宏任务中的异步代码,也就是被挤到后面的那个async1 end 对于这个,需要说明一下,因为在谷歌浏览器中,浏览器将await后面的代码提升上一个循环的微任务当中,也就是下列结果的promsise1前面,所以在浏览器里面打印的是下面这份结果,这个我们需要区分开来。
执行结果(浏览器)
//script start
// async2 end
//Promise
//script end
//async1 end
//promise1
//promise2
//setTimeout
执行结果(编译器)
//script start
// async2 end
//Promise
//script end
//promise1
//promise2
//setTimeout
//async1 end 这一行不同
总结
我是小白,希望能和大家一起学习js,如果存在错误,敬请指出,谢谢大家!