JavaScript异步编程解决方案

Generator 函数

学习指导:阮一峰ES6 -- Generator函数

基本概念

  • 语法上:Generator 函数是一个状态机,封装了多个内部状态;执行 Generator 函数会返回一个遍历器对象。也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
  • 形式上:Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号*;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();

// 定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world)
// 即该函数有三个状态:hello,world 和 return 语句(结束执行)。
  • 调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
// 上述函数下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。
// 即每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,
// 直到遇到下一个yield表达式(或return语句)为止。
// 换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }

/*
解释说明:
上面代码一共调用了四次next方法。
第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。
next方法返回一个对象,它的value属性就是当前yield表达式的值hello,
done属性的值false,表示遍历还没有结束。

第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。
next方法返回的对象的value属性就是当前yield表达式的值world,
done属性的值false,表示遍历还没有结束。

第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句
(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,
就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),
done属性的值true,表示遍历已经结束。

第四次调用,此时 Generator 函数已经运行完毕,
next方法返回对象的value属性为undefined,done属性为true。
以后再调用next方法,返回的都是这个值。
*/

yield 表达式说明

  • Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
  • 遍历器对象的next方法的运行逻辑如下。
    • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
    • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
    • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
    • 如果该函数没有return语句,则返回的对象的 value 属性值为 undefined。
  • 需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* gen(){
  yield 123 + 456;
}

// yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值.

yield 表达式语法

  • yield表达式只能用在 Generator 函数里面,用在其他地方都会报错
(function (){
  yield 1;
})()
// SyntaxError: Unexpected number
  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面
function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}
  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号
function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

用法实例讲解

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};  // 错误用法
// forEach方法的参数是一个普通函数,但是在里面使用了yield表达式

// 一种修改方法是改用for循环。
var flat = function* (a) {
  var length = a.length;
  for (var i = 0; i < length; i++) {
    var item = a[i];
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)) {
  console.log(f);
}
// 1, 2, 3, 4, 5, 6

yield表达式与return语句

  • yield 表达式与return 语句既有相似之处,也有区别。
  • 相似之处在于,都能返回紧跟在语句后面的那个表达式的值。
  • 区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。
  • 一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式
  • 正常函数只能返回一个值,因为只能执行一次return;
  • Generator 函数可以返回一系列的值,因为可以有任意多个yield
  • 从另一个角度看,也可以说 Generator 生成了一系列的值。

next 方法的参数

  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}
var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

/*
上面代码先定义了一个可以无限运行的 Generator 函数f,
如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。

当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,
下一轮循环就会从-1开始递增。
 */
  • 这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。
  • 通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

/*
上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),
除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。
第三次运行Next方法的时候不带参数,所以z等于undefined,
返回对象的value属性等于5 + NaN + undefined,即NaN。

如果向next方法提供参数,返回结果就完全不一样了。
上面代码第一次调用b的next方法时,返回x+1的值6;
第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;
第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,
这时x等于5,y等于24,所以return语句的值等于42。
*/
  • 注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

for...of 循环

    1. for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}
for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

// 上面代码使用for...of循环,依次显示 5 个yield表达式的值。
// 这里需要注意,一旦next方法的返回对象的done属性为true,
// for...of循环就会中止,且不包含该返回对象,
// 所以上面代码的return语句返回的6,不包括在for...of循环之中。
  • 2)利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。
function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
  • 3)加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator属性上面
function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
  • 4)除了for...of循环以外,扩展运算符(...)解构赋值Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 扩展运算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解构赋值
let [x, y] = numbers();
x // 1
y // 2

// for...of 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

Generator函数的异步操作

function* asyncJob() {
  // ...其他代码
  var f = yield readFile(fileA);
  // ...其他代码
}

函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。
也就是说,yield命令是异步两个阶段的分界线。
协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。

异步任务的封装

var fetch = require('node-fetch');

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

// Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。
// 就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。

// 执行上述代码
var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

// 首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。
// 由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

async 函数

学习指导:阮一峰ES6 -- async函数

含义

  • async 函数是什么?一句话,它就是 Generator 函数的语法糖
// generator 函数依次读取两个文件
const fs = require('fs');
const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};
const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// async函数,就是下面这样
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

// async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
  • async函数对 Generator 函数的改进,体现在以下四点:
    • 内置执行器 —— Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
    • 更好的语义 —— async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
    • 更广的适用性 —— co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
    • 返回值是 Promise —— async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

  • 引入demo
/* async 表示异步调用 返回一个Promise对象 */
async function timeout(ms) {
    await new Promise((resolve) => {
      setTimeout(resolve, ms);
    });
}  /* 与下边等价*/

// function timeout(ms) {
//     return new Promise((resolve) => {
//       setTimeout(resolve, ms);
//     });
// }

async function asyncPrint(value, ms) {
    await timeout(ms);
    console.log(value);
}

asyncPrint('hello world', 3000)  // 3000毫秒以后,输出hello world
  • async的表达方式

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

async 的语法

  • 返回Promise 对象

    • async函数返回一个 Promise 对象。
    • async函数内部return语句返回的值,会成为then方法回调函数的参数。
    async function f() {
      return 'hello world';
    }
    
    f().then(v => console.log(v))
    // "hello world"
    // 函数 f 内部return命令返回的值,会被then方法回调函数接收到。
    
  • async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出错了
  • Promise 对象的状态变化

    • async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。
    • 也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
    async function getTitle(url) {
      let response = await fetch(url);
      let html = await response.text();
      return html.match(/<title>([\s\S]+)<\/title>/i)[1];
    }
    getTitle('https://tc39.github.io/ecma262/').then(console.log)
    // "ECMAScript 2017 Language Specification"
    
    // 函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。
    // 只有这三个操作全部完成,才会执行then方法里面的 console.log。
    
  • await 命令

    • 正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。
    async function f() {
      return await 123;
    }
    
    f().then(v => console.log(v))
    // 123
    
    // await命令的参数是数值123,它被转成 Promise 对象,并立即resolve
    
    • await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。
    async function f() {
      await Promise.reject('出错了');
    }
    
    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // 出错了
    
    // await 语句前面没有 return,但是 reject 方法的参数依然传入了 catch 方法的回调函数。
    // 这里如果在await前面加上return,效果是一样的。
    
    • 只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行
    async function f() {
      await Promise.reject('出错了');
      await Promise.resolve('hello world'); // 不会执行
    }
    
    • 可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。
    async function f() {
      try {
        await Promise.reject('出错了');
      } catch(e) {
      }
      return await Promise.resolve('hello world');
    }
    
    f()
    .then(v => console.log(v))
    // hello world
    
    • 另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误
    async function f() {
      await Promise.reject('出错了')
        .catch(e => console.log(e));
      return await Promise.resolve('hello world');
    }
    
    f()
    .then(v => console.log(v))
    // 出错了
    // hello world
    

使用注意点

  • 第一点,前面已经说过,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
  • 第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
// 上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。
// 这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。


// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
// getFoo和getBar都是同时触发,这样就会缩短程序的执行时间
  • 第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 报错
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}
// 上面代码会报错,因为await用在普通函数之中了

// 采用for循环
async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

// 如果确实希望多个请求并发执行,可以使用Promise.all方法。
// 当三个请求都会resolved时,下面两种写法效果相同。
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的写法
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

async函数实现原理

  • async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里
async function fn(args) {
  // ...
}

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

// 所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

场景再现

  • 假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
async function chainAnimationsAsync(elem, animations) {
  let ret = null;  // 变量ret用来保存上一个动画的返回值
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略错误,继续执行 */
  }
  return ret;
}
  • 依次远程读取一组 URL,然后按照读取的顺序输出结果。
// 远程操作继发.只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

// 并发发出远程请求
async function logInOrder(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);
  }
}
posted @ 2018-05-16 16:59  MarlonChiu  阅读(308)  评论(0)    收藏  举报