Я инициализирую свое JS-приложение, вызывая несколько методов асинхронной загрузки ресурсов, каждый из которых зависит от предыдущих. Мое интуитивно понятное решение - вкладывать вызовы в функции обратного вызова (cba, cbb, cbc, cbd), вызывая их из внутренней части LoadA, LoadB, LoadC, LoadD соответственно после того, как они (LoadA, LoadB, LoadC, LoadD) были успешно завершены:

app.LoadA( function cba() {
        app.LoadB( function cbb() {
            ....        
            app.LoadC( function cbc() {
            ...           
                app.LoadD( function cbd() {
                    ...
                } );
            } );
        } );
    } );

LoadA( cb )
{
    let url = '.........'; // just an url
    let req = new XMLHttpRequest();
    req.open('GET', url, true);
    req.responseType = 'arraybuffer';

    let lh = this;

    req.onload = function ol(event)         
    {
        let arrayBuffer = req.response; 
        let loadedObject = Convert( arrayBuffer, ......... );
        cb( loadedObject ); // call, if successed!
    }
    req.send(null);
}
....

LoadA возвращается без загрузки объекта, поэтому LoadB должен ждать, пока встроенная функция LoadA загрузки LoadA не вызовет функцию обратного вызова cb и так далее.

Мне не нравится это решение для раскроя, так как его сложно просматривать и поддерживать.

Мой вопрос: есть ли другая (сфокусированная на «счастливом пути», более приятная, короткая, менее запутанная, более понятная и понятная) возможность достичь того же результата?

0
Trantor 30 Авг 2017 в 13:04

3 ответа

Лучший ответ

Чтобы избежать ада обратного вызова, вам нужно использовать Обещания .

Если функции loadA, ..., loadN возвращают обещания, вы просто вызываете .then() после каждого по порядку.

loadA().then(function() {
  loadB().then(function() {
    loadC().then(...
  });
});

Теперь важно помнить, что .then() возвращает Обещание, которое разрешается со значением его аргумента.

Поэтому, если loadA и loadB возвращают Promises, вы можете просто связать их в цепочку следующим образом:

loadA().then(function() {
  return loadB();
).then(...)

Что эквивалентно этому:

loadA().then(loadB).then(loadC).then(...)

Намного проще!

Если ваша функция не возвращает Promise, вам нужно обернуть их внутри одной с помощью вспомогательной функции.

function wrapInsidePromise(f) {
  return new Promise(function(resolve, reject) {
    f(function() {
      resolve();
    });
  });
}

var pLoadA = wrapInsidePromise(app.loadA);
var pLoadB = wrapInsidePromise(app.loadB);
...

pLoadA().then(pLoadB).then(...);

Более того, в ES6 вы можете использовать async/await, что позволяет использовать Promises асинхронно .

async function() {
  await loadA();
  await loadB();
  ...
  let finalResult = await loadN();
  ...
}
2
vassiliskrikonis 31 Авг 2017 в 10:32

Я бы разбил это, используя «Обещание» для каждого асинхронного вызова.

У MDN есть отличное объяснение того, как их использовать здесь: https: //developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

Код вашего приложения может выглядеть примерно так ...

app = {

    loadA: function() {
        return new Promise((resolve) => {
            console.log('Loading A');
            resolve();
        });
    },

    loadB: function () {
        return new Promise((resolve) => {
            console.log('Loading B');
            resolve();
        });
    },

    // For demonstration, I have coded loadC with a rejection if it fails
    // (Success is based on a random boolean) 
    loadC: function () {
        return new Promise((resolve, reject) => {
            console.log('Loading C');
            var success = Math.random() >= 0.5;
            if( success ){
                resolve();
            } else {
                reject('C did not load');
            }

        });
    },

    loadD: function () {
        return new Promise((resolve) => {
            console.log('Loading D');
            resolve();
        });
    }

};

// A global function for reporting errors
function logError(error) {
    // Record the error somehow
    console.log(error);
}

Теперь позвоните им в цепочку обещаний

app.loadA().then(app.loadB).then(app.loadC, logError).then(app.loadD);

Теперь, если в будущем вы решите изменить порядок, в котором вы хотите вызывать свои функции, вам вообще не нужно трогать код в функциях, потому что вы использовали обещания. Вы можете просто изменить строку, содержащую цепочку обещаний:

app.loadA().then(app.loadD).then(app.loadB).then(app.loadC, logError);

(В приведенном выше примере предполагается, что вы можете изменить методы приложения, чтобы они работали как обещания. Если приложение является сторонним разработчиком, решение, которое вы не можете редактировать, будет отличаться)

1
Martin Joiner 31 Авг 2017 в 10:15

Вот сравнение «ада обратного вызова» и лучшего кода, который вы можете получить с помощью async/await:

// dummy implementations: 
//  these functions call cb with value 1 to 4, after 100ms
const loadA = (cb) => setTimeout(_ => cb(1), 100);
const loadB = (cb) => setTimeout(_ => cb(2), 100);
const loadC = (cb) => setTimeout(_ => cb(3), 100);
const loadD = (cb) => setTimeout(_ => cb(4), 100);

function callbackHell() {
    loadA( function cba(a) {
        loadB( function cbb(b) {
            loadC( function cbc(c) {
                loadD( function cbd(d) {
                    console.log([a, b, c, d]);
                });
            });
        });
    });
}

async function nicerCode() {
    const res = [
        await new Promise(loadA),
        await new Promise(loadB),
        await new Promise(loadC),
        await new Promise(loadD)
    ];
    console.log(res);
}

callbackHell();
nicerCode();
.as-console-wrapper { max-height: 100% !important; top: 0; }
2
trincot 30 Авг 2017 в 10:24