연속체 / 콜백이있는 코드를 어떻게 읽을 수있게 유지합니까?


10

요약 : 비동기 코드와 콜백을 사용하더라도 코드를 읽을 수 있도록하기 위해 잘 확립 된 모범 사례 패턴이 있습니까?


비동기 적으로 많은 작업을 수행하고 콜백에 크게 의존하는 JavaScript 라이브러리를 사용하고 있습니다. 간단한 "load A, load B, ..."메소드를 작성하는 것은이 패턴을 사용하는 것이 매우 복잡하고 따르기가 어려워 보입니다.

(구상 된) 예를 들어 보도록하겠습니다. 원격 웹 서버에서 (비동기 적으로) 많은 이미지를로드하려고한다고 가정 해 봅시다. C # / 비동기에서는 다음과 같이 작성합니다.

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();

코드 레이아웃은 "이벤트 흐름"을 따릅니다. 먼저 시작 버튼이 비활성화 된 다음 이미지가로드되고 ( awaitUI가 응답 상태를 유지함) 시작 버튼이 다시 활성화됩니다.

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)는 맨 아래에 있습니다. 일반적으로 읽고 따르는 것이 훨씬 어렵습니다. 못 생겼어요.

나는이 문제에 처한 첫 번째 사람이 아니라고 확신하므로 내 질문은 : 비동기 코드와 콜백을 사용하더라도 코드를 읽을 수 있도록 유지할 수있는 잘 확립 된 모범 사례 패턴이 있습니까?


"비동기"호출이 순차적으로 수행되어야하는 특별한 이유가 있습니까? 이것은 다른 코드의 단순화 된 버전입니까?
이즈 카타

@ Izkata : 그 이유는 내가 원격 서버에 친절하기를 원했기 때문입니다 (= 수백 개의 동시 요청으로 공격하지 마십시오). 그것은 돌에 설정된 요구 사항이 아닙니다. 예, 코드의 단순화 된 버전이며 LoadImageAsync실제로 Ext.Ajax.requestSencha Touch를 호출합니다 .
Heinzi

1
대부분의 브라우저 어쨌든 서버를 망치는 것을 허용하지 않습니다 . 요청을 대기열에 넣고 이전 요청 중 하나가 완료되면 다음 요청을 시작하기 만합니다.
이즈 카타


하느님! 여기에 많은 나쁜 조언이 있습니다. 많은 디자인 패턴이 도움이되지 않습니다. 조사 async.js은 , async.waterfall당신의 대답이다.
Salman von Abbas

답변:


4

C # 5의 콜백 작업과 동일한 수준의 간결성과 표현력을 일반 js로 달성 할 가능성은 거의 없습니다. 컴파일러는 모든 상용구를 작성하는 작업을 수행하며 js 런타임이이를 수행 할 때까지 가끔씩 콜백을 전달해야합니다.

그러나, 당신은 항상 선형 코드의 단순함의 수준까지 콜백을 가지고 싶어하지 않을 수 있습니다 - 던지는 기능을 주위에 추한 될 필요가 없습니다, 거기있어 전 세계의 코드 이런 종류의 작업, 그리고 그들이 않고 제정신을 유지 async하고 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

@Heinzi 그것이 얼마나 다른지에 동의했지만, 이것은 다른 언어가 같은 것을 처리하는 다른 방법을 갖는 방법의 좋은 예라고 생각합니다. 다른 언어로 번역 할 때 어색한 느낌이 든다면 다른 패러다임을 사용하여 더 쉬운 방법이있을 것입니다.
이즈 카타

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.
그러나 "끝"부분을 함수의 맨 아래로 옮기는 것은 쉽습니다. 익명 함수를 즉시 실행하면 이에 대한 참조를 선언하지 않아도됩니다.

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);

"끝"부분을 성공 콜백으로 함수에 전달할 수도 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.