Async:简洁优雅的异步之道

栏目: JavaScript · 发布时间: 6年前

内容简介:在异步处理方案中,目前最为简洁优雅的便是学习A函数必须有为了直观的感受A函数的魅力,下面使用

前言

在异步处理方案中,目前最为简洁优雅的便是 async 函数(以下简称A函数)。经过必要的分块包装后,A函数能使多个相关的异步操作如同同步操作一样聚合起来,使其相互间的关系更为清晰、过程更为简洁、调试更为方便。它本质是 Generator 函数的语法糖,通俗的说法是使用G函数进行异步处理的增强版。

尝试

学习A函数必须有 Promise 基础,最好还了解 Generator 函数,有需要的可查看延伸小节。

为了直观的感受A函数的魅力,下面使用 Promise 和A函数进行了相同的异步操作。该异步的目的是获取用户的留言列表,需要分页,分页由后台控制。具体的操作是:先获取到留言的总条数,再更正当前需要显示的页数(每次切换到不同页时,总数目可能会发生变化),最后传递参数并获取到相应的数据。

let totalNum = 0; // Total comments number.
let curPage = 1; // Current page index.
let pageSize = 10; // The number of comment displayed in one page.

// 使用A函数的主代码。
async function dealWithAsync() {
  totalNum = await getListCount();
  console.log('Get count', totalNum);
  if (pageSize * (curPage - 1) > totalNum) {
    curPage = 1;
  }

  return getListData();
}

// 使用Promise的主代码。
function dealWithPromise() {
  return new Promise((resolve, reject) => {
    getListCount().then(res => {
      totalNum = res;
      console.log('Get count', res);
      if (pageSize * (curPage - 1) > totalNum) {
        curPage = 1;
      }

      return getListData()
    }).then(resolve).catch(reject);
  });
}

// 开始执行dealWithAsync函数。
// dealWithAsync().then(res => {
//   console.log('Get Data', res)
// }).catch(err => {
//   console.log(err);
// });

// 开始执行dealWithPromise函数。
// dealWithPromise().then(res => {
//   console.log('Get Data', res)
// }).catch(err => {
//   console.log(err);
// });

function getListCount() {
  return createPromise(100).catch(() => {
    throw 'Get list count error';
  });
}

function getListData() {
  return createPromise([], {
    curPage: curPage,
    pageSize: pageSize,
  }).catch(() => {
    throw 'Get list data error';
  });
}


function createPromise(
  data, // Reback data
  params = null, // Request params
  isSucceed = true,
  timeout = 1000,
) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      isSucceed ? resolve(data) : reject(data);
    }, timeout);
  });
}

对比 dealWithAsyncdealWithPromise 两个简单的函数,能直观的发现:使用A函数,除了有 await 关键字外,与同步代码无异。而使用 Promise 则需要根据规则增加很多包裹性的链式操作,产生了太多回调函数,不够简约。另外,这里分开了每个异步操作,并规定好各自成功或失败时传递出来的数据,近乎实际开发。

1 登堂

1.1 形式

A函数也是函数,所以具有普通函数该有的性质。不过形式上有两点不同:一是定义A函数时, function 关键字前需要有 async 关键字(意为异步),表示这是个A函数。二是在A函数内部可以使用 await 关键字(意为等待),表示会将其后面跟随的结果当成异步操作并等待其完成。

以下是它的几种定义方式。

// 声明式
async function A() {}

// 表达式
let A = async function () {};

// 作为对象属性
let o = {
  A: async function () {}
};

// 作为对象属性的简写式
let o = {
  async A() {}
};

// 箭头函数
let o = {
  A: async () => {}
};

1.2 返回值

执行A函数,会固定的返回一个 Promise 对象。

得到该对象后便可监设置成功或失败时的回调函数进行监听。如果函数执行顺利并结束,返回的P对象的状态会从等待转变成成功,并输出 return 命令的返回结果(没有则为 undefined )。如果函数执行途中失败,JS会认为A函数已经完成执行,返回的P对象的状态会从等待转变成失败,并输出错误信息。

// 成功执行案例

A1().then(res => {
  console.log('执行成功', res); // 10
});

async function A1() {
  let n = 1 * 10;
  return n;
}

// 失败执行案例

A2().catch(err => {
  console.log('执行失败', err); // i is not defined.
});

async function A2() {
  let n = 1 * i;
  return n;
}

1.3 await

只有在A函数内部才可以使用 await 命令,存在于A函数内部的普通函数也不行。

引擎会统一将 await 后面的跟随值视为一个 Promise ,对于不是 Promise 对象的值会调用 Promise.resolve() 进行转化。即便此值为一个 Error 实例,经过转化后,引擎依然视其为一个成功的 Promise ,其数据为 Error 的实例。

当函数执行到 await 命令时,会暂停执行并等待其后的 Promise 结束。如果该P对象最终成功,则会返回成功的返回值,相当将 await xxx 替换成 返回值 。如果该P对象最终失败,且错误没有被捕获,引擎会直接停止执行A函数并将其返回对象的状态更改为失败,输出错误信息。

最后,A函数中的 return x 表达式,相当于 return await x 的简写。

// 成功执行案例

A1().then(res => {
  console.log('执行成功', res); // 约两秒后输出100。
});

async function A1() {
  let n1 = await 10;
  let n2 = await new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 2000);
  });
  return n1 * n2;
}

// 失败执行案例

A2().catch(err => {
  console.log('执行失败', err); // 约两秒后输出10。
});

async function A2() {
  let n1 = await 10;
  let n2 = await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(10);
    }, 2000);
  });
  return n1 * n2;
}

2 入室

2.1 继发与并发

对于存在于JS语句( for , while 等)的 await 命令,引擎遇到时也会暂停执行。这意味着可以直接使用循环语句处理多个异步。

以下是处理继发的两个例子。A函数处理相继发生的异步尤为简洁,整体上与同步代码无异。

// 两个方法A1和A2的行为结果相同,都是每隔一秒输出10,输出三次。

async function A1() {
  let n1 = await createPromise();
  console.log('N1', n1);
  let n2 = await createPromise();
  console.log('N2', n2);
  let n3 = await createPromise();
  console.log('N3', n3);
}

async function A2() {
  for (let i = 0; i< 3; i++) {
    let n = await createPromise();
    console.log('N' + (i + 1), n);
  }
}

function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

接下来是处理并发的三个例子。A1函数使用了 Promise.all 生成一个聚合异步,虽然简单但灵活性降低了,只有都成功和失败两种情况。A3函数相对A2仅仅为了说明应该怎样配合数组的遍历方法使用 async 函数。重点在A2函数的理解上。

A2函数使用了循环语句,实际是继发的获取到各个异步值,但在总体的时间上相当并发(这里需要好好理解一番)。因为一开始创建 reqs 数组时,就已经开始执行了各个异步,之后虽然是逐一继发获取,但总花费时间与遍历顺序无关,恒等于耗时最多的异步所花费的时间(不考虑遍历、执行等其它的时间消耗)。

// 三个方法A1, A2和A3的行为结果相同,都是在约一秒后输出[10, 10, 10]。

async function A1() {
  let res = await Promise.all([createPromise(), createPromise(), createPromise()]);
  console.log('Data', res);
}

async function A2() {
  let res = [];
  let reqs = [createPromise(), createPromise(), createPromise()];
  for (let i = 0; i< reqs.length; i++) {
    res[i] = await reqs[i];
  }
  console.log('Data', res);
}

async function A3() {
  let res = [];
  let reqs = [9, 9, 9].map(async (item) => {
    let n = await createPromise(item);
    return n + 1;
  });
  for (let i = 0; i< reqs.length; i++) {
    res[i] = await reqs[i];
  }
  console.log('Data', res);
}

function createPromise(n = 10) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(n);
    }, 1000);
  });
}

2.2 错误处理

一旦 await 后面的 Promise 转变成 rejected ,整个 async 函数便会终止。然而很多时候我们不希望因为某个异步操作的失败,就终止整个函数,因此需要进行合理错误处理。注意,这里所说的错误不包括引擎解析或执行的错误,仅仅是状态变为 rejectedPromise 对象。

处理的方式有两种:一是先行包装 Promise 对象,使其始终返回一个成功的 Promise 。二是使用 try.catch 捕获错误。

// A1和A2都执行成,且返回值为10。
A1().then(console.log);
A2().then(console.log);

async function A1() {
  let n;
  n = await createPromise(true);
  return n;
}

async function A2() {
  let n;
  try {
    n = await createPromise(false);
  } catch (e) {
    n = e;
  }
  return n;
}

function createPromise(needCatch) {
  let p = new Promise((resolve, reject) => {
    reject(10);
  });
  return needCatch ? p.catch(err => err) : p;
}

2.3 实现原理

前言中已经提及,A函数是使用G函数进行异步处理的增强版。既然如此,我们就从其改进的方面入手,来看看其基于G函数的实现原理。A函数相对G函数的改进体现在这几个方面:更好的语义,内置执行器和返回值是 Promise

更好的语义。G函数通过在 function 后使用 * 来标识此为G函数,而A函数则是在 function 前加上 async 关键字。在G函数中可以使用 yield 命令暂停执行和交出执行权,而A函数是使用 await 来等待异步返回结果。很明显, asyncawait 更为语义化。

// G函数
function* request() {
  let n = yield createPromise();
}

// A函数
async function request() {
  let n = await createPromise();
}

function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

内置执行器。调用A函数便会一步步自动执行和等待异步操作,直到结束。如果需要使用G函数来自动执行异步操作,需要为其创建一个自执行器。通过自执行器来自动化G函数的执行,其行为与A函数基本相同。可以说,A函数相对G函数最大改进便是内置了自执行器。

// 两者都是每隔一秒钟打印出10,重复两次。

// A函数
A();

async function A() {
  let n1 = await createPromise();
  console.log(n1);
  let n2 = await createPromise();
  console.log(n2);
}

// G函数,使用自执行器执行。
spawn(G);

function* G() {
  let n1 = yield createPromise();
  console.log(n1);
  let n2 = yield createPromise();
  console.log(n2);
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}


function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

延伸

ES6精华:Promise Generator:JS执行权的真实操作者


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

ACM程序设计

ACM程序设计

曾棕根 / 北京大学 / 2011-4 / 34.00元

《ACM程序设计(第2版)》详细讲解了ACM国际大学生程序设计竞赛(ACM/ICPC)编程、调试方法,以及提高时间、空间性能的策略,并充分利用了C++泛型编程的高效率、规范化的特性,全部采用C++泛型编程。第1章讲解了ACM程序设计入门知识;第2章讲解了C++泛型编程的容器、迭代器和常用算法;第3章讲解了ACM程序设计的基本编程技巧;第4章讲解了50道原版ACM竞赛题的解题思路,并配有C++泛型编......一起来看看 《ACM程序设计》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具