什么是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
方法返回一个对象,表示当前数据成员的信息。这个对象具有value
和done
两个属性,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
思考
异步是什么
异步的概念就是一步一步去执行,上一个事件执行完成了,下一个事件才会执行,可以对比一下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
这个文件以后,回调函数才会执行。
回调函数的强耦合问题
var fs = require('fs');
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
//...
});
});
以读取多个文件为例,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"(callback hell)。
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,原来的语义变得很不清楚。
Generator函数的出现
协程
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(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
function* f(){
yield 1;
yield 2;
return 3;
}
async function f(){
await 1;
await 2;
await 3;
};
f();
async
函数就是将 Generator
函数的星号(*)
替换成async
,将yield
替换成await
,仅此而已。
(1)内置执行器。
Generator
函数的执行必须靠执行器(自动管理模式的封装),而async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行。
(2)更好的语义。
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
async
函数的await
命令后面,可以是 Promise
对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved
的 Promise
对象)。
(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入门
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!