您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

ES6+ async/await

前面几节我们已经学习了异步的方案 Promise 和 Generator,上一节我们也通过案例使用 Promise + Generator 实现了比较好的异步案。同时我们实现了简版的 co 库,让我们在使用 Generator 处理异步任务时更加方便,但这不是美的案。

本节我们将学习 ES7 推出的 async/await 其特性是对 JS 的异步编程进行了重要的改进,在不阻塞主线程的情况下,它给我们提供了使用同步的风格来编写异步任务的能力。另外,我们要明确的是 async/await 其实是 Promise + Generator 的语法糖,为了帮助我们像写同步一样书写异步,风格更优雅,捕获也更容易。

本节我们将通过对上一节案例的改造。在不需要 co 库的情况下直接使用 async/await 让我们更加深刻地理解异步方案的演变过程。

我们通过案例来讲解 Promise + Generator 在实际应用中的使用,通过 Generator 和 yield 让异步看起来像同步一样执行。但是这样里面存在的问题就是器直接执行,需要手动处理。为了深层回调的问题我们借助了 co 库来帮助我们去执行器,从而了回调地狱的问题。下面是上一节的。

const ajax = function(api) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (api === 'api_1') {
        resolve('api_2');
      }
      if (api === 'api_2') {
        resolve();
      }
    }, )
  })
}

function * getValue() {
  const api = yield ajax('api_1');
  const value = yield ajax(api);
  return value;
}

co(getValue()).then(res => {
  console.log(res);
})

上面的中 getValue 是器,不能直接,这里用 co 库来进行执行,然后通过 Promise 的链式执行后的结果。但是这里借助了 co 的库,我们其实最希望的是能像执行普通一样直接 getValue 就能执行并得到结果。 async/await 的出现就是为了抹平在时所做的额外步骤。那让我们看看 async/await 是怎么用的:

async function getValue() {
  const api = await ajax('api_1');
  const value = await ajax(api);
  console.log(value)
  return value;
}
getValue()	// 控制台打印 value的值是:100

上面的中我们可以看出使用 async/await 定义的 getValue 和器 */yield 定义的基本相同,但是在执行时 async/await 定义的直接即可。从这里我们就能看到 async/await 的优点,无需过多的操作非常优雅和简洁。

上面我们基本了解了 async ,下面我们就来看看它的基本使用和需要注意的地方。

定义异步时需要使用 asyncfunction 关键字一起来完成,类似器中的 yield 来暂停异步任务,在 async 中使用 await 关键去等待异步任务返回的结果。

async 其本质是 Promise + Generator 组成的语法糖,它为了减少了 Promise 的链式,解放了 Generator 的单步执行。主要语法如下:

async function name([param[, param[, ... param]]]) { 
    statements 
}

上面中的 statements 是主体的表达式,async 可以通过 return 来返回值,这个返回值会被包装成 Promise 实例,可以被链式。下面我们来看两段等价。

// 下面两段时相同的
async function foo() {
   return 
}
function foo() {
   return Promise.resolve()
}

// 下面两段时相同的
async function foo() {
  await ;
}
function foo() {
   return Promise.resolve().then(() => undefined)
}

上面的两段时相同的,这里我们就不去探究 async 是怎么实现的,其大概原理类似上节写的 co 库,有兴趣的小伙伴可以去 上去看看 async 编译是什么样子的。

当在 async 中返回的是普通值或 await 后跟普通值时,此时的 async 是同步的。在 Promise 中失败是不能被 try...catch 捕获的,需要通过 catch 的方式来捕获。而使用 async 则是可以通过 try...catch 来捕获。

async function foo() {
	return new Error('Throw an error');
}

foo().then(res => {
  console.log(res)
}).catch(err => {
  console.error(err)	// Error: Throw an error
})

async function foo2() {
  try{
    var v = await foo()
    console.log(v)
  } catch(e) {
    console.log(e);	// Error: Throw an error
  }
}
foo2()

上面的中在执行 foo() 直接抛出了,而 Promise 和 async/await 对的捕获是不同的,我们知道 Promise 是通过 then 中的失败回调和 catch来捕获的,而 async 使用的是 try...catch 更像同步的方式。

但是有个问题,当程序需要同时处理多个异步任务时,那我们使用 async/await 怎样捕获那个异步任务出现呢?try 块中的只要程序出现就会抛出,但是不知道是哪个异步任务出错了不利于定位问题。如果使用多个 try...catch :

const task = function (num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (num === ) {
        reject('throw error')
      } else {
       	resolve('imooc'); 
      }
    }, )
  })
}

async function foo() {
  try {
    let res1 = await task();
    try {
      let res2 = await task();
      try {
        let res3 = await task();
      } catch(e) {
        console.log('res3', e)
      }
    } catch(e) {
      console.log('res2', e)
    }
  } catch(e) {
    console.log('res1', e)
  }
}
foo()	// res3 throw error

看到上面的你是不是觉得很难受啊,又回到了嵌套地狱的原始问题了。async 在异常捕获时,没有非常完美的案,这主要源自依赖 try...catch 对的捕获。但有一些还算比较优雅的案,我们已经知道了 async 返回的是 Promise 那么我们是不是可以使用 Promise 的 catch 来捕获呢?答案是当然的呢。

async function foo() {
  let res1 = await task().catch(err => console.log('res1', err));
  let res2 = await task().catch(err => console.log('res2', err));
  let res3 = await task().catch(err => console.log('res3', err));
}
foo()	// res3 throw error

上面的看起来就比嵌套的 try...catch 感觉好很多,这也是比较好的式。在使用 catch 时需要弄清楚 Promise 和 async 之关系,不然就很难理解这种写法。

既然 async/await 这么优雅简洁,那在编程的过程中都使用这个就好啦!其实这里是坑,很多时候 async/await 都会被滥用导致程序卡顿,执行时间过长。

async function foo() {
  let res1 = await task();
  let res2 = await task();
  let res3 = await task();

  return { res1, res2, res3 }
}
foo()

在很多时候我们会写成这样的,如果后任务依赖前任务这样写完全没问题,但是如果是三个独立的异步任务,那这样写就会导致程序执行时间加长。这样的过于同步化,我们需要牢记的是 await 看起来是同步的,但它仍然属于异步的,最终还是走的回调,只是语言底层给我们做了很多工作。

针对没有关联的异步任务我们需要把它们解开,

const task = function (num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
       	resolve('imooc ' + num); 
    }, )
  })
}

async function foo() {
  let res1Promes = task();
  let res2Promes = task();
  let res3Promes = task();

  let res1 = await res1Promes;
  let res2 = await res2Promes;
  let res3 = await res3Promes;

	console.log({ res1, res2, res3 })

  return { res1, res2, res3 }
}
foo();	// { res1: 'imooc 100', res2: 'imooc 200', res3: 'imooc 300' }

这里需要明白的一点是:为什么要把 task 拿到 await 外面去执行呢?await 的本质就是暂停异步任务,等待返回结果,等到结果返回后就会继续往下执行。还要知道的是每个 task 都是异步任务,像之前的那种写法,await 会等待上异步任务完成才会走下。而我们把 task 拿出来了,也就是每个 task 会按照异步的方式去执行。这个时候三个 task 都已经开始执行了,当遇到 await 就只需要等到任务完成就行。所需要的时间是异步任务中耗时最长的,而不是之前的总和。

本节我们主要通过延续上一节的案例,用 async 给出了最优的案,从而完善了整个异步演变的过程,让我们更加清晰地理解为什么会有 Promise?为什么会有器?为什么会有 async/await?由浅入深层层递进地讲解了 ES6 以后对异步任务处理的演变。然后我们主要学习了 async 的基本使用和处理的捕获。最后,我们讲解了如果不滥用 async 的案例,让我们在以后写程序的过程中更加得心应手。


联系我
置顶