详谈ES6中的迭代器(Iterator)和生成器(Generator)(8)

function run(taskDef) { // 创建迭代器,让它在别处可用 let task = taskDef(); // 启动任务 let result = task.next(); // 递归使用函数来保持对 next() 的调用 function step() { // 如果还有更多要做的 if (!result.done) { result = task.next(); step(); } } // 开始处理过程 step(); }

函数run()接受一个生成器函数作为参数,这个函数定义了后续要执行的任务,生成一个迭代器并将它储存在变量task中。首次调用迭代器的next()方法时,返回的结果被储存起来稍后继续使用。step()函数会检查result.done的值,如果为false则执行迭代器的next()方法,并再次执行step()操作。每次调用next()方法时,返回的最新信息总会覆写变量result。在代码的最后,初始化执行step()函数并开始整个的迭代过程,每次通过检查result.done来确定是否有更多任务需要执行

借助这个run()函数,可以像这样执行一个包含多条yield语句的生成器

run(function*() { console.log(1); yield; console.log(2); yield; console.log(3); });

这个示例最终会向控制台输出多次调用next()方法的结果,分别为数值1、2和3。当然,简单输出迭代次数不足以展示迭代器高级功能的实用之处,下一步将在迭代器与调用者之间互相传值

【向任务执行器传递数据】

给任务执行器传递数据的最简单办法是,将值通过迭代器的next()方法传入作为yield的生成值供下次调用。在这段代码中,只需将result.value传入next()方法即可

function run(taskDef) { // 创建迭代器,让它在别处可用 let task = taskDef(); // 启动任务 let result = task.next(); // 递归使用函数来保持对 next() 的调用 function step() { // 如果还有更多要做的 if (!result.done) { result = task.next(result.value); step(); } } // 开始处理过程 step(); }

现在result.value作为next()方法的参数被传入,这样就可以在yield调用之间传递数据了

run(function*() { let value = yield 1; console.log(value); // 1 value = yield value + 3; console.log(value); // 4 });

此示例会向控制台输出两个数值1和4。其中,数值1取自yield 1语句中回传给变量value的值;而4取自给变量value加3后回传给value的值。现在数据已经能够在yield调用间互相传递了,只需一个小小改变便能支持异步调用

【异步任务执行器】

之前的示例只是在多个yield调用间来回传递静态数据,而等待一个异步过程有些不同。任务执行器需要知晓回调函数是什么以及如何使用它。由于yield表达式会将值返回给任务执行器,所有的函数调用都会返回一个值,因而在某种程度上这也是一个异步操作,任务执行器会一直等待直到操作完成

下面定义一个异步操作

function fetchData() { return function(callback) { callback(null, "Hi!"); }; }

本示例的原意是让任务执行器调用的所有函数都返回一个可以执行回调过程的函数,此处fetchData()函数的返回值是一个可接受回调函数作为参数的函数,当调用它时会传入一个字符串"Hi!"作为回调函数的参数并执行。参数callback需要通过任务执行器指定,以确保回调函数执行时可以与底层迭代器正确交互。尽管fetchData()是同步函数,但简单添加一个延迟方法即可将其变为异步函数

function fetchData() { return function(callback) { setTimeout(function() { callback(null, "Hi!"); }, 50); }; }

在这个版本的fetchData()函数中,让回调函数延迟了50ms再被调用,所以这种模式在同步和异步状态下都运行良好。只需保证每个要通过yield关键字调用的函数都按照与之相同的模式编写

理解了函数中异步过程的运作方式,可以将任务执行器稍作修改。当result.value是一个函数时,任务执行器会先执行这个函数再将结果传入next()方法

function run(taskDef) { // 创建迭代器,让它在别处可用 let task = taskDef(); // 启动任务 let result = task.next(); // 递归使用函数来保持对 next() 的调用 function step() { // 如果还有更多要做的 if (!result.done) { if (typeof result.value === "function") { result.value(function(err, data) { if (err) { result = task.throw(err); return; } result = task.next(data); step(); }); } else { result = task.next(result.value); step(); } } } // 开始处理过程 step(); }

通过===操作符检査后,如果result.value是一个函数,会传入一个回调函数作为参数调用它,回调函数遵循Node.js有关执行错误的约定:所有可能的错误放在第一个参数(err)中,结果放在第二个参数中。如果传入了err,意味着执行过程中产生了错误,这时通过task.throw()正确输出错误对象;如果没有错误产生,data被传入task.next()作为结果储存起来,并继续执行step()。如果result.value不是一个函数,则直接将其传入next()方法

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wyjydf.html