Javascript异步编程(二: Generator函数)

来源:互联网 发布:淘宝铝合金箱子 编辑:程序博客网 时间:2024/06/04 01:25

协程概念

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做”协程”(coroutine),意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。

维基百科

https://en.wikipedia.org/wiki/Coroutine

协程有点像函数,又有点像线程。它的运行流程大致如下。

第一步,协程A开始执行。第二步,协程A执行到一半,进入暂停,执行权转移到协程B。第三步,(一段时间后)协程B交还执行权。第四步,协程A恢复执行。

上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。

让我举个列子

function asnycJob() {  //TODO ...其他代码  const f = yield readFile(fileA);  //TODO ...其他代码}

上面代码的函数 asyncJob 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。
协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

Generator函数的概念

Generator 函数是 ES6 提供的一种异步编程解决方案。

Generator 函数有多种理解角度。从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,遍历器对象(Iterator Object)。

也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

是不是看着一脸懵逼状态,当然概念永远是抽象的,我举个例子

function* f(i) {    i++;    yield i;    i++;    console.log('中间');    yield i;    return 'finish';}for(let v of f(i)){    console.log(v);}

你们猜猜看返回结果是什么?

2中间3

猜对了嘛?

我来解释下上面代码,首先for..of内部是ES6迭代器,因为函数f返回本身是个迭代器对象,所以可以直接遍历出现。

当程序遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。当再次调用函数时,内部指针会指向上次yield的位置。下一次再从该位置继续向后执行。

Generator 函数封装异步原理

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

(1)Generator 函数的数据交换

next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据。

我们将遍历改成用next()来调用迭代器对象,

首先封装函数

function* gen(x){    let y = yield x + 2;    return y;}let g = gen(1);

常规调用next代码如下

console.log(g.next()) // { value: 3, done: false }console.log(g.next()) // { value: undefined, done: true }

当给next传入参数代码如下。

console.log(g.next()) // { value: 3, done: false }console.log(g.next(99))// { value: 99, done: true }

上面代码中,第一个 next 方法的 value 属性,返回表达式 x + 2 的值(3)。

第二个 next 方法带有参数99,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量 y 接收。因此,这一步的 value 属性,返回的就是2(变量 y 的值)。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

需要注意的是

yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。

另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

(2) Generator 函数的错误处理

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

function* gen(x){  try {    var y = yield x + 2;  } catch (e){     console.log(e);  }  return y;}var g = gen(1);console.log(g.next());g.throw'出错了');

输出结果

{ value: 3, done: false }出错了

上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try … catch 代码块捕获。

这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

Generator 函数实战

下面看看如何使用 Generator 函数,执行一个真实的异步任务。

const fetch = require('node-fetch');function* gen(){    const url = 'https://api.github.com/users/github';    const result = yield fetch(url);    console.log(result['avatar_url']);}

Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了 yield 命令。

执行这段代码的方法如下。

const g = gen();const result = g.next();result.value.then((data) =>{    return data.json();}).then((data)=>{    g.next(data);});

上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用 next 方法(第二行),执行异步任务的第一阶段。由于 Fetch 模块返回的是一个 Promise 对象,因此要用 then 方法调用下一个next 方法。

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。后面部分,就将介绍如何自动化异步任务的流程管理。