JavaScript Generator异步过度如何实现
这篇文章主要讲解了“JavaScript Generator异步过度如何实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“JavaScript Generator异步过度如何实现”吧!
异步过渡方案Generator
在使用 Generator
前,首先知道 Generator
是什么。
如果读者有 Python 开发经验,就会发现,无论是概念还是形式上,ES2015 中的 Generator
几乎就是 Python 中 Generator
的翻版。
Generator
本质上是一个函数,它最大的特点就是可以被中断,然后恢复执行。通常来说,当开发者调用一个函数之后,这个函数的执行就脱离了开发者的控制,只有函数执行完毕之后,控制权才能重新回到调用者手中,因此程序员在编写方法代码时,唯一
能够影响方法执行的只有预先定义的 return
关键字。
Promise
也是如此,我们也无法控制 Promise
的执行,新建一个 Promise
后,其状态自动转换为 pending
,同时开始执行,直到状态改变后我们才能进行下一步操作。
而 Generator
函数不同,Generator
函数可以由用户执行中断或者恢复执行的操作,Generator
中断后可以转去执行别的操作,然后再回过头从中断的地方恢复执行。
1. Generator 的使用
Generator
函数和普通函数在外表上最大的区别有两个:
在
function
关键字和方法名中间有个星号(*)。方法体中使用
yield
关键字。
function* Generator() { yield "Hello World"; return "end"; }
和普通方法一样,Generator
可以定义成多种形式:
// 普通方法形式 function* generator() {} //函数表达式 const gen = function* generator() {} // 对象的属性方法 const obi = { * generator() { } }
Generator 函数的状态
yield
关键字用来定义函数执行的状态,在前面代码中,如果 Generator
中定义了 x
个 yield
关键字,那么就有 x + 1
种状态(+1是因为最后的 return
语句)。
2. Generator 函数的执行
跟普通函数相比,Generator
函数更像是一个类或者一种数据类型,以下面的代码为例,直接执行一个 Generator
会得到一个 Generator
对象,而不是执行方法体中的内容。
const gen = Generator();
按照通常的思路,gen
应该是 Generator()
函数的返回值,上面也提到Generator
函数可能有多种状态,读者可能会因此联想到 Promise
,一个 Promise
也可能有三种状态。不同的是 Promise
只能有一个确定的状态,而 Generator
对象会逐个经历所有的状态,直到 Generator
函数执行完毕。
当调用 Generator
函数之后,该函数并没有立刻执行,函数的返回结果也不是字符串,而是一个对象,可以将该对象理解为一个指针,指向 Generator
函数当前的状态。(为了便于说明,我们下面采用指针的说法)。
当 Generator
被调用后,指针指向方法体的开始行,当 next
方法调用后,该指针向下移动,方法也跟着向下执行,最后会停在第一个遇到的 yield
关键字前面,当再次调用 next
方法时,指针会继续移动到下一个 yield
关键字,直到运行到方法的最后一行,以下面代码为例,完整的执行代码如下:
function* Generator() { yield "Hello World"; return "end"; } const gen = Generator(); console.log(gen.next()); // { value: 'Hello World', done: false } console.log(gen.next()); // { value: 'end', done: true } console.log(gen.next()); // { value: undefined, done: true }
上面的代码一共调用了三次 next
方法,每次都返回一个包含执行信息的对象,包含一个表达式的值和一个标记执行状态的 flag
。
第一次调用 next
方法,遇到一个 yield
语句后停止,返回对象的 value
的值就是 yield
语句的值,done
属性用来标志 Generator
方法是否执行完毕。
第二次调用 next
方法,程序执行到 return
语句的位置,返回对象的 value
值即为 return
语句的值,如果没有 return
语句,则会一直执行到函数结束,value
值为 undefined
,done
属性值为 true
。
第三次调用 next
方法时,Generator
已经执行完毕,因此 value
的值为undefined
。
2.1 yield 关键字
yield
本意为 生产 ,在 Python、Java 以及 C# 中都有 yield
关键字,但只有Python 中 yield
的语义相似(理由前面也说了)。
当 next
方法被调用时,Generator
函数开始向下执行,遇到 yield
关键字时,会暂停当前操作,并且对 yield
后的表达式进行求值,无论 yield
后面表达式返回的是何种类型的值,yield
操作最后返回的都是一个对象,该对象有 value
和 done
两个属性。
value
很好理解,如果后面是一个基本类型,那么 value
的值就是对应的值,更为常见的是 yield
后面跟的是 Promise
对象。
done
属性表示当前 Generator
对象的状态,刚开始执行时 done
属性的值为false
,当 Generator
执行到最后一个 yield
或者 return
语句时,done
的值会变成 true
,表示 Generator
执行结束。
注意:yield关键字本身不产生返回值。例如下面的代码:
function* foo(x) { const y = yield(x + 1); return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: undefined, done: true }
为什么第二个 next
方法执行后,y
的值却是 undefined
。
实际上,我们可以做如下理解:next
方法的返回值是 yield
关键字后面表达式的值,而 yield
关键字本身可以视为一个不产生返回值的函数,因此 y
并没有被赋值。上面的例子中如果要计算 y
的值,可以将代码改成:
function* foo(x) { let y; yield y = x + 1; return 'end'; }
next
方法还可以接受一个数值作为参数,代表上一个 yield
求值的结果。
function* foo(x) { const y = yield(x + 1); return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next(10)); // { value: 10, done: true }
上面的代码等价于:
function* foo(x) { let y = yield(x + 1); y = 10; return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: 10, done: true }
next
可以接收参数代表可以从外部传一个值到 Generator
函数内部,乍一看没有什么用处,实际上正是这个特性使得 Generator
可以用来组织异步方法,我们会在后面介绍。
2.2 next 方法与 Iterator 接口
一个 Iterator
同样使用 next
方法来遍历元素。由于 Generator
函数会返回一个对象,而该对象实现了一个 Iterator
接口,因此所有能够遍历 Iterator
接口的方法都可以用来执行 Generator
,例如 for/of
、aray.from()
等。
可以使用 for/of
循环的方式来执行 Generator
函数内的步骤,由于 for/of
本身就会调用 next
方法,因此不需要手动调用。
注意:循环会在 done
属性为 true
时停止,以下面的代码为例,最后的 'end'
并不会被打印出来,如果希望被打印,需要将最后的 return
改为 yield
。
function* Generator() { yield "Hello Node"; yield "From Lear" return "end" } const gen = Generator(); for (let i of gen) { console.log(i); } // 和 for/of 循环等价 console.log(Array.from(Generator()));;
前面提到过,直接打印 Generator
函数的示例没有结果,但既然 Generator
函数返回了一个遍历器,那么就应该具有 Symbol.iterator
属性。
console.log(gen[Symbol.iterator]);
// 输出:[Function: [Symbol.iterator]]
3. Generator 中的错误处理
Generator
函数的原型中定义了 throw
方法,用于抛出异常。
function* generator() { try { yield console.log("Hello"); } catch (e) { console.log(e); } yield console.log("Node"); return "end"; } const gen = generator(); gen.next(); gen.throw("throw error");
// 输出
// Hello
// throw error
// Node
上面代码中,执行完第一个 yield
操作后,Generator
对象抛出了异常,然后被函数体中 try/catch
捕获。当异常被捕获后,Generator
函数会继续向下执行,直到遇到下一个 yield
操作并输出 yield
表达式的值。
function* generator() { try { yield console.log("Hello World"); } catch (e) { console.log(e); } console.log('test'); yield console.log("Node"); return "end"; } const gen = generator(); gen.next(); gen.throw("throw error");
// 输出
// Hello World
// throw error
// test
// Node
如果 Generator
函数在执行的过程中出错,也可以在外部进行捕获。
function* generator() { yield console.log(undefined, undefined); return "end"; } const gen = generator(); try { gen.next(); } catch (e) { }
Generator
的原型对象还定义了 return()
方法,用来结束一个 Generator
函数的执行,这和函数内部的 return
关键字不是一个概念。
function* generator() { yield console.log('Hello World'); yield console.log('Hello 夏安'); return "end"; } const gen = generator(); gen.next(); // Hello World gen.return(); // return() 方法后面的 next 不会被执行 gen.next();
4. 用 Generator 组织异步方法
我们之所以可以使用 Generator
函数来处理异步任务,原因有二:
Generator
函数可以中断和恢复执行,这个特性由yield
关键字来实现。Generator
函数内外可以交换数据,这个特性由next
函数来实现。
概括一下 Generator
函数处理异步操作的核心思想:先将函数暂停在某处,然后拿到异步操作的结果,然后再把这个结果传到方法体内。
yield
关键字后面除了通常的函数表达式外,比较常见的是后面跟的是一个 Promise
,由于 yield
关键字会对其后的表达式进行求值并返回,那么调用 next
方法时就会返回一个 Promise
对象,我们可以调用其 then
方法,并在回调中使用 next
方法将结果传回 Generator
。
function* gen() { const result = yield readFile_promise("foo.txt"); console.log(result); } const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data); });
上面的代码中,Generator
函数封装了 readFile_promise
方法,该方法返回一个 Promise
,Generator
函数对 readFile_promise
的调用方式和同步操作基本相同,除了 yield
关键字之外。
上面的 Generator
函数中只有一个异步操作,当有多个异步操作时,就会变成下面的形式。
function* gen() { const result = yield readFile_promise("foo.txt"); console.log(result); const result2 = yield readFile_promise("bar.txt"); console.log(result2); } const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data).value.then(function (data) { g.next(data); }) });
然而看起来还是嵌套的回调?难道使用 Generator
的初衷不是优化嵌套写法吗?说的没错,虽然在调用时保持了同步形式,但我们需要手动执行 Generator
函数,于是在执行时又回到了嵌套调用。这是 Generator
的缺点。
5. Generator 的自动执行
对 Generator
函数来说,我们也看到了要顺序地读取多个文件,就要像上面代码那样写很多用来执行的代码。无论是 Promise
还是 Generator
,就算在编写异步代码时能获得便利,但执行阶段却要写更多的代码,Promise
需要手动调用 then
方法,Generator
中则是手动调用 next
方法。
当需要顺序执行异步操作的个数比较少的情况下,开发者还可以接受手动执行,但如果面对多个异步操作就有些难办了,我们避免了回调地狱,却又陷到了执行地狱里面。我们不会是第一个遇到自动执行问题的人,社区已经有了很多解决方案,但为了更深入地了解 Promise
和 Generator
,我们不妨先试着独立地解决这个问题,如何能够让一个 Generator
函数自动执行?
5.1 自动执行器的实现
既然 Generator
函数是依靠 next
方法来执行的,那么我们只要实现一个函数自动执行 next
方法不就可以了吗,针对这种思路,我们先试着写出这样的代码:
function auto(generator) { const gen = generator(); while (gen.next().value !== undefined) { gen.next(); } }
思路虽然没错,但这种写法并不正确,首先这种方法只能用在最简单的 Generator
函数上,例如下面这种:
function* generator() { yield 'Hello World'; return 'end'; }
另一方面,由于 Generator
没有 hasNext
方法,在 while
循环中作为条件的:gen.next().value !== undefined
在第一次条件判断时就开始执行了,这表示我们拿不到第一次执行的结果。因此这种写法行不通。
那么换一种思路,我们前面介绍了 for/of
循环,那么也可以用它来执行 Generator
。
function* Generator() { yield "Hello World"; yield "Hello 夏安"; yield "end"; } const gen = Generator(); for (let i of gen) { console.log(i); }
// 输出结果
// Hello World
// Hello 夏安
// end
看起来没什么问题了,但同样地也只能拿来执行最简单的 Generator
函数,然而我们的主要目的还是管理异步操作。
5.2 基于Promise的执行器
前面实现的执行器都是针对普通的 Generator
函数,即里面没有包含异步操作,在实际应用中,yield
后面跟的大都是 Promise
,这时候 for/of
实现的执行器就不起作用了。
通过观察,我们发现 Generator
的嵌套执行是一种递归调用,每一次的嵌套的返回结果都是一个 Promise
对象。
const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data).value.then(function (data) { g.next(data); }) });
那么,我们可以根据这个写出新的执行函数。
function autoExec(gen) { function next(data) { const result = gen.next(data); // 判断执行是否结束 if (result.done) return result.value; result.value.then(function (data) { next(data); }); } next(); }
这个执行器因为调用了 then
方法,因此只适用于 yield
后面跟一个 Promise
的方法。
5.3 使用 co 模块来自动执行
为了解决 generator
执行的问题,TJ 于2013年6月发布了著名 co
模块,这是一个用来自动执行 Generator
函数的小工具,和 Generator
配合可以实现接近同步的调用方式,co
方法仍然会返回一个 Promise
。
const co = require("co"); function* gen() { const result = yield readFilePromise("foo.txt"); console.log(result); const result2 = yield readFilePromise("bar.txt"); console.log(result2); } co(gen);
只要将 Generator
函数作为参数传给 co
方法就能将内部的异步任务顺序执行,要使用 co
模块,yield
后面的语句只能是 promsie
对象。
到此为止,我们对异步的处理有了一个比较妥当的方式,利用 generator+co
,我们基本可以用同步的方式来书写异步操作了。但 co
模块仍有不足之处,由于它仍然返回一个 Promise
,这代表如果想要获得异步方法的返回值,还要写成下面这种形式:
co(gen).then(function (value) { console.log(value); });
另外,当面对多个异步操作时,除非将所有的异步操作都放在一个 Generator
函数中,否则如果需要对 co
的返回值进行进一步操作,仍然要将代码写到 Promise
的回调中去。
感谢各位的阅读,以上就是“JavaScript Generator异步过度如何实现”的内容了,经过本文的学习后,相信大家对JavaScript Generator异步过度如何实现这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是蜗牛博客,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:niceseo99@gmail.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
评论