• async/await 原理及执行顺序分析


    Generator函数简介

    Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态,但是只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

    有这样一段代码:

    1. functionhelloWorldGenerator() {
    2.   yield 'hello';
    3.   yield 'world';
    4.   return 'ending';
    5. }
    6. var hw = helloWorldGenerator();

    调用及运行结果:

    1. hw.next()// { value: 'hello'donefalse }
    2. hw.next()// { value: 'world'donefalse }
    3. hw.next()// { value: 'ending'donetrue }
    4. hw.next()// { value: undefined, donetrue }

    由结果可以看出,Generator函数被调用时并不会执行,只有当调用next方法、内部指针指向该语句时才会执行,即函数可以暂停,也可以恢复执行。每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

    Generator函数暂停恢复执行原理

    要搞懂函数为何能暂停和恢复,那你首先要了解协程的概念。

    一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

    协程是一种比线程更加轻量级的存在。普通线程是抢先式的,会争夺cpu资源,而协程是合作的,可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。它的运行流程大致如下:

    1. 协程A开始执行

    2. 协程A执行到某个阶段,进入暂停,执行权转移到协程B

    3. 协程B执行完成或暂停,将执行权交还A

    4. 协程A恢复执行

    协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

    执行器

    通常,我们把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器,co 模块就是一个著名的执行器。

    Generator 是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点:

    1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

    2. Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

    一个基于 Promise 对象的简单自动执行器:

    1. function run(gen){
    2.   var g = gen();
    3.   function next(data){
    4.     var result = g.next(data);
    5.     if (result.done) return result.value;
    6.     result.value.then(function(data){
    7.       next(data);
    8.     });
    9.   }
    10.   next();
    11. }

    我们使用时,可以这样使用即可,

    1. functionfoo() {
    2.     let response1 = yield fetch('https://xxx'//返回promise对象
    3.     console.log('response1')
    4.     console.log(response1)
    5.     let response2 = yield fetch('https://xxx'//返回promise对象
    6.     console.log('response2')
    7.     console.log(response2)
    8. }
    9. run(foo);

    上面代码中,只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。

    async/await

    ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。可以说async 是Generator函数的语法糖,并对Generator函数进行了改进。

    前文中的代码,用async实现是这样:

    1. const foo = async () => {
    2.     let response1 = await fetch('https://xxx'
    3.     console.log('response1')
    4.     console.log(response1)
    5.     let response2 = await fetch('https://xxx'
    6.     console.log('response2')
    7.     console.log(response2)
    8. }

    一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

    async函数对 Generator 函数的改进,体现在以下四点:

    1. 内置执行器。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。

    2. 更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

    3. 更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

    4. 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。

    这里的重点是自带了执行器,相当于把我们要额外做的(写执行器/依赖co模块)都封装了在内部。比如:

    1. async function fn(args) {
    2.   // ...
    3. }

    等同于:

    1. function fn(args) {
    2.   return spawn(function* () {
    3.     // ...
    4.   });
    5. }
    6. function spawn(genF) { //spawn函数就是自动执行器,跟简单版的思路是一样的,多了Promise和容错处理
    7.   return new Promise(function(resolve, reject) {
    8.     const gen = genF();
    9.     function step(nextF) {
    10.       let next;
    11.       try {
    12.         next = nextF();
    13.       } catch(e) {
    14.         return reject(e);
    15.       }
    16.       if(next.done) {
    17.         return resolve(next.value);
    18.       }
    19.       Promise.resolve(next.value).then(function(v) {
    20.         step(function() { return gen.next(v); });
    21.       }, function(e) {
    22.         step(function() { return gen.throw(e); });
    23.       });
    24.     }
    25.     step(function() { return gen.next(undefined); });
    26.   });
    27. }

    async/await执行顺序分析

    通过上面的分析,我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来看个例子:

    1. console.log('script start')
    2. async function async1() {
    3. await async2()
    4. console.log('async1 end')
    5. }
    6. async function async2() {
    7. console.log('async2 end')
    8. }
    9. async1()
    10. setTimeout(function() {
    11. console.log('setTimeout')
    12. }, 0)
    13. new Promise(resolve => {
    14. console.log('Promise')
    15. resolve()
    16. })
    17. .then(function() {
    18. console.log('promise1')
    19. })
    20. .then(function() {
    21. console.log('promise2')
    22. })
    23. console.log('script end')
    24. // script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

    分析这段代码:

    • 执行代码,输出script start

    • 执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。

    • 遇到setTimeout,产生一个宏任务

    • 执行Promise,输出Promise。遇到then,产生第一个微任务

    • 继续执行代码,输出script end

    • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务

    • 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1

    • 执行await,实际上会产生一个promise返回,即

    let promise_ = new Promise((resolve,reject){ resolve(undefined)})
    

    执行完成,执行await后面的语句,输出async1 end

    • 最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

  • 相关阅读:
    java计算机毕业设计项目材料管理系统源码+系统+数据库+lw文档+mybatis+运行部署
    Java面试过程中高频基础面试题
    前端开发:$nextTick()的使用及原理
    已解决AttributeError: ‘function‘ object has no attribute ‘ELement‘
    AQS 为什么要使用双向链表?
    播放器开发(五):视频帧处理并用SDL渲染播放
    【Python21天学习挑战赛】文件读写操作
    C++之STL
    【JavaScript总结】js基础知识点
    智慧煤矿/智慧矿区视频汇聚存储与安全风险智能分析平台建设思路
  • 原文地址:https://blog.csdn.net/qq_41581588/article/details/125931938