zhengrenzhe's blog   About

使用ES6的Generator完成异步操作

ES6中引入了Generator这个东西,它是一种不同于传统异步的一种新的方法,他的最大特点就是可以“暂停执行”。

JS传统的异步方法中,事件/回调应该是最常用的,但当事件/回调很多,并且互相依赖的时候,那就完了,很容易就造成了回调地狱:

            });
        });
    });
});

后来有了Promise,它可以把这些回调“拍扁”,就成了下面这样:

.then(function(){})
.then(function(){})
.then(function(){})
.then(function(){})
.then(function(){})
.then(function(){})
.then(function(){})

看上去是好点了,但有些情况下,不管怎么异步都会使代码冗余,但又不得不使用异步,此时你一定会无比怀念同步,就像下面这种情况:

在开发chrome扩展时,内容脚本经常要和背景脚本通信,因为chrome限定了内容脚本的权限,有些操作必须得让背景脚本来完成,所以双方通信是必不可少的。但是这种情况下是显然不可能用同步的,不然就有可能导致界面卡死。所以两者通信有两种方式:

let port = chrome.runtime.connect( { name: 'xxx' } );
port.postMessage('hehe');
port.onMessage.addListener(function( msg ){
    console.log( msg );
});

这样就蛋疼了啊,因为所有消息响应事件回调是在一起了,假如我想获得个cookie,首先先发出个消息,接着就完了。。。等背景脚本查到cookie,以同样的方式发出一个消息,而此时在内容脚本中接收消息的地方却是在一个统一的事件回调函数中,也太不方便了,我TM就是想把cookie的获取和赋值放在一起啊。。。不过也是可以曲线救国一下的,就是每个消息新开一个链接,配合promise来完成,之后再断开连接,但是这样真的好吗。。。

好在现在有了Generator这个大杀器,而且特别适合这种状况。它有两大杀手锏来完成这个任务:

Generator是如何解决上面的问题呢?首先看他的第一大杀手锏:暂停执行。 创建一个Generator的语法就像下面这样,类似于普通函数,但是有一个 *

let gen = (function* (){
    port.postMessage({
        'method': obj.name,
        'data': obj.data
    });
    let t = yield;
    console.log( t );
})();

当创建了一个Generator后,这个Generator是不会执行的,仅是返回了指向Generator内部状态的指针对象,我们需要手动调用 gen.next();来开始执行。但Generator仅会执行到yield语句的地方,这就是Generator执行的规则:碰到yield就暂停执行,此时postMessage方法是已经执行了的。

这个消息被发送到了背景脚本,背景脚本在获取到我要的数据后,同样使用postMessage方法把消息发送至内容脚本。

消息到了内容脚本时,由onMessage回调统一处理,它可不管你的消息是从哪发或发给谁的,只要是消息,那就会到它这里。但是我们必须要辨别消息是发给谁的,并且根据发送目标来执行对应的代码,如果没有Generator,onMessage的回调里将不可避免的出现一大堆处理逻辑的代码,但是有了Generator,我们可以轻松的把接收到的数据注入到在内容脚本中的发送方,也就是上面名为gen的Generator中,为了实现这点,就必须靠Generator的第二个杀手锏:注入外部数据到Generator内

port.onMessage.addListener(function( msg ){
    gen.next( msg.data );
});

上面的代码忽略了判断接收者的部分,假设是只有gen这一个接收者(消息发送者)的。Generator的next方法还可以接收一个参数,作为上一个yield语句的返回值,此时next方法的参数为响应消息的数据,调用next方法后,从上一次暂停的yield语句开始执行,此时yield的返回值就成了从next方法中传入的参数并赋值给变量t,这时就完成了整个消息的接收。

整个接收过程如下图演示:

这就是使用Generator来实现的异步基本流程,你还可以更进一步的把Generator包在Promise里,这样就更加方便了。

上面说的案例又可以理解为“协程”,就是相互协作的线程(函数),协程的流程基本如下:

  1. 协程A开始
  2. 协程A执行到一定进度,暂停执行,此时协程B开始执行,控制权交给B
  3. 协程B执行完后,控制权交给A,协程A继续执行。

上面的例子基本就是这样,协程A就是gen,gen执行到发出消息则暂停,协程B,也就是背景脚本开始执行。发回响应后,协程B结束,并在onMessage回调中调用协程A的next方法,将控制权交给协程A,协程A继续执行。

所以有了Generator之后,再也不用担心多个事件回调造成的混乱啦。

← scrapy爬知乎带验证码登录  OSX 10.11.2下Xcode7.2的GLFW, GLEW环境配置 →