JS-Generator+async

613 阅读8分钟

什么是Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

这里需要补充一下遍历器对象(Iterator Object)

Iterator

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
next方法返回一个对象,表示当前数据成员的信息。这个对象具有valuedone两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++]} :
        {done: true};
    }
  };
};
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性,另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
原生具备 Iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

next 方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
yield表达式如果用在另一个表达式之中,必须放在圆括号里面;yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

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);                                 // 带参 x=5
b.next() // { value:6, done:false }             // yield (x + 1) x+1 = 5+1
b.next(12) // { value:8, done:false }           // 带参 (yield (x + 1))= 12
b.next(13) // { value:42, done:true }           // 带参 (yield (y / 3))= 13

for...of 循环

下面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

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

注意:

next()是对应yield后面表达式的,如果在for...of后在使用已经没有对应的yield了,所以会{ value: undefined, done: true },但是先next()for...of就只会根据剩余yield做显示

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

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

next()在for...of后

let arrs = flat(arr);
for (var f of arrs) {
  console.log(f);
}; 
console.log(arrs.next());
// 1 2 3 4 5 6 { value: undefined, done: true }

next()在for...of前

let arrs = flat(arr);
console.log(arrs.next());
for (var f of arrs) {
  console.log(f);
}; 
// { value:1, done: false } 2 3 4 5 6 

斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

数组

 function list(max){
	 let arr = [];
	 let a = 0;
	 let b = 1;
	 while(arr.length<max){
		 arr.push(a);
		 [a,b] = [b,a+b];
	 } 
	 return arr;
 }
 console.log(list(5));// [0,1,1,2,3]

generator函数

 function* list(max){
	let a = 0;
	let b = 1;
	let n = 0;
	while(n<max){
	  yield a;
	  [a,b] = [b,a+b];
	  n++;
	};
	return;
  };
  var arr = list(5);
  for (let item of arr) {
  	console.log(item);
  }
// 0 1 1 2 3

思考

Generator函数好在哪里?回归本质:函数是 ES6 提供的一种异步编程解决方案。 由斐波那契数列为例,普通构造函数跟Generator函数都能实现业务,但是区别在于:普通函数返回的>是数组;Generator函数是从上而下一步一步执行的用next()方法就会很明显。

异步是什么

异步的概念就是一步一步去执行,上一个事件执行完成了,下一个事件才会执行,可以对比一下if条件,异步的判断条件就是上一个事件是否执行完成。

异步编程的实现:回调函数

JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

var fs = require('fs');
fs.readFile('./zf.js', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log('data',data);
});

上面代码中,readFile函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了./zf.js这个文件以后,回调函数才会执行。

一个有趣的问题是,为什么 Node 约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是null)?

原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。

回调函数的强耦合问题

var fs = require('fs');
fs.readFile(fileA, 'utf-8', function (err, data) {
    fs.readFile(fileB, 'utf-8', function (err, data) {
        //...
    });
});

以读取多个文件为例,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"(callback hell)。

Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。

fs-readfile-promise模块,它的作用就是返回一个 Promise 版本的readFile函数。Promise 提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

Promise的最大问题

Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

多余执行的冗余:如在某段程序的函数中,出现的语句,在对返回的参数没有任何的影响,但是又执行了多次,是为多余执行,此冗余是对CPU的消耗,应该杜绝该种冗余,应该注释掉。

代码数量的冗余:代码中太多的注释,或者一些没有使用到的变量,函数而存在程序中,这种冗余会让代码的可读性降低

Generator函数的出现

ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同

协程

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下。

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
  • 第三步,(一段时间后)协程B交还执行权。
  • 第四步,协程A恢复执行。 上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
function* asyncJob() {
  // ...其他代码
  var f = yield readFile(fileA);
  // ...其他代码
}

上面代码的函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

Generator 函数的数据交换和错误处理

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

next返回值的value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

function* gen(x){
  try {
    yield;
  } catch (e){
    console.log('内部捕获',e);
  }
}

var g = gen(1);

g.next()
try {
  g.throw('a');
  g.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

上面代码,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

Generator的自动管理

function run(fn) {
  var gen = fn();
  function next(data) {
	  console.log('自动循环',data)
    var result = gen.next(data);
	console.log(result.value)
    if (result.done) return;
    next(result.value)
  };
  next();
};
function* f(){
	yield 1;
	yield 2;
	return 3;
}
console.log('同步执行开始');
run(f);
console.log('执行结束')

async...await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。

function* f(){
	yield 1;
	yield 2;
	return 3;
}
async function f(){
	await 1;
	await 2;
        await 3;
};
f();

async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点:

  1. 内置执行器
  2. 更好的语义
  3. 更广的适用性
  4. 返回值是 Promise

(1)内置执行器。
Generator 函数的执行必须靠执行器(自动管理模式的封装),而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

(2)更好的语义。
asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。
async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolvedPromise 对象)。

(4)返回值是 Promise。 async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

系统检验

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

let num = 10
async function foo() {
	console.log('async:'+await new Promise(res => {
		setTimeout(() => console.log('async-set'+ ++num),0)
		num++;
		res(num);
		num++
	}));
	console.log('async2:'+num)
}
console.log('1',++num)
console.log('2',num++)
setTimeout(()=>console.log('set1'+num),0)
console.log('c1:'+num)
foo()
console.log('c2:'+num)
setTimeout(()=>console.log('set2:'+num),0)

参见

1. 阮一峰-es6入门