资讯专栏INFORMATION COLUMN

ES6 系列之我们来聊聊 Async

Songlcy / 3373人阅读

摘要:标准引入了函数,使得异步操作变得更加方便。在异步处理上,函数就是函数的语法糖。在实际项目中,错误处理逻辑可能会很复杂,这会导致冗余的代码。的出现使得就可以捕获同步和异步的错误。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

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);

因为 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 () => {
  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 吗?

async 函数返回一个 Promise 对象

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

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

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

参考

(译) 6 个 Async/Await 优于 Promise 的方面

(译) 如何逃离 async/await 地狱

精读《async/await 是把双刃剑》

ECMAScript 6 入门

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,对作者也是一种鼓励。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98642.html

相关文章

  • ES6 完全使用手册

    摘要:前言这里的泛指之后的新语法这里的完全是指本文会不断更新这里的使用是指本文会展示很多的使用场景这里的手册是指你可以参照本文将项目更多的重构为语法此外还要注意这里不一定就是正式进入规范的语法。 前言 这里的 ES6 泛指 ES5 之后的新语法 这里的 完全 是指本文会不断更新 这里的 使用 是指本文会展示很多 ES6 的使用场景 这里的 手册 是指你可以参照本文将项目更多的重构为 ES6...

    kgbook 评论0 收藏0
  • 前端进阶资源整理

    摘要:前端进阶进阶构建项目一配置最佳实践状态管理之痛点分析与改良开发中所谓状态浅析从时间旅行的乌托邦,看状态管理的设计误区使用更好地处理数据爱彼迎房源详情页中的性能优化从零开始,在中构建时间旅行式调试用轻松管理复杂状态如何把业务逻辑这个故事讲好和 前端进阶 webpack webpack进阶构建项目(一) Webpack 4 配置最佳实践 react Redux状态管理之痛点、分析与...

    BlackMass 评论0 收藏0
  • javascript异步与promise

    摘要:到这里,我已经发出了一个请求买汉堡,启动了一次交易。但是做汉堡需要时间,我不能马上得到这个汉堡,收银员给我一个收据来代替汉堡。到这里,收据就是一个承诺保证我最后能得到汉堡。 同期异步系列文章推荐谈一谈javascript异步javascript异步中的回调javascript异步之Promise.all()、Promise.race()、Promise.finally()javascr...

    rollback 评论0 收藏0
  • ES6 系列我们聊聊装饰器

    摘要:第二部分源码解析接下是应用多个第二部分对于一个方法应用了多个,比如会编译为在第二部分的源码中,执行了和操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。有了装饰器,就可以改写上面的代码。 Decorator 装饰器主要用于: 装饰类 装饰方法或属性 装饰类 @annotation class MyClass { } function annotation(ta...

    eternalshallow 评论0 收藏0
  • ES6 系列我们聊聊 Promise

    前言 Promise 的基本使用可以看阮一峰老师的 《ECMAScript 6 入门》。 我们来聊点其他的。 回调 说起 Promise,我们一般都会从回调或者回调地狱说起,那么使用回调到底会导致哪些不好的地方呢? 1. 回调嵌套 使用回调,我们很有可能会将业务代码写成如下这种形式: doA( function(){ doB(); doC( function(){ ...

    MycLambert 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<