异步过渡方案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 中定义了 xyield 关键字,那么就有 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 值为 undefineddone 属性值为 true

第三次调用 next 方法时,Generator 已经执行完毕,因此 value 的值为undefined

2.1 yield 关键字

yield 本意为 生产 ,在 Python、Java 以及 C# 中都有 yield 关键字,但只有Python 中 yield 的语义相似(理由前面也说了)。

next 方法被调用时,Generator 函数开始向下执行,遇到 yield 关键字时,会暂停当前操作,并且对 yield 后的表达式进行求值,无论 yield 后面表达式返回的是何种类型的值,yield 操作最后返回的都是一个对象,该对象有 valuedone 两个属性。

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/ofaray.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 方法,该方法返回一个 PromiseGenerator 函数对 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 方法。

当需要顺序执行异步操作的个数比较少的情况下,开发者还可以接受手动执行,但如果面对多个异步操作就有些难办了,我们避免了回调地狱,却又陷到了执行地狱里面。我们不会是第一个遇到自动执行问题的人,社区已经有了很多解决方案,但为了更深入地了解 PromiseGenerator,我们不妨先试着独立地解决这个问题,如何能够让一个 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 内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

JavaScript Generator异步过度的实现详解的更多相关文章

  1. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  2. HTML5数字输入仅接受整数的实现代码

    这篇文章主要介绍了HTML5数字输入仅接受整数的实现代码,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. amaze ui 的使用详细教程

    这篇文章主要介绍了amaze ui 的使用详细教程,本文通过多种方法给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. html5简介_动力节点Java学院整理

    这篇文章主要介绍了html5简介,用于指定构建网页的元素,这些元素中的大多数都用于描述网页内容,有兴趣的可以了解一下

  5. ios 8 Homescreen webapp,关闭和打开iPad停止javascript

    我有一个适用于iPad的全屏HTML5网络应用程序,并且刚刚安装了IOS8来试用它,它一切正常,直到你关闭并重新启动iPad.一旦web应用程序重新启动javascript就会停止并加载新页面不会重新启动它.在iPad上的Safari中打开同一页面时,关闭和打开iPad会继续按预期工作.其他人注意到了这个或想出了一个解决方案吗?解决方法这似乎是我在iOS8.1.1更新中解决的.

  6. iOS 6 javascript与object.defineProperty的间歇性问题

    当访问使用较新的Object.defineProperty语法定义属性的对象的属性时,有没有其他人注意到新iOS6javascript引擎中的间歇性错误/问题?https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty我正在看到javascript失败的情况,说

  7. ios – 如何使用JSExport导出内部类的方法

    解决方法似乎没有办法将内部类函数导出到javascript.我将内部类移出并创建了独立的类,它起作用了.

  8. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  9. ios – 内存泄漏与UIWebView和Javascript

    清楚地包含一个Javascript文件到我的HTML是使UIWebView泄漏内存.当我重复使用相同的UIWebView对象时,或者每当我有内容实例化一个新的漏洞时,会出现泄漏的事实,导致我认为必须有一些JavaScript文件被loadHTMLString处理,导致泄漏.有人知道如何解决这个问题吗?

  10. iOS应用程序的UI自动化测试如何与乐器和Javascript

    从WWDC2010视频会议中了解iOS应用程序的自动化UI测试,但没有实践.从代码项目project,我们可以有一个例子.这个问题在这里听到有涉及这个的人.任何限制?解决方法我建议从AlexWollmer开始使用thisblogpost.他创建了一个非常有用的JavaScript库:tuneup_jswithtest()函数,它允许测试分离和有用的帮助者以及为自动化仪器编写测试的断言.

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部