您现在的位置是:网站首页> 编程资料编程资料

JavaScript Generator异步过度的实现详解_javascript技巧_

2023-05-24 296人已围观

简介 JavaScript Generator异步过度的实现详解_javascript技巧_

异步过渡方案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.lo
                
                

-六神源码网