Async/Await 原理

前言

如下是我们最常见的一个使用Async/Await的例子

javascript 复制代码
// 异步函数,等待一段时间后返回结果
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('操作完成');
    }, ms);
  });
}

// 使用 async 和 await 处理异步操作
async function executeAsync() {
  console.log('开始执行异步操作');
  
  try {
    const result = await delay(2000); // 等待2秒钟
    console.log('异步操作完成:', result);
  } catch (error) {
    console.error('异步操作出错:', error);
  }
}

executeAsync();

这次只想将这个es6语法再展开深入的聊一聊,把熟悉的陌生人变成熟人~

出现的原因

如下参考了李兵老师的浏览器原理:

javascript 复制代码
fetch('https://www.geekbang.org')
      .then((response) => {
          console.log(response)
          return fetch('https://www.geekbang.org/test')
      }).then((response) => {
          console.log(response)
      }).catch((error) => {
          console.log(error)
      })

进行异步请求,我们明明可以结合浏览器原生支持的fetch 方法,返回一个Promise 对象进行then链式调用,为什么还会出现async/await呢?

从这段 Promise 代码可以看出来,使用 promise.then 也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读。

基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。你可以参考下面这段代码:

javascript 复制代码
async function foo(){
  try{
    let response1 = await fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = await fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
  }catch(err) {
       console.error(err)
  }
}
foo()

通过上面代码,你会发现整个异步处理的逻辑都是使用同步代码的方式来实现的,而且还支持 try catch 来捕获异常,这就是完全在写同步代码,所以是非常符合人的线性思维的。这也是它为什么会受欢迎的原因了~

Generator(生成器)

Generator生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。我们可以看下面这段代码:

javascript 复制代码
function* genDemo() {
    console.log("开始执行第一段")
    yield 'generator 2'

    console.log("开始执行第二段")
    yield 'generator 2'

    console.log("开始执行第三段")
    yield 'generator 2'

    console.log("执行结束")
    return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next())       // { value: 'generator 2', done: false }

console.log(gen.next().value) // generator 2
console.log('main 1')
console.log(gen.next().value) // generator 2
console.log('main 2')
console.log(gen.next().value) // generator 2
console.log('main 3')
console.log(gen.next().value) // generator 2
console.log('main 4')

执行上面这段代码,观察输出结果,你会发现函数 genDemo 并不是一次执行完的,全局代码和 genDemo 函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。下面我们就来看看生成器函数的具体使用方式:

  • 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
  • 外部函数可以通过 next 方法恢复函数的执行。

关于函数的暂停和恢复,相信你一定很好奇这其中的原理,那么接下来我们就来简单介绍下 JavaScript 引擎 V8 是如何实现一个函数的暂停和恢复的,这也会有助于你理解后面要介绍的 async/await。

要搞懂函数为何能暂停和恢复,那你首先要了解协程的概念。协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

为了让你更好地理解协程是怎么执行的,我结合上面那段代码的执行过程,画出了下面的"协程执行流程图",你可以对照着代码来分析:

从图中可以看出来协程的四点规则:

  1. 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
  2. 要让 gen 协程执行,需要通过调用 gen.next
  3. 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
  4. 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。

Async/Await

Async

  • async声明function是一个异步函数,返回一个promise对象,可以使用 then 方法添加回调函数。
  • async函数内部return语句返回的值,会成为then方法回调函数的参数。
javascript 复制代码
async function test() {
  return 'test';
}

// async返回的是一个promise对象
console.log(test()); // Promise { 'test' }

test().then(res => {
  console.log(res); // test
})

// 如果async函数没有返回值 async函数返回一个undefined的promise对象
async function fn() {
  console.log('没有返回');
}
console.log(fn()); // Promise { undefined }

Await

  • await 操作符只能在异步函数 async函数内部使用。
  • 如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果。如果等待的不是 Promise 对象,则返回该值本身。
javascript 复制代码
async function test() {
    return new Promise((resolve)=>{
      setTimeout(() => {
          resolve('test 1000');
      }, 1000);
    })
  }

  async function next() {
      let  res1 = await test(),
      let  res2 = await 100;
      console.log(res1);
      console.log(res2);
  }
  next(); // 1s 后才打印出结果 为什么呢 就是因为 res1在等待promise的结果 阻塞了后面代码。
  

为什么说 Async/Await是Generator的语法糖?

这是要还原的效果

javascript 复制代码
async function getResult() {
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })
    

    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })

}

getResult()

// 1
// 2
// 3

如果用generator实现呢?

也仿照着这样写

javascript 复制代码
function* getResult(params) {
    
    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen = getResult()

gen.next();
gen.next();
gen.next();

但是发现打印顺序是 3,2,1明显不对。

这里的问题主要是三个 new Promise几乎是同一时刻执行了,然后resoleve(3)中的异步任务刚执行完,才会出现这种问题,所以需要等第一个promise执行完resolve之再执行下一个,所以要这么实现:

javascript 复制代码
function* getResult(params) {

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen = getResult()

gen.next().value.then(() => {
    gen.next().value.then(() => {
        gen.next();
    });
});

// 1
// 2
// 3

但是呢,总不能有多少个await,就要自己写多少个嵌套吧,所以还是需要封装一个函数,显然,递归实现最简单

scss 复制代码
function* getResult(params) {

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen = getResult()

function co(g) {
    const nextObj = g.next();
    
    // 当nestObj.done = true时,代表函数运行完毕了
    if (nextObj.done) {
        return;
    }
    nextObj.value.then(()=>{
        co(g)
    })
}

co(gen)

// 1
// 2
// 3

这样Async/Await的效果就实现了~

相关推荐
Jiaberrr1 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy1 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白1 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、1 小时前
Web Worker 简单使用
前端
web_learning_3211 小时前
信息收集常用指令
前端·搜索引擎
tabzzz1 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百2 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao2 小时前
自动化测试常用函数
前端·css·html5
码爸2 小时前
flink doris批量sink
java·前端·flink
深情废杨杨2 小时前
前端vue-父传子
前端·javascript·vue.js