33

Async:简洁优雅的异步之道

 5 years ago
source link: https://segmentfault.com/a/1190000016212269?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

在异步处理方案中,目前最为简洁优雅的便是 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执行权的真实操作者


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK