Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 系列之我们来聊聊 Async #100

Open
mqyqingfeng opened this issue Oct 23, 2018 · 13 comments
Labels

Comments

@mqyqingfeng
Copy link
Owner

@mqyqingfeng mqyqingfeng commented Oct 23, 2018

async

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

在异步处理上,async 函数就是 Generator 函数的语法糖。

举个例子:

// 使用 generator
var fetch = require('node-fetch');
var co = require('co');

function* gen() {
    var r1 = yield fetch('https://api.github.com/users/github');
    var json1 = yield r1.json();
    console.log(json1.bio);
}

co(gen);

当你使用 async 时:

// 使用 async
var fetch = require('node-fetch');

var fetchData = async function () {
    var r1 = await fetch('https://api.github.com/users/github');
    var json1 = await r1.json();
    console.log(json1.bio);
};

fetchData();

其实 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

spawn 函数指的是自动执行器,就比如说 co。

再加上 async 函数返回一个 Promise 对象,你也可以理解为 async 函数是基于 Promise 和 Generator 的一层封装。

async 与 Promise

严谨的说,async 是一种语法,Promise 是一个内置对象,两者并不具备可比性,更何况 async 函数也返回一个 Promise 对象……

这里主要是展示一些场景,使用 async 会比使用 Promise 更优雅的处理异步流程。

1. 代码更加简洁

/**
 * 示例一
 */
function fetch() {
  return (
    fetchData()
    .then(() => {
      return "done"
    });
  )
}

async function fetch() {
  await fetchData()
  return "done"
};
/**
 * 示例二
 */
function fetch() {
  return fetchData()
  .then(data => {
    if (data.moreData) {
        return fetchAnotherData(data)
        .then(moreData => {
          return moreData
        })
    } else {
      return data
    }
  });
}

async function fetch() {
  const data = await fetchData()
  if (data.moreData) {
    const moreData = await fetchAnotherData(data);
    return moreData
  } else {
    return data
  }
};
/**
 * 示例三
 */
function fetch() {
  return (
    fetchData()
    .then(value1 => {
      return fetchMoreData(value1)
    })
    .then(value2 => {
      return fetchMoreData2(value2)
    })
  )
}

async function fetch() {
  const value1 = await fetchData()
  const value2 = await fetchMoreData(value1)
  return fetchMoreData2(value2)
};

2. 错误处理

function fetch() {
  try {
    fetchData()
      .then(result => {
        const data = JSON.parse(result)
      })
      .catch((err) => {
        console.log(err)
      })
  } catch (err) {
    console.log(err)
  }
}

在这段代码中,try/catch 能捕获 fetchData() 中的一些 Promise 构造错误,但是不能捕获 JSON.parse 抛出的异常,如果要处理 JSON.parse 抛出的异常,需要添加 catch 函数重复一遍异常处理的逻辑。

在实际项目中,错误处理逻辑可能会很复杂,这会导致冗余的代码。

async function fetch() {
  try {
    const data = JSON.parse(await fetchData())
  } catch (err) {
    console.log(err)
  }
};

async/await 的出现使得 try/catch 就可以捕获同步和异步的错误。

3. 调试

const fetchData = () => new Promise((resolve) => setTimeout(resolve, 1000, 1))
const fetchMoreData = (value) => new Promise((resolve) => setTimeout(resolve, 1000, value + 1))
const fetchMoreData2 = (value) => new Promise((resolve) => setTimeout(resolve, 1000, value + 2))

function fetch() {
  return (
    fetchData()
    .then((value1) => {
      console.log(value1)
      return fetchMoreData(value1)
    })
    .then(value2 => {
      return fetchMoreData2(value2)
    })
  )
}

const res = fetch();
console.log(res);

promise 断点演示

因为 then 中的代码是异步执行,所以当你打断点的时候,代码不会顺序执行,尤其当你使用 step over 的时候,then 函数会直接进入下一个 then 函数。

const fetchData = () => new Promise((resolve) => setTimeout(resolve, 1000, 1))
const fetchMoreData = () => new Promise((resolve) => setTimeout(resolve, 1000, 2))
const fetchMoreData2 = () => new Promise((resolve) => setTimeout(resolve, 1000, 3))

async function fetch() {
  const value1 = await fetchData()
  const value2 = await fetchMoreData(value1)
  return fetchMoreData2(value2)
};

const res = fetch();
console.log(res);

async 断点演示

而使用 async 的时候,则可以像调试同步代码一样调试。

async 地狱

async 地狱主要是指开发者贪图语法上的简洁而让原本可以并行执行的内容变成了顺序执行,从而影响了性能,但用地狱形容有点夸张了点……

例子一

举个例子:

(async () => {
  const getList = await getList();
  const getAnotherList = await getAnotherList();
})();

getList() 和 getAnotherList() 其实并没有依赖关系,但是现在的这种写法,虽然简洁,却导致了 getAnotherList() 只能在 getList() 返回后才会执行,从而导致了多一倍的请求时间。

为了解决这个问题,我们可以改成这样:

(async () => {
  const listPromise = getList();
  const anotherListPromise = getAnotherList();
  await listPromise;
  await anotherListPromise;
})();

也可以使用 Promise.all():

(async () => {
  Promise.all([getList(), getAnotherList()]).then(...);
})();

例子二

当然上面这个例子比较简单,我们再来扩充一下:

(async () => {
  const listPromise = await getList();
  const anotherListPromise = await getAnotherList();

  // do something

  await submit(listData);
  await submit(anotherListData);

})();

因为 await 的特性,整个例子有明显的先后顺序,然而 getList() 和 getAnotherList() 其实并无依赖,submit(listData) 和 submit(anotherListData) 也没有依赖关系,那么对于这种例子,我们该怎么改写呢?

基本分为三个步骤:

1. 找出依赖关系

在这里,submit(listData) 需要在 getList() 之后,submit(anotherListData) 需要在 anotherListPromise() 之后。

2. 将互相依赖的语句包裹在 async 函数中

async function handleList() {
  const listPromise = await getList();
  // ...
  await submit(listData);
}

async function handleAnotherList() {
  const anotherListPromise = await getAnotherList()
  // ...
  await submit(anotherListData)
}

3.并发执行 async 函数

async function handleList() {
  const listPromise = await getList();
  // ...
  await submit(listData);
}

async function handleAnotherList() {
  const anotherListPromise = await getAnotherList()
  // ...
  await submit(anotherListData)
}

// 方法一
(async () => {
  const handleListPromise = handleList()
  const handleAnotherListPromise = handleAnotherList()
  await handleListPromise
  await handleAnotherListPromise
})()

// 方法二
(async () => {
  Promise.all([handleList(), handleAnotherList()]).then()
})()

继发与并发

问题:给定一个 URL 数组,如何实现接口的继发和并发?

async 继发实现:

// 继发一
async function loadData() {
  var res1 = await fetch(url1);
  var res2 = await fetch(url2);
  var res3 = await fetch(url3);
  return "whew all done";
}
// 继发二
async function loadData(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

async 并发实现:

// 并发一
async function loadData() {
  var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
  return "whew all done";
}
// 并发二
async function loadData(urls) {
  // 并发读取 url
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

async 错误捕获

尽管我们可以使用 try catch 捕获错误,但是当我们需要捕获多个错误并做不同的处理时,很快 try catch 就会导致代码杂乱,就比如:

async function asyncTask(cb) {
    try {
       const user = await UserModel.findById(1);
       if(!user) return cb('No user found');
    } catch(e) {
        return cb('Unexpected error occurred');
    }

    try {
       const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
    } catch(e) {
        return cb('Error occurred while saving task');
    }

    if(user.notificationsEnabled) {
        try {
            await NotificationService.sendNotification(user.id, 'Task Created');
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    if(savedTask.assignedUser.id !== user.id) {
        try {
            await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    cb(null, savedTask);
}

为了简化这种错误的捕获,我们可以给 await 后的 promise 对象添加 catch 函数,为此我们需要写一个 helper:

// to.js
export default function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}

整个错误捕获的代码可以简化为:

import to from './to.js';

async function asyncTask() {
     let err, user, savedTask;

     [err, user] = await to(UserModel.findById(1));
     if(!user) throw new CustomerError('No user found');

     [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
     if(err) throw new CustomError('Error occurred while saving task');

    if(user.notificationsEnabled) {
       const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
       if (err) console.error('Just log the error and continue flow');
    }
}

async 的一些讨论

async 会取代 Generator 吗?

Generator 本来是用作生成器,使用 Generator 处理异步请求只是一个比较 hack 的用法,在异步方面,async 可以取代 Generator,但是 async 和 Generator 两个语法本身是用来解决不同的问题的。

async 会取代 Promise 吗?

  1. async 函数返回一个 Promise 对象

  2. 面对复杂的异步流程,Promise 提供的 all 和 race 会更加好用

  3. Promise 本身是一个对象,所以可以在代码中任意传递

  4. async 的支持率还很低,即使有 Babel,编译后也要增加 1000 行左右。

参考

  1. [译] 6 个 Async/Await 优于 Promise 的方面
  2. [译] 如何逃离 async/await 地狱
  3. 精读《async/await 是把双刃剑》
  4. ECMAScript 6 入门
  5. How to write async await without try-catch blocks in Javascript

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@jslixiaolin

This comment has been minimized.

Copy link

@jslixiaolin jslixiaolin commented Oct 29, 2018

消灭零评论 ^_^

@ysc123

This comment has been minimized.

Copy link

@ysc123 ysc123 commented Jan 3, 2019

如果 await 后面跟的不是 promise ,那么是不是,就不是所谓的顺序执行了!

@KaiOrange

This comment has been minimized.

Copy link

@KaiOrange KaiOrange commented Jan 4, 2019

最后2个"async 会取代 Promise 吗?" 看着怪怪的

@ZYSzys

This comment has been minimized.

Copy link

@ZYSzys ZYSzys commented Jan 4, 2019

@mqyqingfeng 想问一下题主,async/await 到底应该算是 ES7(2016) 还是 ES8 (2017) 呢?

网上有说是 ES7 的,也有说是 ES8 的 :-)

@chnliquan

This comment has been minimized.

Copy link

@chnliquan chnliquan commented Jan 18, 2019

如果 await 后面跟的不是 promise ,那么是不是,就不是所谓的顺序执行了!

如果 await 后面的 x 不是 promise 会转换为 promise.resolve(x),也就是还是返回的 promise

@mailylqj

This comment has been minimized.

Copy link

@mailylqj mailylqj commented Apr 2, 2019

@mqyqingfeng 想问一下题主,async/await 到底应该算是 ES7(2016) 还是 ES8 (2017) 呢?

网上有说是 ES7 的,也有说是 ES8 的 :-)

ES8 (2017) 在babel官网有

@heyunjiang

This comment has been minimized.

Copy link

@heyunjiang heyunjiang commented Apr 11, 2019

// 并发二
async function loadData(urls) {
  // 并发读取 url
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

map 循环里是不是不该加 await ,才能实现并发读取

@Yanhua67

This comment has been minimized.

Copy link

@Yanhua67 Yanhua67 commented Apr 22, 2019

@heyunjiang 都是对的,都可以实现。

@lizhongzhen11

This comment has been minimized.

Copy link

@lizhongzhen11 lizhongzhen11 commented Jun 29, 2019

async看了有三遍了,一直忘,总是感觉很奇怪下面这段代码

async function setTime(ms) {
  await setTimeout(() => {console.log('aaaaaaaaa')}, ms)
}

async function print(val, ms) {
  await setTime(ms)
  console.log(val)
}
print('vvvvvvvv', 3000);
// vvvvvvvv
// Promise{<resolved>: undefined}
// aaaaaaaaa

像上面的代码,为何直接await setTimeout,print函数没有等setTimeout执行完毕后再打印?
请各位大佬解解惑哈~

好吧,还是我自己不够理解啊。。。
await后面如果跟一个promise对象,那么会等该promise resolve后才会继续下面的执行,但是,我这里的代码,直接await setTimeout,不是一个promise,也就不会等它执行完才会执行下面的代码了。

async function test() {
  const tt = await setTimeout(() => {console.log(333)}, 3000)
  console.log(444)
  console.log(tt)
}
test()
// 444
// 562
// Promise{<resolved>: undefined}
// 333
@CommanderXL

This comment has been minimized.

Copy link

@CommanderXL CommanderXL commented Jul 10, 2019

@lizhongzhen11 这个地方你可以想象下,await 后面直接接个异步的函数,比如你例子当中的 setTimeout 方法,那后面的程序什么时候知道你这个异步的方法被执行完了呢?promise 就不一样了,虽然也是异步执行,但是这个 promise 被 resolve 后就表明这个 promise (异步任务)执行结束了。那么就可以开始执行下面的方法了。

@rongda

This comment has been minimized.

Copy link

@rongda rongda commented Jul 16, 2019

async function test() {
  const tt = await setTimeout(() => {console.log(333)}, 3000)
  console.log(444)
  console.log(tt)
}
test()
// 444
// 562
// Promise{<resolved>: undefined}
// 333

不是promise会转为promise,resolved后就继续往下执行了

async function test() {
  const tt = await Promise.resolve(setTimeout(() => {console.log(333)}, 3000))
  console.log(444)
  console.log(tt)
}
test()
@mananl

This comment has been minimized.

Copy link

@mananl mananl commented Jul 16, 2019

看完后感觉理解的又深了不少~很赞

@EmilyYoung71415

This comment has been minimized.

Copy link

@EmilyYoung71415 EmilyYoung71415 commented Aug 28, 2019

== 0 ==
async function test() {
    await setTimeout(() =>console.log(111), 3000)
    console.log(222)
}// 222 111
// 问题提出:为啥await没有起作用?await不是返回一个promise,当promise决议后才下一步吗?

== 1 ==
async function test() {
    await new Promise(resolve => setTimeout(()=>console.log(111), 1000))
    // 如果没有resolve 就不会往下执行 
    console.log(222)// 未执行
}// 111

== 2 ==
async function test() {
    await new Promise(resolve => setTimeout(()=>resolve(console.log(111)), 1000))
    console.log(222)
}// 111 222 正常版本

== 3 ==
async function test() {
    await Promise.resolve(setTimeout(()=>console.log(111),500));
    console.log(222)
}// 222 111 因为 await后面是一个已经resolve的promise 所以直接下一步

== 4 ==
async function test() {
    await Promise.all([setTimeout(()=>console.log(111),500)]);
    console.log(222)
}// 222 111  当all的数组元素不是promise的时候 会调用Promise.resolve方法包装 产生与3一样结果

== 5 ==
async function test(){
    await new _Promise(resolve=>setTimeout(()=>resolve(console.log(111)),1000));
    console.log(222)
}// 111 222 _Promise是自己模拟的promise

由上,
1、await后跟的函数调用立即被调用(虽然听起来很像废话..

async function test() {
    await setTimeout(()=>console.log(1),1000);
    await setTimeout(()=>console.log(2),500);
    await console.log(3)
}// 3 2 1

但并不代表执行全是同步的,请看下列代码的输出:

async function test() {
    console.log(1)
    await console.log(2)
    console.log(3)
}
console.log(4)
// 1 3 4 2

因为归根结底 await后面的普通函数实际上看成由Promise.resolve包裹后执行的

async function test() {
    console.log(1)
    await Promise.resolve(console.log(2));
    console.log(3)
}
console.log(4)
// 1 3 4 2

2、await后面是promise对象且promise未被决议才会产生阻塞
(如果是自己写的promise也会产生和Promise一样的效果)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.