简化Javascript的异步编码

在开发项目中的前端功能时,经常会有这种场景:

用户双击图形界面某处或者点击某个按钮,弹出窗口显示一个显示具体信息的窗体或者消息窗。

通常情况下,这个窗体的内容包含由一个可显示的表单信息,可能包含下拉菜单,多选框等组件,这些内容都需要跟后端服务器进行交互获得最新的数据。

enter image description here

以往在开发时,我基本都是写类似以下的代码:

function A(){
    //示例使用Ext
    Ext.Ajax.request({
        url:'myAction.do?action=showInfoA',
        params:{
            id: 1
        },
        success:function(response){
            var resultData = Ext.decode(response.responseText);
            //下拉框A的store loadData
            storeA.loadData(resultData.data);
            B();
        }
    })
}
function B(){
    //示例使用Ext
    Ext.Ajax.request({
        url:'myAction.do?action=showInfoB',
        params:{
            id: 1
        },
        success:function(response){
            var resultData = Ext.decode(response.responseText);
            //下拉框B的store loadData
            storeB.loadData(resultData.data);

            C();

        }
    })
}

function C(){
    //示例使用Ext
    Ext.Ajax.request({
        url:'myAction.do?action=showInfoC',
        params:{
            id: 1
        },
        success:function(response){
            var resultData = Ext.decode(response.responseText);
            //下拉框C的store loadData
            storeC.loadData(resultData.data);
            render();   
        }
    })
}
function render(){
    configWin.show();
}

当然除了这种方法,我们也可以先渲染待显示的Window窗体,给每个下拉框绑定一个事件,在下拉框激活时再去后台获取数据。

就目前编写的方法本身来看,它讲这些本可以异步处理的工作串行化了,导致效率的降低。

另外还有一种场景就是使用模板的情况,并且可能还涉及到前端的国际化。那么在编写代码时,通常就会类似以下代码:

var render = function (template, data) {
  _.template(template, data);
};
$.get("template", function (template) {
  // something
  $.get("data", function (data) {
    // something
    $.get("l10n", function (l10n) {
      // something
      render(template, data, l10n);
    });
  });
});

在这种情况下,执行的过程仍然被串行化了,导致执行的效率下降,并且在层次较深的情况下,导致代码难以阅读和维护。

如果深度很深的情况就会出现,最后一页都是}的情况了

世界上本没有嵌套回调,写得人多了,也便有了}}}}}}}}}}}}

下面将简单介绍使用三个有用的JS库来帮助我们将这些代码逻辑转换真正异步并行,并且易于阅读和维护的代码。这三个库分别是Eventproxy,Step,Async。

一、使用Eventproxy

首先来看在实际项目使用的第一个场景:

enter image description here

如上图所示,深度嵌套的代码经过华丽变身后,转化为以上的漂亮代码了。当然上面的代码仍然存在问题,针对异常的处理并不完整。

EventProxy提供了多组API,可以满足不同场景的需求.

1.1 多类型异步协作

前面示例中的模板和国际化以及数据加载的过程的代码可以转换为以下代码:

var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) {
  // 在所有指定的事件触发后,将会被调用执行
  // 参数对应各自的事件名
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
  ep.emit('tpl', content);
});
db.get('some sql', function (err, result) {
  ep.emit('data', result);
});

all方法将handler注册到事件组合上。当注册的多个事件都触发后,将会调用handler执行,每个事件传递的数据,将会依照事件名顺序,传入handler作为参数。

1.2 重复异步协作

此处以读取目录下的所有文件为例,在异步操作中,我们需要在所有异步调用结束后,执行某些操作。

var ep = new EventProxy();
ep.after('got_file', files.length, function (list) {
  // 在所有文件的异步执行结束后将被执行
  // 所有文件的内容都存在list数组中
});
for (var i = 0; i < files.length; i++) {
  fs.readFile(files[i], 'utf-8', function (err, content) {
    // 触发结果事件
    ep.emit('got_file', content);
  });
}

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

在最近开发的流程设计器中,画布需要异步加载对应控件的类文件,在开发中采用了类似方法解决:

a、传入待渲染到画布中的JSON数据

b、解析JSON文件,异步加载每个需要使用到的控件对应的JS模块类文件

c、全部解析完成后,在当前画布中渲染图形效果

具体的代码也类似上面读取文件的过程实现。

enter image description here

 

1.3 持续型异步协作

此处以股票为例,数据和模板都是异步获取,但是数据会是刷新,视图会重新刷新。

var ep = new EventProxy();
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);

 

tailall方法比较类似,都是注册到事件组合上。不同在于,指定事件都触发之后,如果事件依旧持续触发,将会在每次触发时调用handler,极像一条尾巴。

1.4 小结与注意事项 {#id-[实践]简化Javascript的异步编码-1.4小结与注意事项}

除了能够处理以上三种通常场景以外,Evenproxy还提供了Group,以及异常处理方法等,具体可以查看Eventproxy官方示例和文档。

  • 请勿使用all作为业务中的事件名。该事件名为保留事件。
  • 异常处理部分,请遵循Node的最佳实践。(如果在浏览器端运行则遵守浏览器端异常处理一般方法)

如果对Eventproxy的实现感兴趣也可以前往阅读其代码说明http://html5ify.com/eventproxy/eventproxy.html

 

二、使用Step

Step(https://github.com/creationix/step)提供的功能与Eventproxy类似,但是它针对的目标主要是Node的运行环境,所以这里将只会进行简单的介绍。

首先我们来看官方文档给出的一个实例:

Step(
  function readSelf() {
    fs.readFile(__filename, this);
  },
  function capitalize(err, text) {
    if (err) throw err;
    return text.toUpperCase();
  },
  function showIt(err, newText) {
    if (err) throw err;
    console.log(newText);
  }
);

 

在上面的示例中,我们将this
作为一个回调传给fs.readFile.当读取文件结束时,step会将读取结果作为参数传递给函数链中的下一个函数。紧接着在capitalize函数里可以完成一些同步任务并简单返回新的值,并且在我们调用回调时step会继续传递结果出去。

step还能提供类似eventproxy的重复的处理方法:

Step(
  function readDir() {
    fs.readdir(__dirname, this);
  },
  function readFiles(err, results) {
    if (err) throw err;
    // Create a new group
    var group = this.group();
    results.forEach(function (filename) {
      if (/\.js$/.test(filename)) {
        fs.readFile(__dirname + "/" + filename, 'utf8', group());
      }
    });
  },
  function showAll(err , files) {
    if (err) throw err;
    console.dir(files);
  }
);

对于Step提供的其它方法,可以前往其官方查看具体的示例说明。

 

三、使用Async

相比前面介绍的Eventproxy和Step来说,Async(https://github.com/creationix/step)提供的对于异步流程控制的处理方法更为丰富,它主要提供了两类API

  1. 提供集合相关的处理方法
  2. 提供异步流程控制的处理方法

3.1 Collections

3.2 Control Flow

我们这里将简单针对前文提及的几种场景进行API使用的介绍,其它的API使用方法可以前往官方网站查看。

前面提到的模板
国际化,数据最后渲染页面的方法使用async实现后的代码将如下,可以使用的API有两种series和parallel。

async.series([
    function(callback){
        fs.readFile('template.tpl', 'utf-8', function (err, content) {
          callback(null, coptent);
        });

    },
    function(callback){
        db.get('some sql', function (err, result) {
         callback(null, result);
        });

    }
],
// optional callback
function(err, results){
    console.log(results);
    //这种结果将以[]数组方式存储
});


async.series([
    tpl:function(callback){
        fs.readFile('template.tpl', 'utf-8', function (err, content) {
          callback(null, coptent);
        });

    },
    data:function(callback){
        db.get('some sql', function (err, result) {
         callback(null, result);
        });

    }
],
// optional callback
function(err, results){
    console.log(results);
    //这种结果将以{tpl:tpl,data:data}数组方式存储
});

series方法中的函数链将以定义的顺序执行。比如

async.series([
    function(callback){
        setTimeout(function(){
            console.log("start 1");
            callback(null, 1);
        }, 200);

    },
    function(callback){
        setTimeout(function(){
            console.log("start 2");
            callback(null, 2);
        }, 100);

    }
],
// optional callback
function(err, results){
    console.log(results);
});

以上示例代码使用settimeout来模拟代码执行的延时情况,以上结果将会打印:

start 1
start 2
[1, 2]

由于模板和数据之前并没有直接的依赖关系,那么我们可以改成并发异步的方法来执行。上面的代码只需要将series修改为parallel即修改并发异步执行的代码了。

前面的示例代码修改为并发执行的代码后,打印结果将会是:

start 2
start 1
[1,2]

可以看到两个函数的执行顺序将不再按照定义顺序执行,但是结果的顺序仍然保持顺序。

相比较Eventproxy和Step来说,Async提供了更多更丰富的API方法,足以满足各种场景下的需求。

比如执行的事件间是有依赖关系的,比如先根据一个序号查找该用户的信息,然后根据返回结果再执行下一个事件请求,数据需要在这些函数间进行传递.Step可以满足实现这一场景。

下面我们再来看Async提供给我们的解决方法:

async.waterfall([
    function(callback){
        console.log("start 1");
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        console.log("start 2");
        callback(null, 'three');
    },
    function(arg1, callback){
        console.log("start 3");
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    console.log(result);
   // result now equals 'done'    
});

上面的代码执行完成后的打印结果如下:

start 1
start 2
start 3
done

 

对于某些更复杂的场景,比如csser的一个应用场景:

  • 判断用户登录状态
  • 判断用户提交数据的合法性
  • 分析并获得可以存储的板贴数据
  • 查询并确认板贴所属贴板是否有效(是否存在,以及是否为发布者拥有)
  • 板贴入库
  • 更新关联操作(比如用户的板贴数、板贴列表,贴板的板贴数和板贴列表等等)
  • 产生用户动态
  • 产生标签动态
  • 通知关注者, 贴板成员,贴板作者
  • 最后返回结果

这一系列任务,如果组织不好,代码会相当凌乱,难以维护,借助
async.auto,产生了清晰明了的代码:

async.auto({
    // 分析并获取有效数据
    datas: function (callback) {},

    // 查贴板,确认是否存在
    board: function (callback) {},

    // 入库贴板内容
    post: ['datas', 'board', function (callback) {}],

    // 更新贴板表和用户表
    updateBoardAndUser: ['post', function (callback, result) {}],

    // 产生用户动态
    feed: ['post', function(callback, result) {}],

    // 产生标签动态
    tagFeed: ['feed', function (callback, result) {}],

    // 通知关注者, 贴板成员,贴板作者
    notify: ['post', function (callback, result) {}]

// 返回JSON结果
}, function (err, result) {});

 

总结

根据实际工作场景选择合适的库,可以帮助我们编写出易于阅读和维护的javascript异步代码,这可以让我们更容易的关注每个具体的事件背后的单一事件的实现。