Generator
Generator 函数是 ES6
提供的一种异步编程解决方案,语法行为与传统函数完全不同。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
定义了⼀个⽣成器,仅仅需要在关键字function后⾯加上⼀个星号(*)。⽣成器函数体内就能够使用新关键字yield,从而生成独⽴的值。Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是迭代器对象(Iterator Object)。
const hw = helloWorldGenerator(); //调⽤⽣成器得到⼀个迭代器
hw.next(); // {value: "hello", done: false}
hw.next(); // {value: "world", done: false}
hw.next(); // {value: "ending", done: false}
hw.next(); // {value: undefined, done: true}
迭代器⽤于控制生成器函数的执行。迭代器对象暴露的最基本接⼝是next⽅法。每次调用next
方法,生成器就开始执⾏代码,当代码执⾏到yield关键字时,就会生成⼀个中间结果(⽣成值序列中的⼀项),然后返回⼀个新对象,其中封装了结果值和⼀个指示完成的指示器。
每当⽣成⼀个当前值后,迭代器就到下一个yield
表达式停住,随后耐⼼等待下⼀次值请求的到达。
第一次调用,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* 表达式
function* WarriorGenerator() {
yield "Sun Tzu";
yield* NinjaGenerator(); // yield* 将执⾏权交给了另⼀个⽣成器
yield "Genghis Khan";
}
function* NinjaGenerator() {
yield "Hattori";
yield "Yoshi";
}
for (let warrior of WarriorGenerator()) {
console.log(warrior);
}
// "Sun Tzu" "Hattori" "Yoshi" "Genghis Khan"
for...of
循环可以自动遍历 Generator 函数运行时生成的Iterator
对象,且此时不再需要调用next
方法。
在迭代器上使⽤yield
*操作符,程序会跳转到另外⼀个生成器上执行。本例中,程序从WarriorGenerator
跳转到⼀个新的NinjaGenerator
⽣成器上,每次调⽤WarriorGenerator
返回迭代器的next
方法,都会使执行重新寻址到了NinjaGenerator
上。
使用生成器
用生成器生成ID序列
function* IdGenerator() {//定义⽣成器函数IdGenerator
let id = 0;//⼀个始终记录ID的变量,这个变量⽆法在⽣成器外部改变
while (true) {
yield ++id;
}//循环⽣成⽆限⻓度的ID序列 }
}
const idIterator = IdGenerator();//这个迭代器我们能够向⽣成器请求新的ID值
const ninja1 = { id: idIterator.next().value };
const ninja2 = { id: idIterator.next().value };
const ninja3 = { id: idIterator.next().value };//请求3个新ID值
我们能够调⽤idIterator.next()
⽅法来控制⽣成器执⾏。每当遇到⼀次yield语句⽣成器就会停⽌执⾏,返回⼀个新的ID值可以⽤于给我们的对象赋值。代码中没有任何会被不小心修改的全局变量。使用while
无限循环会在每次遇到yield
表达式停住。
使用迭代器遍历DOM树
通过递归回调的方式遍历
<div id="subTree">
<form>
<input type="text" />
</form>
<p>Paragraph</p>
<span>Span</span>
</div>
<script>
function traverseDOM(element, callback) {
callback(element);// ⽤回调函数处理当前节点
element = element.firstElementChild;
while (element) {
traverseDOM(element, callback);
element = element.nextElementSibling;
}// 遍历每个⼦树
}
const subTree = document.getElementById("subTree");
traverseDOM(subTree, function (element) {
});// 通过调⽤traverseDOM⽅法从根节点开始遍历
</script>
通过生成器递归遍历
function* DomTraversal(element) {
yield element;
element = element.firstElementChild;
while (element) {
yield* DomTraversal(element); // ⽤yield将迭代控制转移到另⼀个DomTraversal⽣成器实例上
element = element.nextElementSibling;
}
}
const subTree = document.getElementById("subTree");
for (const element of DomTraversal(subTree)) {
console.log(element);
} // 使⽤for-of对节点进行循环迭代
我们在不使用回调函数的情况下,使用生成器解耦代码,从而将生产者(html
节点)的代码和消费者(for-of
循环打印、访问过的节点)的代码分割开。
与生成器交互
next方法参数
function* NinjaGenerator(action) { //⽣成器可以像其他函数⼀样接收标准参数
const imposter = yield("Hattori " + action);
console.log(imposter);
yield("Yoshi (" + imposter + ") " + action); //传递回的值将成为yield表达式的返回值,因此impostrer的值是第二次调用next传递的值
}
const ninjaIterator = NinjaGenerator("skulk"); //普通的参数传递
const result1 = ninjaIterator.next(); // {value: "Hattori skulk", done: false}
const result2 = ninjaIterator.next("Hanzo"); // {value: "Yoshi (Hanzo) skulk", done: false}
// or
const ninjaIterator = NinjaGenerator("skulk"); //普通的参数传递
const result1 = ninjaIterator.next(); // {value: "Hattori skulk", done: false}
const result2 = ninjaIterator.next(); // {value: "Yoshi (undefined) skulk", done: false}
next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的。V8
引擎直接忽略第一次使用next
方法时的参数,只有从第二次使用next
方法开始,参数才是有效的。从语义上讲,第一个next
方法用来启动遍历器对象,所以不用带有参数。
向生成器抛出异常
function* NinjaGenerator() {
try {
yield 'Hattori';
yield 'Hanzo';
} catch (e) {
console.log('内部捕获', e);
}
}
const ninjaIterator = NinjaGenerator();
const result1 = ninjaIterator.next();
try {
ninjaIterator.throw('a');
ninjaIterator.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
//内部捕获 a
//外部捕获 b
遍历器对象ninjaIterator
连续抛出两个错误。第一个错误被 Generator 函数体内的catch
语句捕获。ninjaIterator
第二次抛出错误,由于 Generator 函数内部的catch
语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch
语句捕获。throw
方法也可以接受一个参数,该参数会被catch
语句接收。
return()方法
function* NinjaGenerator() {
yield 1;
yield 2;
yield 3;
}
const ninjaIterator = NinjaGenerator();
ninjaIterator.next() // { value: 1, done: false }
ninjaIterator.return('foo') // { value: "foo", done: true }
ninjaIterator.next() // { value: undefined, done: true }
遍历器对象ninjaIterator
调用return()
方法后,返回值的value
属性就是return()
方法的参数foo
。并且,Generator 函数的遍历就终止了,返回值的done
属性为true
,以后再调用next()
方法,done
属性总是返回true
。
通过执行上下文跟踪生成器函数
function* NinjaGenerator(action) {
yield "Hattori " + action;
return "Yoshi " + action;
}
const ninjaIterator = NinjaGenerator("skulk");
const result1 = ninjaIterator.next();
const result2 = ninjaIterator.next();
一般情况下当程序从⼀个标准函数返回后,对应的执行环境山下文会从栈中弹出,并被完整地销毁。但在生成器中不是这样。
相对应的NinjaGenerator
会从栈中弹出,但由于ninjaIterator
还保存着对它的引⽤,所以它不会被销毁。可以把它看作⼀种类似闭包的事物。以这种⽅式,我们能保证只要函数还存在,环境及变量就都存在着。
const result1 = ninjaIterator.next();
如果这只是⼀个普通的函数调⽤,这个语句会创建⼀个新的next()的执行环境上下文项,并放入栈中。但生成器绝不标准。它会重新激活对应的执行上下文。在这个例⼦中,是NinjaGenerator
上下文,并把该上下文放⼊栈的顶部,从它上次离开的地方继续执行。
const result2 = ninjaIterator.next();
在这个位置,又把整个流程走了⼀遍,首先通过ninjaIterator
激活NinjaGenerator
的上下文引用,将其入栈,在上次离开的位置继续执行。这次遇到了return语句。这个语句会返回值Yoshi skulk并结束生成器的执行,随之生成器进入结束状态。