async-await原理

163 阅读9分钟

1、原理1

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。例:

// async返回的是Promise 对象?
async function testAsync() {
  return 'hello';
  // Promise对象的返回值如果不是Promise,会通过Promise.resolve()转化为Promise,再进行处理
}
const result = testAsync()
console.log(result);//Promise { 'hello' }说明async返回的是Promise对象

那既然async返回的是Promise对象,那么async后面的函数可以接.then()或者.catch()...嘛?我们试一试就知道了。

// async返回的是Promise 对象,并且可以使用Promise的方法?
async function testAsync() {
   // await await等待还是Promise对象
   return 'hello';
}
testAsync().then(res => {
  console.log(res)
}).catch(err => {
  console.log(err)
})
// hello 说明async返回的是Promise对象,而且可以使用Promise的方法,并且默认状态是resolved

上面代码说明,async函数内部return语句返回的值,会成为then方法回调函数的参数

2、原理2

当async函数内部抛出错误的时候,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被.then()方法的第二个回调函数接收或者.catch()方法回调函数接收到。

// async函数内部抛出错误或者Promise状态为reject
async function testError(){
  //throw new Error('出错啦~');
  await Promise.reject('出错了'); //await前面有return和没有return效果样
}
testError( )
// .then(()=>{}, (error)=>{ console . log(error);})
.catch(error=> {console.log(error);})
//Error: 出错啦~

3、原理3

await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。代码说明:

// await
async function getName(){
// return '返回值';
  return await '返回值';//上面直接return等价于这个return
}
getName().then( result=> {console.log(resu1t);})
//返回值

4、原理4

await的使用,必须要有async,async返回的是一个Promise对象,await等待的就是这个Promise对象,所以await不能没有async(但是async可以没有await)。如果await没有async会报错:

// await没有async会报错
function testAwait(){
  return await '西红柿炒鸡蛋'
}
testAwait().catch(error=>{
  console.1og(error);
})
//SyntaxError: await is only valid in async function

二、深入Async-await规则

1、async封装Promise

// async封装Promise
async function fn1() {
  return '喜洋洋';//相当于return Promise.resolve('喜洋洋')
  const data = await fn1();//接收data值
}
fn1()//执行async两数,返回的是一个Promise对象
.then(data => {
console.log('content = ', data)
})
//content = 喜洋洋

2、await相当于then

// await---. then( )
async function getName(){
  const operate = Promise.resolve('白雪公主')//执行两数
  const name = await operate //await 相当子Promise的then operate. then(name=>{})
  console.log('name:' ,name)
}
getName();
(async function(){
  const person = await '七个小矮人' //await Promise.resolve('七个小矮人') await 后面不跟Promise,也会被封装成Promise
  console.1og('person:',person)//400
})();//自执行函数
//name:白雪公主
//person:七个小矮人

3、多个await时,按时序执行

当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行

async function testOrder() {
  await Promise.reject('出错了') //UnhandLedPromiseRejectionWarning:出错了
  await Promise. resolve( 'hello world'); //不会执行
}
testorder();

4、try…catch相当于catch

如果希望即使前一个异步操作失败,也不要中断后面的异步操作。可将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

// try...catch
!(async function () {
    const testError = Promise.reject( '出错啦~~' )//rejected状态
    // const testError=throw new Error( '出错啦www ');
    try {
      const result = await testError; //await 相当F then,但是reject 不会触发then
      console.1og('success: '+result) // 不会输出,因为const result = await testError 被报错,被catch捕获
    } catch (error) {
      console.error('error: '+error)//try...catch相当F Promise的catch
    }
})()
//error:出错啦~~

当await后面是Promise对象的时候,我们也可直接在await后面直接.catch捕获错误:

async function testError() {
await Promise.reject('出错了')
  .catch(error => console.log(error));//这里捕获错误, 不会影响下一个await执行
  return await Promise.resolve('hello world');
}
testError()
  .then(result => console.log(resu1t))

三、解析Async-await语法

我们浅浅看一个面试题:

//面试题
function getJSON() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.1og(2);
      resolve(2)
    }, 2000 )
  })
}
async function testAsync() {
  await getJSON()
  console.1og(3);
}
testAsync()
//2
//3

我们必须知道:

(1)await不能单独出现,其函数前面一定要有async。

(2)await会干两件事:

第一,将写在await后面的代码放到async创建的那个Promise里面执行

第二、将写在await下面的代码放到前一个创建的那个Promise对象的.then里面执行

(3)await返回的也是Promise对象,他只是把await下面的代码放到了await返回的promise的.then里面执行。

翻译如下:

function getJSON() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.1og(2);
      resolve(2)
    }, 2000)
  })
}
// 编译成 Promise 原理
function testAsync() {
  return Promise.resolve().then(() => { 
    return getJSON();
  })
  .then(() => {
    console.1og(3);
  })
}
testAsync()

四、拓展Async-await应用

1、场景1

// Promise解决方式
function doCallback(n) {
  var myPromise = new Promise(function (resolve, reject) {
  //处理异步任务
  var flag = true;
  setTimeout(function () {
    if (flag) {
      resolve(n)
    } else {
      reject('失败')
    },0)
  })
  return myPromise; 
}
doCallback(1)
.then((result) => { //then足成功执行的方法返回的还是一个Promise对象
  console.log(result);//打印张三res 是执行
  return fn(2); 
})
.then((result) => {
  console.log(result);
  return fn(3)
})
.then((result) => {
  console.log(resu1t);
  return fn(4)
})
.then((result) => {
  console.log(result);
})
.catch((result) => { // catch是失败执行的方法
  console.log(result);
})
// 好多then形成.then链

通过以上Promise方法,可以明显解决回调地狱“向右移”的浮夸表现,但是,Promise是基于 then, catch 的链式调用,但也是基于回调函数。.then链多多少少还是违背原生代码,async-await更加贴近于原生代码:

// 封装一个返回promise的异步任务
function doCallback(str) {
  var myPromise = new Promise(function (resolve, reject) {
  //处理异步任务
  var flag = true;
  setTimeout(function () {
    if (flag) {
      resolve(str)
    } else {
      reject('失败')
    },0)
  })
  return myPromise; 
}

// 封装 个执行上述异 步任务的async 函数
async function testAsync() {
  var result1 = await doCallback(1); //await直接拿到fn()返回的promise的数据,并且赋值给result
  var result2 = await doCallback(2); //await后面的代码,都可以看做是异步回调callback里的内容, 都是异步的
  var result3 = await doCallback(3);
  var result4 = await doCallback(4);
  console.log(result1);
  console.log(resu1t2);
  console.log(resu1t3);
  console.log(resu1t4);
}
//这样是不是简洁优雅多了呢?
//执行两数
testAsync();
//1
//2
//3
//4

async / await 和 Promise 并不互斥,二者相辅相成。同时async / await 并不能改变异步的本质,js是单线程的,异步需要回调,都是要基于 event loop 来实现

2、场景2

function wait() {
  return new Promise(resolve =>
    setTimeout(resolve, 1000)
  )
}

async function main() {
  console.time();
  const x = wait();
  const y = wait();
  const z = wait();
  await x;
  await y;
  await z;
  console.timeEnd();
}
main(); // default: 1.002s


async function main2() {
  console.time();
  await wait();
  await wait();
  await wait();
  console.timeEnd();
}
main2(); // default: 3.005s

main的运行时间是1s多一点,这是因为:a,b,c的异步请求会按顺序发起。而这个过程是不需要互相依赖等待的。等到wait的时候,其实是比较那个异步耗时最多。就会等待最长。最长的耗时就是整体的耗时。

题目中的setTimeout()就是个异步任务。在所有同步任务执行完之前,任何的异步任务是不会执行的。

new Promise(xx)相当于同步任务, 会立即执行。

所以: x,y,z 三个任务是几乎同时开始的, 最后的时间依然是1000ms (比这稍微大一点点, 超出部分在1x1000ms之内)。

3、场景3 - forEach 中用 await 会产生什么问题?怎么解决这个问题?

问题:对于异步代码,forEach 并不能保证按顺序执行。

举个例子:

async function test() { 
  let arr = [4, 2, 1] 
  arr.forEach(async item => { 
    const res = await handle(item) 
    console.log(res)
  }) 
  console.log('结束')
 } 
 function handle(x) { 
   return new Promise((resolve, reject) => { 
     setTimeout(() => { 
     resolve(x)
     }, 1000 * x)
   }) 
 }
 test()

我们期望的结果是:

4 2 1 结束

但是实际上会输出:

结束 1 2 4

问题原因

这是为什么呢?我想我们有必要看看forEach底层怎么实现的。

// 核心逻辑 
for (var i = 0; i < length; i++) { 
  if (i in array) { 
    var element = array[i];
    callback(element, i, array);
  } 
} 

可以看到,forEach 拿过来直接执行了,这就导致它无法保证异步任务的执行顺序。比如后面的任务用时短,那么就有可能抢在前面的任务之前执行。

解决方案

如何来解决这个问题呢?

其实也很简单, 我们利用for...of就能轻松解决。

async function test() { 
  let arr = [4, 2, 1] 
  for(const item of arr) { 
    const res = await handle(item) 
    console.log(res) 
  }
  console.log('结束')
} 

解决原理——Iterator

这个问题看起来好像很简单就能搞定,那么想过这么做为什么可以成功吗?

其实,for...of并不像forEach那么简单粗暴的方式去遍历执行,而是采用一种特别的手段——迭代器去遍历。

首先,对于数组来讲,它是一种可迭代数据类型。那什么是可迭代数据类型呢?

原生具有[Symbol.iterator]属性数据类型为可迭代数据类型。如数组、类数组(如arguments、NodeList)、Set和Map。

可迭代对象可以通过迭代器进行遍历。

let arr = [4, 2, 1]; 
// 这就是迭代器 
let iterator = arr[Symbol.iterator](); 
console.log(iterator.next()); 
console.log(iterator.next()); 
console.log(iterator.next()); 
console.log(iterator.next()); 
// {value: 4, done: false} 
// {value: 2, done: false} 
// {value: 1, done: false} 
// {value: undefined, done: true}

因此,我们的代码可以这样来组织:

async function test() { 
  let arr = [4, 2, 1] 
  let iterator = arr[Symbol.iterator](); 
  let res = iterator.next(); 
  while(!res.done) { 
    let value = res.value; 
    console.log(value); 
    await handle(value); 
    res = iterater.next();
  } 
  console.log('结束')
} 
// 4 // 2 // 1 // 结束

多个任务成功地按顺序执行!其实刚刚的for...of循环代码就是这段代码的语法糖。

重新认识生成器

回头再看看用iterator遍历[4,2,1]这个数组的代码。

let arr = [4, 2, 1]; 
// 迭代器 
let iterator = arr[Symbol.iterator](); 
console.log(iterator.next()); 
console.log(iterator.next()); 
console.log(iterator.next()); 
console.log(iterator.next()); 
// {value: 4, done: false} 
// {value: 2, done: false} 
// {value: 1, done: false} 
// {value: undefined, done: true} 

咦?返回值有valuedone属性,生成器也可以调用 next,返回的也是这样的数据结构,这么巧?

没错,生成器本身就是一个迭代器。

既然属于迭代器,那它就可以用for...of遍历了吧?

当然没错,不信来写一个简单的斐波那契数列(50以内):

function* fibonacci(){ 
  let [prev, cur] = [0, 1]; 
  console.log(cur); 
  while(true) { 
    [prev, cur] = [cur, prev + cur]; 
    yield cur; 
  } 
} 
for(let item of fibonacci()) { 
  if(item > 50) break; 
  console.log(item);
} 
// 1 // 1 // 2 // 3 // 5 // 8 // 13 // 21 // 34 

这就是迭代器的魅力:)同时又对生成器有了更深入的理解,没想到Generator还有这样的身份。

总结

async-await是promise的语法糖,不仅让我们书写代码时更加流畅,而且增强了代码的可读性。特别注意的是:虽然async-await 是建立在 Promise机制之上的,但是并不能取代其地位,他们两者相辅相成,息息相关。