码迷,mamicode.com
首页 > 其他好文 > 详细

EventProxy流程控制

时间:2015-02-17 22:28:39      阅读:167      评论:0      收藏:0      [点我收藏+]

标签:node.js eventproxy

EventProxy是一个通过控制事件触发顺序来控制业务流程的工具。

1. 利用事件机制解耦复杂业务逻辑
2. 移除被广为诟病的深度callback嵌套问题
3. 将串行等待变成并行等待,提升多异步协作场景下的执行效率
4. 友好的Error handling

node.js的亮点是回调函数,node.js流程控制、传参都是通过回调函数来实现的。开发中经常会遇到回调嵌套的场景,尤其是在业务复杂的场景下,会嵌套n层回调函数,这样做的原因是为了控制代码执行的流程。

下面是一个需要同步读取文件的例子

var str = ‘‘;
fs.readFile(‘file1.txt‘, function(err, data1) {
    str += data1;
    fs.readFile(‘file2.txt‘, function(err, data2) {
        str += data2;
        fs.readFile(‘file3.txt‘, function(err, data3) {
            str += data3;
        });
    });
});

上面代码有3层回调嵌套,用嵌套来保障文件的顺序读取。这样代码可读性就比较差了,而开发中用到的嵌套往往有很多层,代码很难读。

我们用EventProxy来解耦

安装:npm install eventproxy

引包:var EventProxy = require(‘eventproxy‘);
            var ep = new EventProxy();

1、异步协作 all

多个操作并行执行,每个操作完成后触发一个自定义事件.ep.all监听所有操作触发的事件,然后在回调函数里面提供各事件提供的结果

ep.all(‘read1‘, ‘read2‘, ‘read3‘, function(data1, data2, data3) {
    //监听事件,所有事件都触发完成后,在这里汇总结果
    console.info(data1 + ‘~‘ + data2 + ‘~‘ + data3);
});
fs.readFile(‘file1.txt‘, function(err, data1) {
    ep.emit(‘read1‘, data1);//触发事件
});
fs.readFile(‘file2.txt‘, function(err, data2) {
    ep.emit(‘read2‘, data2);//触发事件
});
fs.readFile(‘file3.txt‘, function(err, data3) {
    ep.emit(‘read3‘, data3);//触发事件
});

2、重复异步协作 after

`after`方法适合重复的操作,比如读取10个文件,调用5次数据库等。将handler注册到N次相同事件的触发上。达到指定的触发数,handler将会被调用执行,每次触发的数据,将会按触发顺序,存为数组作为参数传入。

var files = [‘file1.txt‘, ‘file2.txt‘, ‘file3.txt‘];
ep.after(‘readfile‘, files.length, function(list) {
    // 在所有文件的异步执行结束后将被执行
    // 所有文件的内容都存在list数组中

   //list中值是无序的,因为for循环中ep.emit触发顺序是无序的(node异步特点),排列顺序取决于node事件触发的顺序。可以通过group来控制对应关系,后续讲。

});

for (var i = 0; i < files.length; i++) {
    fs.readFile(files[i], function(err, data) {
        // 触发结果事件
        ep.emit(‘readfile‘, data);
    });
}

3、持续性异步事件 tail

此处以股票为例,数据和模板都是异步获取,但是数据会持续刷新,视图会需要重新刷新。
ep.tail(‘tpl‘, ‘data‘, function (tpl, data) {
  // 在所有指定的事件触发后,将会被调用执行,如果事件依旧保持触发,这里依旧多次执行
  // 参数对应各自的事件名的最新数据
});
fs.readFile(‘template.tpl‘, ‘utf-8‘, function (err, content) {
  ep.emit(‘tpl‘, content);
});
setInterval(function () {
  db.get(‘some sql‘, function (err, result) {
    ep.emit(‘data‘, result);
  });
}, 2000);

`tail`与`all`方法比较类似,都是注册到事件组合上。不同在于,all指定事件都触发后则执行一次,tail指定事件都触发之后,执行一次,如果事件依旧持续触发,将会在每次触发时调用handler,回调函数不断返回结果,极像一条尾巴。

4、异常处理

4.1 ep.bind(‘error‘)

ep.all(‘file1‘, ‘file2‘, ‘file3‘, function(data1, data2, data3) {
  
});

//监听error事件
ep.bind(‘error‘, function(err) {
    // 卸载所有的handler,ep.all的将停止运行,function也不会返回结果
    ep.unbind();
});

fs.readFile(‘file1.txt‘, function(err, data1) {
    if (err) {
        // 一旦发生异常,交给error事件的handler处理
        return ep.emit(‘error‘, err);
    }
    ep.emit(‘file1‘, data1 + new Date());
});

fs.readFile(‘file2.txt‘, function(err, data2) {
    if (err) {
        // 一旦发生异常,交给error事件的handler处理
        return ep.emit(‘error‘, err);
    }
    ep.emit(‘file2‘, data2 + new Date());
});

fs.readFile(‘file3.txt‘, function(err, data3) {
    if (err) {
        // 一旦发生异常,交给error事件的handler处理
        return ep.emit(‘error‘, err);
    }
    ep.emit(‘file3‘, data3);
});

4.2 ep.fail()

ep.all(‘file1‘, ‘file2‘, ‘file3‘, function(data1, data2, data3) {
    console.info(data1 + ‘~~‘ + data2 + ‘~~‘ + data3);
});

//监听error事件
ep.fail(function (err) {
    //捕获err
});

fs.readFile(‘file1.txt‘, ep.done(‘file1‘));
fs.readFile(‘file2.txt‘, ep.done(‘file2‘));
fs.readFile(‘file3.txt‘, ep.done(‘file3‘));

`fail`方法侦听了`error`事件,默认处理卸载掉所有handler,并调用回调函数。`fail`除了用于协助`all`方法完成外,也能协助`after`中的异常处理。

ep.fail(function(err) {

});
等价于
ep.bind(‘error‘,function(err) {
    ep.unbind();
});

ep.done(‘file1‘);
等价于
function (err, content) {
  if (err) {
    // 一旦发生异常,一律交给error事件的handler处理
    return ep.emit(‘error‘, err);
  }
  ep.emit(‘file1‘, content);
}

当然手工emit的方式并不太好,我们更进一步的版本:

```js
ep.done(‘file1‘, function (tpl) {
  // 将内容更改后,返回即可
  return file1.trim();
});
```

##注意事项
如果emit需要传递多个参数时,ep.done(‘file1‘,function(){})的方式不能满足需求,还是需要ep.done(function(){}),进行手工emit多个参数。

5、ep.group()

在`after`的回调函数中,结果顺序是与用户`emit`的顺序有关。为了满足返回数据按发起异步调用的顺序排列,`EventProxy`提供了`group`方法。

var files = [‘file1.txt‘, ‘file2.txt‘, ‘file3.txt‘];
ep.after(‘readfile‘, files.length, function(list) {
    // 在所有文件的异步执行结束后将被执行
    // 所有文件的内容都存在list数组中,for循环ep.emit事件是同步的,所以list中的值是有序的。
    console.info(new Buffer(list[0])+"~~"+new Buffer(list[1])+"~~"+new Buffer(list[2]));
});

for (var i = 0; i < files.length; i++) {
    fs.readFile(files[i], ep.group(‘readfile‘));
}

`group`秉承`done`函数的设计,它包含异常的传递。同时它还隐含了对返回数据进行编号,在结束时,按顺序返回。

```js
ep.group(‘readfile‘);
// 约等价于
function (err, data) {
  if (err) {
    return ep.emit(‘error‘, err);
  }
  ep.emit(‘readfile‘, data);
};
``
当回调函数的数据还需要进行加工时,可以给`group`带上回调函数,只要在操作后将数据返回即可:

```js
ep.group(‘readfile‘, function (data) {
  // some code
  return data;
});

6、异步事件触发: emitLater && doneLater

在node中,`emit`方法是同步的,EventProxy中的`emit`,`trigger`等跟node的风格一致,也是同步的。看下面这段代码,可能眼尖的同学一下就发现了隐藏的bug:
```js
var ep = EventProxy.create();

db.check(‘key‘, function (err, permission) {
  if (err) {
    return ep.emit(‘error‘, err);
  }
  ep.emit(‘check‘, permission);
});

ep.once(‘check‘, function (permission) {
  permission && db.get(‘key‘, function (err, data) {
    if (err) {
      return ep.emit(‘error‘);
    }
    ep.emit(‘get‘, data);
  });
});

ep.once(‘get‘, function (err, data) {
  if (err) {
    retern ep.emit(‘error‘, err);
  }
  render(data);
});

ep.on(‘error‘, errorHandler);
```
没错,万一`db.check`的`callback`被同步执行了,在`ep`监听`check`事件之前,它就已经被抛出来了,后续逻辑没办法继续执行。尽管node的约定是所有的`callback`都是需要异步返回的,但是如果这个方法是由第三方提供的,我们没有办法保证`db.check`的`callback`一定会异步执行,所以我们的代码通常就变成了这样:

```js
var ep = EventProxy.create();

ep.once(‘check‘, function (permission) {
  permission && db.get(‘key‘, function (err, data) {
    if (err) {
      return ep.emit(‘error‘);
    }
    ep.emit(‘get‘, data);
  });
});

ep.once(‘get‘, function (err, data) {
  if (err) {
    retern ep.emit(‘error‘, err);
  }
  render(data);
});

ep.on(‘error‘, errorHandler);

db.check(‘key‘, function (err, permission) {
  if (err) {
    return ep.emit(‘error‘, err);
  }
  ep.emit(‘check‘, permission);
});
```
我们被迫把`db.check`挪到最后,保证事件先被监听,再执行`db.check`。`check`->`get`->`render`的逻辑,在代码中看起来变成了`get`->`render`->`check`。如果整个逻辑更加复杂,这种风格将会让代码很难读懂。

这时候,我们需要的就是 __异步事件触发__:

```js
var ep = EventProxy.create();

db.check(‘key‘, function (err, permission) {
  if (err) {
    return ep.emitLater(‘error‘, err);
  }
  ep.emitLater(‘check‘, permission);
});

ep.once(‘check‘, function (permission) {
  permission && db.get(‘key‘, function (err, data) {
    if (err) {
      return ep.emit(‘error‘);
    }
    ep.emit(‘get‘, data);
  });
});

ep.once(‘get‘, function (err, data) {
  if (err) {
    retern ep.emit(‘error‘, err);
  }
  render(data);
});

ep.on(‘error‘, errorHandler);
```
上面代码中,我们把`db.check`的回调函数中的事件通过`emitLater`触发,这样,就算`db.check`的回调函数被同步执行了,事件的触发也还是异步的,`ep`在当前事件循环中监听了所有的事件,之后的事件循环中才会去触发`check`事件。代码顺序将和逻辑顺序保持一致。
当然,这么复杂的代码,必须可以像`ep.done()`一样通过`doneLater`来解决:

```js
var ep = EventProxy.create();

db.check(‘key‘, ep.doneLater(‘check‘));

ep.once(‘check‘, function (permission) {
  permission && db.get(‘key‘, ep.done(‘get‘));
});

ep.once(‘get‘, function (data) {
  render(data);
});

ep.fail(errorHandler);
```
最终呈现出来的,是一段简洁且清晰的代码。

## 注意事项
- 请勿使用`all`作为业务中的事件名。该事件名为保留事件。
- 异常处理部分,请遵循Node的最佳实践(回调函数首个参数为异常传递位)。


参考文献:

EventProxy Readme

EventProxy流程控制

标签:node.js eventproxy

原文地址:http://blog.csdn.net/shmnh/article/details/43866677

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!