阅读 413

Generator,async/await究竟是什么?(二.async/await)

上一章探讨了Generator的相关知识他也是理解async/await的一个基础。async/await是对Promise处理异步任务同步化的进一步优化,语法简单,是前端人员很喜欢使用的语法糖。那么async/await背后到底有什么呢:

  1. async/await优点有什么?
  2. asyncFunction构造函数。
  3. async/await语法,及错误捕获。
  4. async/await运行原理。
  5. 通过PromiseGenerator函数,实现一个async/await

1. async/await 优点有什么

  1. async/await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise
  2. 更符合人类的思考方式,使用Promise时,需要写.then()等多个回调,去获取内部的数据,阅读起来多少还是有些不便,async/await书写顺序即是执行顺序,容易理解。
  3. Promise是根据函数式编程的范式,对异步过程进行了一层封装,async/await基于协程的机制,是对异步过程更精确的一种描述。

2. AsyncFunction 构造函数

Generator一样,async函数也是通过构造函数创建出的新异步函数对象。而且也不是一个全局对象。

let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
async function asyncExpression(){}
let asyncFn = new AsyncFunction()
console.log(asyncExpression instanceof AsyncFunction) // true
console.log(asyncFn instanceof AsyncFunction) // true
// 但不建议这样使用new这个实例,就像创建数组直接使用字面量一样。
// 因为函数表达式更高效,异步函数是与其他代码一起被解释器解析的,而使用new函数体是单独解析的。
复制代码

调用 AsyncFunction构造函数时可以省略 new,其效果是一样的。

3. async/await 语法,及错误捕获

当我们用创建出一个async函数时,async 一定是返回一个Promise

async function foo() {
   return 1
}
// 等价与
function foo() {
   return Promise.resolve(1)
}
复制代码
  1. async 意为异步的意思,一定是在此函数中执行异步任务而设立的,当然只写同步任务也没关系,这时 async 便是冗余的。
  2. 它与Generator函数很像,内部可以有多个await,执行到await表达式时会暂停阻塞整个async函数并等待await表达式后面的Promise异步结果,直到拿到Promise 返回的 resovle 结果后恢复async函数的执行。
function promiseFn(){
    return new Promise((resovle, reject) => {
        setTimeout(() => {
          resovle('success')
        }, 2000)
    })
}
async function foo() {
   let a = await promiseFn();
   console.log(a);
   console.log('after await'); 
}
console.log(foo())  //两秒后输出 success , after await
复制代码
  1. await必须在async函数体内执行,不然会报语法错误。
  2. 由于await是同步执行,如果await后的异步任务报错,会导致后面代码无法执行,所以要用try/catch来捕获错误,可以让后续代码继续执行。
function promiseFn(){
    return new Promise((resovle, reject) => {
        setTimeout(() => {
          reject('error')
        }, 2000)
    })
}
async function foo() {
    try{
        let a = await promiseFn();
    }catch(e) {
        console.log(e); //两秒后输出 error
    }
}
复制代码

5.在 async函数中await在执行Promise异步任务时会阻塞async函数体内后面的代码,但 async函数调用并不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行,这也正是 await 必须用在 async函数中的原因。

function promiseFn1() {
    return new Promise((resovle, reject) => {
        setTimeout(() => {
              resovle('1success')
        }, 2000)
    })
};
function promiseFn2() {
    return new Promise((resovle, reject) => {
        setTimeout(() => {
            resovle('2success')
        }, 1000)
    })
}
async function foo() {
    let a = await promiseFn2();
    console.log(a);
}
foo()
promiseFn1().then(res => {
    console.log(res);
})
// 一秒后输出:2success 
// 两秒后输出:1success
复制代码

6.多个await且没有互相的依赖时,使用Promise.all请求可以同时请求,加快速度。

function promiseFn1() {
  return new Promise((resovle, reject) => {
    setTimeout(() => {
      resovle('success1')
    }, 10000)
  })
}
function promiseFn2() {
  return new Promise((resovle, reject) => {
    setTimeout(() => {
      resovle('success2')
    }, 5000)
  })
}

async function foo() {
    let res = await Promise.all([promiseFn1(), promiseFn2()])
    console.log(res);  //十秒后 ['success1', 'success2']
}
// 如果是两个await依次进行,得到所有结果的时间为15秒。
foo()
复制代码

4. async/await运行原理

async/await是由 generator+yield 控制流程 + Promise 实现回调。对语法进行了封装。 所以理解 generator+yield是如何控制流程的,我会简单介绍进程线程协程的概念,并结合 generatorasync/await的语法来说明实现过程:

进程

  1. 简单的理解是一个应用程序的启动实例,打开浏览器,打开vscode,这时就开启了两个进程。
  2. 广义的定义为: 1)是一个具有独立功能的程序关于某个数据集合的一次运行活动。
    2)它是操作系统动态执行的基本单元,进程即是基本的分配单元,也是基本的执行单元。
  3. 每一个进程都有属于他的地址空间,包含文本区域、数据区域、堆栈。
  4. 进程本身不会运行,是线程的容器。
  5. 进程有五种状态:1)创建状态 2)就绪状态 3)执行状态 4)阻塞状态 5)终止状态**

线程

  1. vscode要编辑代码,打开命令窗口,打开git跟踪,这就是一个线程,多个进程工作。
  2. 一个进程中至少有一个主线程,也可有更多子线程,不然就没有存在的意义。
  3. 对操作系统来说,线程是最小的执行单元,线程和进程由操作系统进(CPU)行调度。
  4. 一个标准的线程由当前的线程ID、当前指令指针、寄存器和堆栈组成。
  5. 同一个进程中的多个线程之间可以并发执行。(并发: 两个或以上线程同时跳转运行,但同一时间只有一个线程运行)
  6. 线程状态:1)就绪状态 2)运行状态 3)阻塞状态。

协程

  1. 是一种比线程更加轻量级的存在,一个线程可以拥有多个协程。
  2. 协程的调度完全由用户控制。
  3. 协程拥有自己的寄存器上下文和栈。
  4. 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快

由于JavaScript运行时,是单线程的,利用循环机制来执行代码. 创建一个Generator函数,就是创建一个协程,可以通过 yield 的方式实现协程的切换,完成异步任务,运行流程大致如下:
1)协程A开始执行;
2)协程A执行到一半,进入暂停,执行权转移到协程B;
3)一段时间后,协程B交还执行权;
4)协程A恢复执行;
遇到yield表达式交出控制权,等得执行结果,拿到结果后,交还执行权并输出。理解了Generator运行机制就对比着async/await看看:

  1. 创建 Generator函数时的*,可以理解为创建 Async函数的 async
function* gen(){}
async function asy(){}
复制代码
  1. 执行到需要暂停下来的地方用yield,变为await,它内部有封装好的next(),所以不用手动调用。但await返回的值经过async包装,始终是Promise
function* gen(){
    yield 1
}
async function asy(){
    await 1
}
复制代码

所以可以说async/await是由 generator+yield 的控制流程 + Promise的语法封装。

5. 实现一个简单的 async/await

// 定义了一个promise,用来模拟异步请求,作用是传入参数++
function getNum(num){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num+1)
        }, 1000)
    })
}

//自动执行器,如果一个Generator函数没有执行完,则递归调用
function asyncFun(func){
  var gen = func();

  function next(data){
    var result = gen.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }
  next();
}

// 所需要执行的Generator函数,内部的数据在执行完成一步的promise之后,再调用下一步
var func = function* (){
  var f1 = yield getNum(1);
  var f2 = yield getNum(f1);
  console.log(f2) ;
};
asyncFun(func);
复制代码

如果此文章对您有帮助或启发,那便是我的荣幸
文章分类
前端
文章标签