【技术积累】JS事件循环,Promise,async/await的运行顺序

参考文章:

[1] 如何解释Event Loop面试官才满意?
http://www.imooc.com/article/293329

[2]【JS】深入理解事件循环,这一篇就够了!(必看)
https://zhuanlan.zhihu.com/p/87684858

[3] 理解promise--一个问题引发的思考
https://segmentfault.com/a/1190000016935513

[4] 关于 async 函数的理解
https://juejin.im/post/5c0f73e4518825689f1b5e6c

浏览器的执行线程

浏览器是多进程的,每一个tab页面代表一个独立的进程。其中浏览器渲染进程(内核)属于浏览器多进程的一种,

主要负责 页面渲染,脚本执行,事件处理等,其包含的主要线程有以下:

GUI渲染线程(负责解析HTML,CSS构成的DOM树),JS引擎线程,事件触发线程,定时器触发线程,http请求线程等。

关于执行中的线程:

主线程:也就是js引擎执行的线程, 只有一个, 页面渲染,函数处理都在这个主线程上执行。

工作线程:幕后线程,存在于浏览器或js引擎内,与主线程分开,处理文件读取,网络请求等异步事件。

注意:JS是单线程语言,并没有专门的异步执行线程,异步操作都是放入任务队列里,等待主线程执行栈来执行。

任务队列:

同步任务:立即执行的任务,一般会直接进入主线程中执行。

异步任务:需要等待执行的任务,ajax网络请求,setTimeout定时函数等,通过在事件表(event table)注册函数进入任务队列 先进先出的机制来协调执行。

所谓事件循环(Event Loop),是指如下过程的不断重复:

同步异步任务分别进入不同的执行环境,同步的进入主线程,异步的进入任务队列。

主线程内的任务执行完毕为空,就会去任务队列读取相对应的任务,推入主线程执行。

在时间循环中每进行一次循环操作称为 tick ,每一次tick的任务处理的关键步骤如下:

1. 在此次tick中选择最先进入 队列的 那个任务(即宏任务 MacroTask),如果有 则执行;

2. 检查是否存在微任务(MicroTasks)如果存在则依次不断执行,直至清空微任务队列。

3. 更新 render

4. 主线程重复执行上述步骤

宏任务 主要包含:script(整体主代码),setTimeout,setInterval,I/O,UI交互事件, requestAnimationFrame,setImmediate(Node.js环境)。

     优先级:主代码块 > setImmediate > MessageChannel > setTimeout / setInterval

微任务 主要包含:Promise.then(await句执行完后面的剩余代码也类似于then),MutationObserver,process.nextTick(Node.js环境)

     优先级:process.nextTick > Promise > MutationObserver

注意:这里的优先级是指在任务队列中执行的优先顺序,优先级高的即使后加入也会先执行。

举例说明:

console.log('script start');                 宏任务1:整体js代码,本次执行的宏任务

setTimeout(function() {
  console.log('setTimeout');              宏任务2:setTimeout,放入宏任务队列
}, 0);

Promise.resolve().then(function() {        
  console.log('promise1');                微任务1:then1,放入微任务队列
}).then(function() {
  console.log('promise2');                微任务2:then2,放入微任务队列
});

console.log('script end');                 宏任务1:整体js代码,本次执行的宏任务

执行顺序:

宏任务1,微任务1,微任务2,宏任务2

再来一个题目,来做个练习:

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');

这个题目就稍微有点复杂了,我们再分析下:

首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到 promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1; 接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2 至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

用流程图看更清晰:

总结

有个小 tip:从规范来看,微任务 优先于 宏任务 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。

Promise语法

ES6引入了Promise。

Promise 是一个值的占位符,这个值在未来的某个时间要么 resolve 要么 reject。

当你知道一个 promise 总是 resolve 或者总是 reject 的时候,你可以写 Promise.resolvePromise.reject,传入你想要 rejectresolvepromise 的值。

new Promise(res => res('resolve OK!'))         等价于   Promise.resolve('resolve OK!')

new Promise((res, rej) => rej('reject NG!'))    等价于   Promise.reject('reject NG!')

复杂练习题:
 

//                          tick:  1           2             3            4
<script>
async function async1() {
  console.log('async1 start')   //2.out-2
  await async2()
    console.log('async1 end')   //           11.+w3        13.out-9
}

async function async2() {
  console.log('async2  start')  //3.out-3
  await async3()
    console.log('async2  end')  //5.+w1      10.out-7
}

async function async3() {
  console.log('async3')         //4.out-4
}

console.log('script start')     //1.out-1

setTimeout(function() {
  console.log('setTimeout')     //6.+h1                               14.out-10
}, 0)

async1()

new Promise(function(resolve) {
  console.log('promise1')       //7.out-5
  resolve()
}).then(function() {
    console.log('promise2')     //8.+w2       12.out-8
})

console.log('script end')       //9.out-6

</script>

Async/Await语法

ES7 引入了一个新的在 JavaScript 中添加异步行为的方式并且使 promise 用起来更加简单!

随着 asyncawait 关键字的引入,我们现在能够创建隐式地返回一个对象的异步函数,而不是显式地使用 Promise 对象!

当遇到await关键字的时候,异步函数的执行被暂停,async函数中剩余的代码会在微任务中运行而不是一个常规(宏)任务!

接着JavaScript引擎跳出异步函数,并且在异步函数被调用的执行上下文中继续执行代码。

await函数中剩余的代码   等价于  Promise.then中的代码。

猜你喜欢

转载自blog.csdn.net/ttyt1217/article/details/106056353