如何保持连续性/回调的代码可读性?


10

简介:尽管使用了异步代码和回调,是否可以遵循一些完善的最佳实践模式来使代码保持可读性?


我正在使用一个JavaScript库,该库异步执行很多工作,并且严重依赖于回调。看来编写一个简单的“负载A,负载B ...”方法变得相当复杂,并且很难使用此模式进行操作。

让我举一个(人为的)例子。假设我要从远程Web服务器(异步)加载一堆图像。在C#/ async中,我将编写如下内容:

disableStartButton();

foreach (myData in myRepository) {
    var result = await LoadImageAsync("http://my/server/GetImage?" + myData.Id);
    if (result.Success) {
        myData.Image = result.Data;
    } else {
        write("error loading Image " + myData.Id);
        return;
    }
}

write("success");
enableStartButton();

代码布局遵循“事件流”:首先,禁用开始按钮,然后加载图像(await确保UI保持响应),然后再次启用开始按钮。

在JavaScript中,使用回调,我想到了这一点:

disableStartButton();

var count = myRepository.length;

function loadImage(i) {
    if (i >= count) {
        write("success");
        enableStartButton();
        return;
    }

    myData = myRepository[i];
    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                return;
            }
            loadImage(i+1); 
        }
    );
}

loadImage(0);

我认为缺点很明显:我必须将循环重做为递归调用,应该在最后执行的代码在函数的中间,开始下载(loadImage(0))的代码在最底部,而且通常很难阅读和遵循。丑陋,我不喜欢它。

我确定我不是第一个遇到此问题的人,所以我的问题是:尽管使用异步代码和回调,我是否可以遵循一些完善的最佳实践模式来保持代码可读性?


您的“异步”调用必须按顺序进行吗?这是其他代码的简化版本吗?
伊兹卡塔2012年

@Izkata:原因仅仅是我想对远程服务器友好(=不要用数百个同时请求轰炸它)。这不是一成不变的要求。是的,它是简化的代码版本,LoadImageAsync实际上是Ext.Ajax.request对Sencha Touch 的调用。
Heinzi 2012年

1
大多数浏览器都不允许您重击服务器 -它们只是将请求排队,并在前一个请求完成时启动下一个请求。
伊兹卡塔2012年


神!这里有很多不好的建议。没有任何设计模式可以帮助您。查看async.jsasync.waterfall就是您的答案。
Salman von Abbas

Answers:


4

在使用普通js的情况下,使用C#5具有几乎相同的简洁性和表达性是极不可能的。编译器会为您编写所有样板文件,直到js运行时完成该工作为止,您仍然需要在此不时传递偶尔的回调。

但是,您可能并不总是希望将回调降低到线性代码的简单程度-抛出函数不必太丑陋,整个世界都在使用这种代码,并且在没有asyncand的情况下它们保持理智await

例如,使用高阶函数(我的js可能有点生锈):

// generic - this is a library function
function iterateAsync(iterator, action, onSuccess, onFailure) {
var item = iterator();
if(item == null) { // exit condition
    onSuccess();
    return;
}
action(item,
    function (success) {
        if(success)
            iterateAsync(iterator, action, onSuccess, onFailure);
        else
            onFailure();
    });
}


// calling code
var currentImage = 0;
var imageCount = 42;

// you know your library function expects an iterator with no params, 
// and an async action with the current item and its continuation as params
iterateAsync(
// this is your iterator
function () {   
    if(currentImage >= imageCount)
        return null;
    return "http://my/server/GetImage?" + (currentImage++);
},

// this is your action - coincidentally, no adaptor for the correct signature is necessary
LoadImageAsync,

// these are your outs
function () { console.log("All OK."); },
function () { console.log("FAILED!"); }
);

2

让我稍微解释一下为什么要这样做,但是我认为这可能接近您想要的?

function loadImages() {
   var countRemainingToLoad = 0;
   var failures = 0;

   myRepository.each(function (myData) {
      countRemainingToLoad++;

      LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) {
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                failures++;
            }
            countRemainingToLoad--;
            if (countRemainingToLoad == 0 && failures == 0) {
                enableStartButton();
            }
        }
    );
}

disableStartButton();
loadImages();

它首先关闭了尽可能多的同时进行的AJAX请求,然后等待所有请求完成,然后再启用“开始”按钮。这将比顺序等待更快,而且我认为这样更容易执行。

编辑:请注意,这假定您.each()有空,并且这myRepository是一个数组。请注意,如果不可用,请在此处使用哪种循环迭代代替它-该循环利用了回调的闭包属性。不过,我不确定您有什么可用,因为它LoadImageAsync似乎是专业图书馆的一部分-我在Google中看不到任何结果。


+1,我确实有.each()空缺,并且,既然您提到了,则严格地不必按顺序进行加载。我一定会尝试您的解决方案。(尽管我会接受vski的回答,因为它更接近于原始的,更笼统的问题。)
Heinzi 2012年

@Heinzi同意它有多么不同,但是(我认为)这也是一个很好的例子,说明不同的语言如何使用不同的方式来处理同一件事。如果将某件东西翻译成另一种语言时感到尴尬,则可能存在使用其他范式的简便方法。
Izkata 2012年

1

免责声明:此答案并不能专门回答您的问题,而是对以下问题的一般回答:“是否存在一些公认的最佳实践模式,即使使用异步代码和回调,我也可以遵循这些最佳模式来保持代码可读性?”

据我所知,没有“成熟的”模式可以解决这个问题。但是,我已经看到了两种避免嵌套回调的噩梦的方法。

1 /使用命名函数代替匿名回调

    function start() {
        mongo.findById( id, handleDatas );
    }

    function handleDatas( datas ) {
        // Handle the datas returned.
    }

这样,您可以通过在另一个函数中发送匿名函数的逻辑来避免嵌套。

2 /使用流管理库。我喜欢使用Step,但这只是一个优先事项。顺便说一下,这是LinkedIn所使用的一种。

    Step( {
        function start() {
            // the "this" magically sends to the next function.
            mongo.findById( this );
        },

        function handleDatas( el ) {
            // Handle the datas.
            // Another way to use it is by returning a value,
            // the value will be sent to the next function.
            // However, this is specific to Step, so look at
            // the documentation of the library you choose.
            return value;
        },

        function nextFunction( value ) {
            // Use the returned value from the preceding function
        }
    } );

当我使用大量嵌套回调时,我使用流管理库,因为当有大量代码使用它时,它更具可读性。


0

简而言之,JavaScript没有的语法糖await
但是将“ end”部分移到函数底部很容易;并且通过间接执行匿名函数,我们可以避免声明对其的引用。

disableStartButton();

(function(i, count) {
    var loadImage = arguments.callee;
    myData = myRepository[i];

    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (!success) {
                write("error loading image " + myData.Id);

            } else {
                myData.Image = data;
                if (i < count) {
                    loadImage(i + 1, count);

                } else {
                    write("success");
                    enableStartButton();
                    return;

                }

            }

        }
    );
})(0, myRepository.length);

您还可以将“结束”部分作为成功回调传递给函数。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.