JavaScript Generator异步过度的实现详解

目录
  • 异步过渡方案Generator
  • 1. Generator 的使用
  • 2. Generator 函数的执行
    • 2.1 yield 关键字
    • 2.2 next 方法与 Iterator 接口
  • 3. Generator 中的错误处理
  • 4. 用 Generator 组织异步方法
  • 5. Generator 的自动执行
    • 5.1 自动执行器的实现
    • 5.2 基于Promise的执行器
    • 5.3 使用 co 模块来自动执行

异步过渡方案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 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • js使用generator函数同步执行ajax任务

    本文实例为大家分享了js使用generator函数同步执行ajax任务的具体代码,供大家参考,具体内容如下 function request(url, callback) { fetch(url, {mode: 'cors', credentials: 'include', headers: new Headers({ 'X-Requested-With': 'XMLHttpRequest' })}) .then(response => response.text()) .then(text =

  • JavaScript 中使用 Generator的方法

    Generator 是一种非常强力的语法,但它的使用并不广泛(参见下图 twitter 上的调查!).为什么这样呢?相比于 async/await,它的使用更复杂,调试起来也不太容易(大多数情况又回到了从前),即使我们可以通过非常简单的方式获得类似体验,但是人们一般会更喜欢 async/await. 然而,Generator 允许我们通过 yield 关键字遍历我们自己的代码!这是一种超级强大的语法,实际上,我们可以操纵执行过程!从不太明显的取消操作开始,让我们先从同步操作开始吧. 我为文中提到

  • 详解JavaScript ES6中的Generator

    今天讨论的新特性让我非常兴奋,因为这个特性是 ES6 中最神奇的特性. 这里的"神奇"意味着什么呢?对于初学者来说,该特性与以往的 JS 完全不同,甚至有些晦涩难懂.从某种意义上说,它完全改变了这门语言的通常行为,这不是"神奇"是什么呢. 不仅如此,该特性还可以简化程序代码,将复杂的"回调堆栈"改成直线执行的形式. 我是不是铺垫的太多了?下面开始深入介绍,你自己去判断吧. 简介 什么是 Generator? 看下面代码: function* qu

  • Javascript生成器(Generator)的介绍与使用

    什么是生成器? 生成器是在函数内部运行的一些代码 返回值后,它会自行暂停,并且-- 调用程序可以要求取消暂停并返回另一个值 这种"返回"不是传统的从函数 return.所以它被赋予了一个特殊的名称--yield. 生成器语法因语言而异.Javascript 的生成器语法类似于 PHP,但是区别也很大,如果你希望它们的作用相同,那么最终你会感到非常困惑. 在 javascript 中,如果想要使用生成器,则需要: 定义特殊的生成器函数 调用该函数创建一个生成器对象 在循环中使用该生成器对

  • 深入理解js generator数据类型

    1. 概述 generator 是ES6引入的新的数据类型, 看上去像一个函数,除了使用return返回, yield可以返回多次. generator 由function* 定义, (注意*号), 2. 例子 函数无法保存状态, 有时需要全局变量来保存数字: 2.1 'use strict'; function next_id(){ var id = 1; while(id<100){ yield id; id++; } return id; } // 测试: var x, pass = tr

  • 小议JavaScript中Generator和Iterator的使用

    一说到 Generator,大家就会扯上异步之类是话题.这显然是被一些奇奇怪怪的东西带坏了.与 Generator 关系密切的应该是 Iterator 才对,拿 Generator 来处理异步也许是一些 C# 程序员才会想的事.当然这种用法确实有一套完整的东西,只是我个人不喜欢而已. 非要把 Generator 和异步联系上,唯一的点就是 next 的调用时机.因为 next 可以异步地调用,所以 Generator 才得以被异步地滥用. 但我觉得 next 这个方法虽然可以异步调用,但正确的使

  • JS Generator 函数的含义与用法实例总结

    本文实例讲述了JS Generator 函数的含义与用法.分享给大家供大家参考,具体如下: 读阮一峰老师<Generator 函数的含义与用法>总结 老师的文章通俗易懂,但是我个人理解上面有一些差,所以看了几遍之后才有呢么一点点体会 把它记录下来. 还是那句话,所有事物的出现都是为了解决对应的问题. 那么Generator出现是为了解决什么问题的呢? 在异步编程的场景下,如果有多个异步任务,如何处理他们的先后执行顺序? 举一个常见的例子,jquery的ajax请求,每一个success都是一个

  • JavaScript中浅讲ajax图文详解

    1.ajax入门案例 1.1 搭建Web环境 ajax对于各位来说,应该都不陌生,正因为ajax的产生,导致前台页面和服务器之间的数据传输变得非常容易,同时还可以实现页面的局部刷新.通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新.这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新. 对于JavaWeb项目而言,ajax主要用于浏览器和服务器之间数据的传输. 如果是单单地堆砌知识点,会显得比较无聊,那么根据惯例,我先不继续介绍ajax,而是来写一个案例吧. 打开

  • 关于javascript的一些知识以及循环详解

    javascript的一些知识点: 1.常用的五大浏览器:chrome,firefox,Safari,ie,opera 2.浏览器是如何工作的简化版: 3.Js由ECMAjavascript;DOM;BOM组成: 4.js是弱类型语言(即需要游览器解析了才知道是什么类型的): 5.js是脚本语言(边解析边执行): 6.script也分行内样式,嵌套样式和外联样式. 外联样式一般写在body的最后,因为放在前面会先加载js代码然后再干其他的,影响用户体验. 7.同步和异步 同步:一行一行依次执行.

  • 关于JavaScript和jQuery的类型判断详解

    对于类型的判断,JavaScript用typeof来进行. 栗子: console.log(typeof null); //object console.log(typeof []); //object console.log(typeof {}); //object console.log(typeof new Date()); //object console.log(typeof new Object); //object console.log(typeof function(){});

  • JavaScript浏览器对象之一Window对象详解

    JavaScript提供了一组以window为核心的对象,实现了对浏览器窗口的访问控制.JavaScript中定义了6种重要的对象: window对象 表示浏览器中打开的窗口: document对象 表示浏览器中加载页面的文档对象: location对象包含了浏览器当前的URL信息: navigation对象 包含了浏览器本身的信息: screen对象 包含了客户端屏幕及渲染能力的信息: history对象 包含了浏览器访问网页的历史信息. 除了window对象之外,其他的5个对象都是windo

  • Javascript类型系统之String字符串类型详解

    javascript没有表示单个字符的字符型,只有字符串String类型,字符型相当于仅包含一个字符的字符串 字符串String是javascript基本数据类型,同时javascript也支持String对象,它是一个原始值的包装对象.在需要时,javascript会自动在原始形式和对象形式之间转换.本文将介绍字符串String原始类型及String包装对象 定义 字符串String类型是由引号括起来的一组由16位Unicode字符组成的字符序列 字符串类型常被用于表示文本数据,此时字符串中的

  • javascript类型系统_正则表达式RegExp类型详解

    前面的话 前面已经介绍过javascript中正则表达式的基础语法.javascript的RegExp类表示正则表达式,String和RegExp都定义了方法,使用正则表达式可以进行强大的模式匹配和文本检索与替换.本文将介绍正则表达式的RegExp对象,以及正则表达式涉及 到的属性和方法 对象 javascript中的正则表达式用RegExp对象表示,有两种写法:一种是字面量写法:另一种是构造函数写法 Perl写法 正则表达式字面量写法,又叫Perl写法,因为javascript的正则表达式特性

  • JavaScript中push(),join() 函数 实例详解

    定义和用法 push方法 可向数组的末尾添加一个或多个元素,并返回一个新的长度. join方法 用于把数组中所有元素添加到一个指定的字符串,元素是通过指定的分隔符进行分割的. 语法 arrayObject.push(newelement1,newelement2,....,newelementX) arrayObject.join(separator). 参数描述newelement1必需.要添加到数组的第一个元素.newelement2可选.要添加到数组的第二个元素.newelementX可选

  • JavaScript类型系统之布尔Boolean类型详解

    前面的话 布尔值Boolean类型可能是三种包装对象Number.String和Boolean中最简单的一种.Number和String对象拥有大量的实例属性和方法,Boolean却很少.从某种意义上说,为计算机设计程序就是与布尔值打交道,作为最基本的事实,所有的电子电路只能识别和使用布尔数据.本文将介绍布尔Boolean类型 定义 布尔Boolean类型表示逻辑实体,它只有两个值,保留字true和false,分别代表真和假这两个状态 Boolean包装类型是与布尔值对应的引用类型,在布尔表达式

  • Javascript的无new构建实例详解

    看jquery源代码第一步的时候,对于jquery对象的创建就看的云里雾里,琢磨半天终于有点感觉了,在此记录下 第一种方式: var A = function(){ return A.prototype.init(); } A.prototype = { init:function(){ this.age = 50; console.log(this); return this; }, age:100 } console.log(A() === new A()); 1.分析下结果为什么为true

  • javascript 作用于作用域链的详解

    javascript 作用于作用域链的详解 一.JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥有全局作用域,一般来说一下几种情形拥有全局作用域: (1)最外层函数和在最外层函数外面定义的变量拥有全局作用域, 例如: var authorName="Bu

随机推荐

其他