Làm thế nào để bạn giữ mã với tiếp tục / gọi lại có thể đọc được?


10

Tóm tắt: Có một số mẫu thực hành tốt nhất được thiết lập tốt mà tôi có thể làm theo để giữ cho mã của mình có thể đọc được mặc dù sử dụng mã không đồng bộ và gọi lại không?


Tôi đang sử dụng thư viện JavaScript thực hiện nhiều nội dung không đồng bộ và phụ thuộc nhiều vào các cuộc gọi lại. Có vẻ như việc viết một phương thức "tải A, tải B, ..." đơn giản trở nên khá phức tạp và khó thực hiện khi sử dụng mẫu này.

Hãy để tôi đưa ra một ví dụ (giả định). Giả sử tôi muốn tải một loạt các hình ảnh (không đồng bộ) từ một máy chủ web từ xa. Trong C # / async, tôi sẽ viết một cái gì đó như thế này:

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

Bố cục mã theo "luồng sự kiện": Đầu tiên, nút bắt đầu bị vô hiệu hóa, sau đó hình ảnh được tải ( awaitđảm bảo rằng giao diện người dùng vẫn phản hồi) và sau đó nút bắt đầu được bật lại.

Trong JavaScript, sử dụng các cuộc gọi lại, tôi đã đưa ra điều này:

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

Tôi nghĩ rằng những hạn chế là rõ ràng: Tôi đã phải thực hiện lại vòng lặp thành một cuộc gọi đệ quy, mã được cho là được thực thi cuối cùng nằm ở đâu đó ở giữa hàm, mã bắt đầu tải xuống ( loadImage(0)) ở dưới cùng, và nó thường khó đọc và theo dõi hơn nhiều. Nó xấu và tôi không thích nó.

Tôi chắc chắn rằng tôi không phải là người đầu tiên gặp phải vấn đề này, vì vậy câu hỏi của tôi là: Có một số mẫu thực hành tốt nhất được thiết lập tốt mà tôi có thể làm theo để giữ cho mã của mình có thể đọc được mặc dù sử dụng mã không đồng bộ và gọi lại không?


Có một lý do cụ thể nào cho các cuộc gọi "không đồng bộ" của bạn phải được thực hiện tuần tự không? Đây có phải là một phiên bản đơn giản hóa của một số mã khác?
Izkata

@Izkata: Lý do là tôi muốn trở nên tốt đẹp với máy chủ từ xa (= không bắn phá nó với hàng trăm yêu cầu đồng thời). Đó không phải là một yêu cầu đặt trong đá. Vâng, đó là một phiên bản mã đơn giản, LoadImageAsynctrên thực tế là một cuộc gọi đến Ext.Ajax.requestSencha Touch.
Heinzi

1
Hầu hết các trình duyệt sẽ không cho phép bạn đập máy chủ bằng mọi cách - họ chỉ xếp hàng các yêu cầu và bắt đầu yêu cầu tiếp theo khi một trong những hoàn thành trước đó hoàn thành.
Izkata


Chúa Trời! Rất nhiều lời khuyên tồi ở đây. Không có số lượng mẫu thiết kế sẽ giúp bạn. Nhìn vào async.js , async.waterfalllà câu trả lời của bạn.
Salman von Abbas

Câu trả lời:


4

Rất khó có khả năng bạn có thể đạt được với js đơn giản có cùng mức độ đồng nhất và biểu cảm khi làm việc với các cuộc gọi lại mà C # 5 có. Trình biên dịch thực hiện công việc bằng cách viết tất cả các mẫu soạn sẵn cho bạn và cho đến khi thời gian chạy của js sẽ làm điều đó, bạn vẫn sẽ phải vượt qua một cuộc gọi lại thỉnh thoảng ở đây và ở đó.

Tuy nhiên, bạn có thể không luôn luôn muốn mang callbacks xuống mức của sự đơn giản của mã tuyến tính - ném chức năng xung quanh không phải là xấu xí, có một toàn bộ thế giới làm việc với loại mã này, và họ tiếp tục lành mạnh mà không cần asyncawait.

Ví dụ: sử dụng các hàm bậc cao hơn (js của tôi có thể hơi rỉ sét):

// 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

Mất một chút để giải mã lý do tại sao bạn làm theo cách này, nhưng tôi nghĩ rằng điều này có thể gần với những gì bạn muốn?

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

Trước tiên, nó sẽ tắt nhiều yêu cầu AJAX như nó có thể thực hiện đồng thời và nó sẽ đợi cho đến khi tất cả chúng được hoàn thành trước khi bật nút Bắt đầu. Điều này sẽ nhanh hơn một sự chờ đợi tuần tự, và, tôi nghĩ, dễ theo dõi hơn nhiều.

EDIT : Lưu ý rằng điều này giả sử bạn có .each()sẵn và đó myRepositorylà một mảng. Hãy cẩn thận với việc lặp lại vòng lặp mà bạn sử dụng ở đây thay cho điều đó, nếu nó không có sẵn - điều này tận dụng các thuộc tính đóng cho cuộc gọi lại. Tuy nhiên, tôi không chắc những gì bạn có sẵn vì LoadImageAsyncdường như là một phần của thư viện chuyên ngành - tôi không thấy kết quả nào trong Google.


+1, tôi đã có .each()sẵn và bây giờ bạn đề cập đến nó, không nhất thiết phải thực hiện tải tuần tự. Tôi chắc chắn sẽ thử giải pháp của bạn. (Mặc dù tôi sẽ chấp nhận câu trả lời của vski, vì nó gần với câu hỏi ban đầu hơn, tổng quát hơn.)
Heinzi

@Heinzi Đồng ý về sự khác biệt của nó, nhưng (tôi nghĩ) đây cũng là một ví dụ điển hình về cách các ngôn ngữ khác nhau có cách khác nhau để xử lý cùng một điều. Nếu có điều gì đó cảm thấy khó xử khi dịch nó sang một ngôn ngữ khác, có lẽ có một cách dễ dàng hơn để làm điều đó bằng cách sử dụng một mô hình khác.
Izkata

1

Tuyên bố miễn trừ trách nhiệm: câu trả lời này không trả lời cụ thể vấn đề của bạn, đó là câu trả lời chung cho câu hỏi: "Có một số mẫu thực hành tốt nhất được thiết lập tốt mà tôi có thể làm theo để giữ cho mã của mình có thể đọc được mặc dù sử dụng mã không đồng bộ và gọi lại không?"

Từ những gì tôi biết, không có mô hình "được thiết lập tốt" để xử lý việc này. Tuy nhiên, tôi đã thấy hai loại phương pháp được sử dụng để tránh những cơn ác mộng gọi lại lồng nhau.

1 / Sử dụng các chức năng được đặt tên thay vì gọi lại ẩn danh

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

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

Bằng cách này, bạn tránh được việc lồng nhau bằng cách gửi logic của hàm ẩn danh trong hàm khác.

2 / Sử dụng thư viện quản lý dòng chảy. Tôi thích sử dụng Bước , nhưng đó chỉ là vấn đề ưu tiên. Nhân tiện, đó là cái mà LinkedIn sử dụng.

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

Tôi sử dụng thư viện quản lý luồng khi tôi sử dụng nhiều hàm gọi lại lồng nhau, bởi vì nó dễ đọc hơn rất nhiều khi có nhiều mã sử dụng nó.


0

Nói một cách đơn giản, JavaScript không có đường cú pháp await.
Nhưng di chuyển phần "kết thúc" vào dưới cùng của chức năng là dễ dàng; và với một hàm ẩn danh thực thi, chúng ta có thể tránh việc khai báo một tham chiếu đến nó.

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

Bạn cũng có thể chuyển phần "kết thúc" dưới dạng gọi lại thành công cho hàm.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.