同步应用
简介
基本概念
Generator函数式ES6提供的一种异步编程解决方案,语法行为和传统函数完全不同.
语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数.返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态
形式上,Generator函数是一个普通函数,但是有两个特征.一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
(产出)表达式,定义不同的内部状态.
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
调用Generator函数,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象.
下一步必须调用遍历器对象的next
方法,使得指针移向下一个状态.换言之,Generator函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行.
每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象.
yield表达式
yield
表达式就是暂停标志
遍历器对象的next方法运行逻辑:
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟着yield
后面的那个表达式的值作为返回的对象的value
属性值 - 下一次调用
next
方法时,再继续向下执行,知道遇到yield
表达式 - 如果没有遇到新的
yield
表达式,就一直运行到函数结束,知道return语句为止.并将return语法后面的表达式的值,作为返回的对象的value
值 - 如果该函数没有
return
语句 ,则返回的对象的value
属性值为undefined
需要注意的是,yield
表达式后面的表达式,只有调用next
方法,内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的"惰性求职"的语法功能
Generator函数可以不用yield
表达式,这时就变成了一个单纯的暂缓执行函数
需要注意,yield
表达式只能用在Generator函数里面.另外,yield
表达式如果用在另一个表达式之中,必须放在圆括号里面.
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号.
与Iterator接口的关系
任意一个对象的Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator
属性,从而使得该对象具有Iterator接口.
Generator函数执行后,返回一个遍历器对象,该对象本身也具有Symbol.iterator
属性,执行后返回自身
next方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
.next
方法可以带一个参数,该参数就会被当做上一个yield
表达式的返回值.
这个功能有很重要的语法意义.Generator函数从暂停状态到恢复运行,它的上下文状态是不变的.通过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
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的.只有从第二次使用next
方法开始,参数才是有效的
如果想要在第一次调用next
方法时,就能输入值,可以在generator函数外面再包一层
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!')
// First input: hello!
for...of循环
可以自动遍历Generator函数时生成的Iterator
对象,且此时不再需要调用next
方法.
这里需要注意:一旦next
方法的返回对象的done
属性,for...of
循环就会中止,且不包含该返回对象.
利用Generator函数和for...of
循环,实现斐波那契数列:
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
利用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
加上遍历器接口的另一种写法是,将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
除了for...of
循环以外,拓展运算符(...
)解构赋值和Array.from
方法内部调用的,都是遍历器接口.他们都可以讲Generator函数返回的Iterator对象作为参数
Generator.prototype.throw
Generator函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在Generator函数体内捕获
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
第一个错误被Generator函数体内的catch
语句捕获.第二次抛出错误,由于Generator函数内部的catch
语句已经被执行过了,不会再捕捉到这个错误了,所以被函数体外的catch
语句捕获.
throw
方法可以接受一个参数,该参数会被catch
语句接受,建议抛出Error
对象的实例.
注意区分遍历器对象的throw
方法和全局的throw
命令.
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('内部捕获', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
上面代码之所以只捕获了a,是因为函数体外的catch
语句块捕获了抛出的a错误以后,就不会再继续try
代码块里面剩余的语句了.
如果Generator函数内部没有部署try...catch
代码块,那么throw
方法抛出的错误,将被外部try...catch
代码块捕获.
如果内外部都没有部署try...catch
代码块,那么程序将报错中断执行.
throw
方法被捕获以后,会附带执行下一条yield
表达式,也即是说,会附带执行一次next
方法.
只要Generator函数内部部署了try...catch
代码块,那么遍历器的throw
方法抛出的错误,不影响下一次遍历.
另外,throw
命令和遍历器中的throw
方法是无关的,两者互不影响.
Generator函数体外抛出的错误可以在函数体内捕获;反过来,Generator函数体内抛出的错误,也可以被函数体外的catch
捕获
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
//会有报错信息,数值没有toUpperCase方法
}
一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了.如果此后还调用next
方法,将返回一个value属性等于undefined
,done属性等于true
的对象.
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
Generator.prototype.return
可以返回给定的值,并且终结遍历Generator函数.
如果return
方法调用时,不提供参数,则返回值的value
属性为undefined
.
如果Generator函数内部有try...finally
代码块,那么return
方法会推迟到finally
代码块执行完再执行
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
next,throw,return的共同点
本质上是同一件事,它们的作用都是让Generator函数恢复执行,并且使用各不同的语句替换yield
表达式
next
是将yield
表达式替换成一个值
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
throw
是将yield
表达式替换成一个throw
语句
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return
是将yield
表达式替换成一个return`语句
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
yield*表达式
如果在Generator函数内部,调用另一个Generator函数,默认情况下是没有效果的.这里就需要用到yield*
表达式,用来在一个Generator函数里面执行另一个Generator函数.
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
yield*
后面的Generator函数(没有return
语句时),等同于在Generator函数内部,部署一个for...of
循环
如果yield*
后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代码中,如果yield
命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象.
如果被代理的Generator函数有return
语句,那么就可以向代理它的Generator函数返回数据.
yield*
命令可以很方便的取出嵌套数组的所有成员
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
作为对象属性的Generator函数
//简写形式
let obj = {
* myGeneratorMethod() {
···
}
};
//等同于
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator函数的this
Generator函数总数返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype
对象上的方法.
Generator函数内部在this
对象上添加一个属性,但是返回的遍历器对象拿不到这个属性.Generator函数也不能跟new
命令一起用,会报错.
让Generator函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this
的方法:
首先,生成一个空对象,使用call
方法绑定Generator函数内部的this
.这样,构造函数调用以后,这个空对象就是Generator函数的实例对象了.
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
上面代码中,执行的是遍历器对象f,但是生成的对象实例是obj,将这个两个对象统一的办法:
将obj换成F.prototype
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
再将F改成构造函数,就可以对它执行new
命令了
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
含义
Generator和状态机
//ES5
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
//ES6
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
上面的Generator实现与ES5实现对比,可以看到少了用来保存状态的外部变量,更符合函数式编程的思想.
Generator与协程
协程是一种程序运行的方式,可以理解为"协作的线程"或"协作的函数".协程既可以用单线程实现,也可以用多线程实现.前者是一种特殊的子例程,后者是一种特殊的线程.
- 协程与子例程的差异
可以并行执行,交换执行权的线程(或函数),就称为协程.
从实现上看,在内存中,子例程只使用一个栈,而协程是同事存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行. - 协程与普通线程的差异
普通的线程是抢先式的,到底哪个线程优先得到资源,必须有运行环境决定,但是协程是合作式的,执行权有协程自己分配.
Generator函数是ES6对于协程的实现,但属于不完全实现.因为只有Generator函数的调用者才能将程序的执行权还给Generator函数.如果是完全执行的协程,任何函数都可以让暂停的协程继续之心.
Generator与上下文
Javascript执行函数(或块级代码)的时候,会在当前上下文环境的上层产生一个函数运行的上下文,变成当前的上下文,由此形成一个上下文环境的堆栈.这个堆栈是"后进先出"的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空
Generator函数不是这样,它执行产生的上下文环境,一旦遇到yield
命令,就会暂时退出堆栈,但是并不小时,里面的所有变量和对象会冻结在当前状态.等到对它执行next
命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行.
应用
- 异步操作的同步化表达
Ajax是典型的异步操作,通过Generator函数部署Ajax操作:
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
//回调成功之后再调用next方法,并传入参数赋值给result
});
}
var it = main();
it.next();
通过Generator函数逐行读取文本文件
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
控制流管理
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
Promise改写
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
Generator函数改写
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
然后使用一个函数按次序自动执行所有步骤
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
这种做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤.
for...of
的本质是一个while
循环.
部署Iterator接口
利用Generator函数可以在任意对象上部署Iterator接口
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
作为数据结构
可以看做一个数组结构,因为Generator函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口.
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
for (task of doStuff()) {
// task是一个函数,可以像回调函数那样使用它
}
异步应用
传统方法
ES6诞生以前,异步编程方法大概一下四种:
- 回调函数
- 事件监听
- 发布/订阅
- Promise对象
基本概念
Promise
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
如果依次读取两个以上的文件,就会出现多重嵌套.因为多个异步操作形成了抢耦合,只要有一个操作需要修改,她的上层回调函数和下层回调函数可能都要跟着修改.这种情况成为"回调函数地狱".
Promise就是为了解决这个问题而提出的,它是一种新的写法,允许将回调函数的嵌套,改为链式调用.采用Promise,连续读取多个文件:
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包装一下,不管什么操作都是一堆then
,原来的语义变得不清楚
Generator函数
异步任务的封装
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格式的数据解析信息
//执行这段代码
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函数将异步操作表示的很简介,但是流程管理却不方便.
Thunk函数
Thunk函数是自动执行Generator函数的一种方法
参数的求值策略
- "传值调用"
在进入函数体之前,就计算参数表达式的值 - "传名调用"
直接将表达式传入函数体,只有在用到的时候求值
Thunk函数的含义
编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体.这个临时函数就叫做Thunk函数.
这就是Thunk函数的定义,它是"传名调用"的一种实现策略,用来替换某个表达式.
JavaScript语言的Thunk函数
JavaScript语言是传值调用的e,它的Thunk函数含义有所不同.在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数.
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
任何参数,只要参数有回调函数,就能写成Thunk函数的形式.下面是一个简单的Thunk函数转换器:
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
Thunkify模块
生产环境的转换器,建议使用Thunkify模块
使用方法:
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
Thunkify的源码与上一节那个简单的转换器非常像
function thunkify(fn) {
return function() {
var args = new Array(arguments.length);
var ctx = this;
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function (done) {
var called;
args.push(function () {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
主要多了一个检查机制,变量called
确保回调函数只运行一次.这样的设计与下文的Generator函数相关.下面例子:
function f(a, b, callback){
var sum = a + b;
callback(sum);
callback(sum);
}
var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3
由于thunkify
只允许回调函数执行一次,所以只输出一行结果.
Generator函数的流程管理
Generator函数可以自动执行
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
但是,这不适合异步操作.以读取文件为例,下面的Generator函数封装了两个异步操作
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
上面代码中,yield
命令用于将程序的执行权移出Generator函数,那么就需要一种方法将执行权再交还给Generator函数
这种方法就是Thunk函数,因为它可以在回调函数里,将执行权交还给Generator函数.
var g = gen();
var r1 = g.next();
//这里r1.value相当于readFileThunk('/etc/fstab'),此时该方法还不会被执行
//readFileThunk('/etc/fstab')(callback)
r1.value(function (err, data) {
if (err) throw err;
var r2 = g.next(data);
//这里的r2.value已经被赋值为Thunk函数名
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
上面代码中,变量g是Generator函数的内部指针,表示目前执行到哪一步.next
方法负责将指针移动到下一步,并返回该步的信息(value和done).
可以知道Generator函数的执行结果,其实是将同一个回调函数,反复传入next
方法的value
属性.这使得我们可以用递归来自动完成这个过程.
Thunk函数的自动流程管理
Thunk函数真正的威力,在于可以自动执行Generator函数.下面就是一个基于Thunk函数的Generator执行器.
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
//此时result.value已经被赋值为上一个Thunk函数名,next是一个回调函数
if (result.done) return;
result.value(next);
//当上一个Thunk函数执行完成之后才会调用回调函数,再继续执行下一个Thunk函数
}
next();
//next方法从来都不会被传入参数
}
function* g() {
// ...
}
run(g);
run函数就是一个Generator函数的自动执行器.内部的next函数就是Thunk的回调函数.next函数先将指针移到Generator函数的下一步(gen.next方法),然后判断Generator函数是否结束(result.done
属性),如果没结束,就将next函数再传入Thunk函数(result.value
属性),否则就直接退出
使用该执行器的前提是每一个异步操作,都要是Thunk函数,也就是说,跟在yield
命令后面的必须是Thunk函数.
Thunk函数并不是Generator函数自动执行的唯一方案.因为自动执行的关键是,必须有一种机制,自动控制Generator函数的流程,接收和交换程序的执行权.回调函数可以做到这一点,Promise对象也可以做到这一点.