深入理解JavaScript生成器:从暂停恢复到异步编程

作者:代码拾荒

时间:2026-10-26

浏览:499

本文深入解析JavaScript ES6引入的生成器(Generator)函数。通过对比普通函数,详细阐述生成器利用yield暂停与恢复执行的能力,解析next()传参机制及双向消息传递原理。进一步探讨生成器在合作式并发、可迭代对象中的应用,以及如何结合Promise实现异步流程控制,并自然过渡到async/await语法。

文章目录

前言

在JavaScript生成器概念出现之前,开发者普遍遵循一个默认假设:函数一旦开始执行,就会一直运行至结束,期间没有任何其他代码能够打断它或插入其中。参考以下代码示例:

 var x = 1;
 function foo(){
  x++;
  bar();
  console.log("x",x);
}
 function bar(){
  x++;
}
 foo();  //x:3

然而,随着ES6引入了一种新的函数类型,我们发现它并不遵循“运行至结束”的特性。这种新类型的函数被称为生成器(Generator)。生成器的出现让我们意识到,代码运行并非总是顺畅无阻,通过暂停机制可以实现异步回调,从而颠覆了以往对函数执行流程的认知。

了解生成器

下面展示一段涉及合作式并发的ES6代码:

var x = 1;
function *foo(){
   x++;
   yield;//暂停
   console.log("x",x)
}
function bar(){
    x++;
}

可以看到,代码使用了`*foo`的形式来定义函数,这代表它是一个生成器而非普通函数。

那么,我们该如何运行前面的代码片段,使得`bar()`在`*foo()`内部的`yield`处执行呢?

具体步骤如下:

(1)首先执行`var it = foo()`,构造一个迭代器`it`来控制这个生成器,该迭代器负责控制其执行流程。

(2)调用`it.next()`以启动生成器`*foo()`,并执行`*foo()`第一行的`x++`。

(3)`*foo()`在`yield`语句处暂停,此时第一个`it.next()`调用结束。尽管`*foo()`仍在运行且处于活跃状态,但它目前处于暂停状态。

(4)此时查看变量`x`的值,结果为2。

(5)接着调用`bar()`,它通过`x++`再次递增`x`。

(6)此时再次查看`x`的值,结果为3。

(7)最后再次调用`it.next()`,使生成器`*foo()`从暂停处恢复执行,并运行`console.log(..)`语句,该语句使用当前值为3。

显然,`foo()`虽然启动了,但并未完整运行,它在`yield`处暂停了。后续恢复`foo()`并让它运行至结束并非必须操作。

因此,生成器是一类特殊的函数,可以启动和停止一次或多次,并不一定非要完成执行。尽管其强大之处尚不明显,但后续我们将看到,它将成为构建以生成器为异步流程控制基础的代码模式的核心组件之一。

对于“生成器函数是一种特殊函数”这一概念,通过以下两个例子来更深入地理解:

代码1:

function *foo(x,y){
   return x*y;
}
var it = foo(6,7);
var res = it.next();
res.value; //42

代码2:

function *foo(x){
  var y = x *(yield);
  return y;
}
var it = foo(6);
//启动foo()
it.next();
var res = it.next(7);
res.value //输出什么?

通过对比这两段代码,可以发现它们的相似之处。我们主要分析第二段代码。首先,传入6作为参数`x`。然后调用`it.next()`,这会启动`foo()`。在`foo()`内部,开始执行语句`var y = x...`,但随后遇到`yield`表达式。它神奇地在这一点上暂停`*foo()`,并在本质上要求调用代码为`yield`表达式提供一个结果值。接下来,调用`it.next(7)`,这一句将值传回作为被暂停的`yield`表达式的结果。因此,此时的赋值语句变为`var y = 6 * 7`,现在返回42作为`it.next(7)`的结果。

实际上,我们考虑的重点是这段代码中的这两行:

var y = x * (yield);
return y;

这段代码,在第一个`yield`这里应该插入什么值呢?由于第一个`next()`运行使得生成器启动并运行到此处,所以显然它无法回答这个问题。那么第二次`next()`调用回答第一个`yield`提出的这个问题,传入了7。

注意,是第二个`next`回答第一个`yield`;

再把代码稍微改动一下:

function *foo(x){
  var y = x *(yield “hello”);
  return y;
}
var it = foo(6);
//启动foo()
var res = it.next();
res.value //输出什么?
res = it.next(7);
res.value //输出什么?

在第一次调用`next`之后,没有传入任何东西,`res.value`的值是`hello`。第二次向上一暂停的`yield`处传入7,于是开始了`6*7`的计算,`res.value`的值变为42。

这里的每一个`next`都得到了回应。

小记:在第一次`next()`调用时没有传入任何值,此时的`value`就是`yield`后的数据。第二次向`next()`传入参数之后把这个参数代入`yield`处。其实呢,`yield`和`next()`这一对组合起来,在生成器的执行过程中构建了一个双向消息传递系统。我们并没有向第一个`next()`调用发送值,这是有意为之。只有暂停的`yield`才能接收这样一个通过`next()`传递的值,而在生成器的起始处我们调用第一个`next()`时,还没有暂停的`yield`来接收这样的一个值,所以不要在第一个`next()`上传递参数。

for...of

就像ES6新增的`for...of`循环一样,这意味着可以通过原生循环语法自动迭代标准迭代器:

var a = [1,3,5,7,9]
for(var v of a){
    console.log(v); //1 3 5 7 9
}

`for...of`循环在每次迭代中自动调用`next()`,它不会向`next()`传入任何值,并且会在接收到`done:true`之后手动停止,这对于在一组数据上循环很方便。循环向`a`请求它的迭代器,并自动使用这个迭代器迭代遍历`a`的值。

iterable(可迭代)

从ES6开始,从一个`iterable`中提取迭代器的方法是:`iterable`必须支持一个函数,其名称是专门的ES6符号值`Symbol.iterator`。调用这个函数时,它会返回一个迭代器,通常每次调用会返回一个全新的迭代器,虽然这一点并不是必须的。就像前面使用`for...of`直接迭代的一样,我们使用迭代器重写:

var a = [1,3,5,7,9]
var it = a[Symbol.iterator]()
it.next().value;//1
it.next().value;//3
it.next().value;//5

生成器+promise

ES6中最完美的世界就是生成器和promise的结合。但如何实现呢?

让我们来试一下,把支持promise的`foo()`和生成器`*main()`放在一起:

function foo(x,y){
    return request(
      "http:url/?x+y"
    )
}
function *main(){
    try{
       var text = yield foo(1,2)
       console.log(text)
    }
    catch(err){
        console.error(err)
    }
}

现在如何运行`*main()`呢?还有一些实现细节需要补充,来实现接收和连接`yield`出来的promise,使它能够在决议之后恢复生成器,先从手工开始实现:

var it = main()
var p = it.next().value //此时p为foo(1,2)
p.then( //等待promise的p决定成功/拒绝
  function(){
    it.next(text)
  },
  function(err){
    it.throw(err)
  }
)

这个模式下生成器`yield`出promise,然后其控制生成器的迭代器来执行它,直到结束,是非常强大有用的一种方法。对于ES7中,在这一方面增加语法支持的提案已经有了一些很强势的支持。

async与await

function foo(){
   return request(
      "http:url/?x+y"
   )
}
async function main(){
  try{
     var text = await foo(1,2)
     console.log(text)
  }
  catch(err){
    console.log(err);
  }
}
main();

可以看到`main`不再被声明为`*main`生成器函数,它现在是一类新的函数:`async`函数,并且我们也不用`yield`暂停点来暂停等待了,而是使用`await`等待并决议。我们`await`了一个promise,`async`函数就会自动获知要做什么,它会暂停这个函数(就像`yield`),直到promise生成成功/拒绝的结果。

小结

生成器是ES6的一个新的函数类型,它并不像普通函数那样总是从运行开始到运行结束。取而代之的是,生成器`yield`可以在运行当中暂停,并且等到将来再次`next()`时再从暂停的地方恢复运行。

这种交替的暂停和恢复是合作式的双向消息传递,这意味着生成器具有独一无二的能力来暂停自身,这是通过关键字`yield`实现的。不过,只有控制生成器的迭代器具有恢复生成器的功能(比如`next()`)

`yield`和`next()`这一对不只是一种控制机制,实际上也是一种双向消息传递机制。`yield..`表达式本质是暂停下来等待某个值,接下来的`next()`调用会向被暂停的`yield`表达式传回一个值(或者是隐式的undefined)

有时,我们还会把可能的异步藏在`yield`后面,把异步移动到控制生成器的迭代器的代码部分,如`yield foo(1,2)`。换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自然地追踪代码,解决了基于回调的异步的缺陷。

以上就是一文了解你不知道的JavaScript生成器篇的详细内容,更多关于JavaScript生成器的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章