Что такое «ад обратных вызовов» и как и почему RX решает эту проблему?


113

Может ли кто-нибудь дать четкое определение вместе с простым примером, объясняющим, что такое «ад обратных вызовов» для тех, кто не знает JavaScript и node.js?

Когда (в каких настройках) возникает "проблема с обратным вызовом"?

Почему это происходит?

Всегда ли «ад обратных вызовов» связан с асинхронными вычислениями?

Или «ад обратных вызовов» может происходить и в однопоточном приложении?

Я прошел реактивный курс на Coursera, и Эрик Мейер в одной из своих лекций сказал, что RX решает проблему «ада обратных вызовов». Я спросил, что такое «ад обратных вызовов» на форуме Coursera, но не получил четкого ответа.

Объяснив «ад обратных вызовов» на простом примере, не могли бы вы также показать, как RX решает «проблему ада обратных вызовов» на этом простом примере?

Ответы:


136

1) Что такое «ад обратных вызовов» для тех, кто не знает javascript и node.js?

В этом другом вопросе есть несколько примеров ада обратного вызова Javascript: как избежать длительного вложения асинхронных функций в Node.js

Проблема в Javascript заключается в том, что единственный способ «заморозить» вычисление и заставить «остальную часть» выполнить последнюю (асинхронно) - это поместить «остальную часть» в обратный вызов.

Например, скажем, я хочу запустить код, который выглядит так:

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...

Что произойдет, если теперь я хочу сделать функции getData асинхронными, что означает, что у меня есть возможность запустить другой код, пока я жду, пока они вернут свои значения? В Javascript единственный способ - переписать все, что касается асинхронного вычисления, используя стиль передачи продолжения :

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){ 
            ...
        });
    });
});

Не думаю, что мне нужно кого-то убеждать в том, что эта версия уродливее предыдущей. :-)

2) Когда (в каких настройках) возникает "проблема с обратным вызовом"?

Когда в вашем коде много функций обратного вызова! Чем больше их в вашем коде, тем труднее с ними работать, и становится особенно плохо, когда вам нужно делать циклы, блоки try-catch и тому подобное.

Например, насколько мне известно, в JavaScript единственный способ выполнить ряд асинхронных функций, одна из которых запускается после предыдущих возвратов, - это использование рекурсивной функции. Вы не можете использовать цикл for.

// we would like to write the following
for(var i=0; i<10; i++){
    doSomething(i);
}
blah();

Вместо этого нам, возможно, придется написать:

function loop(i, onDone){
    if(i >= 10){
        onDone()
    }else{
        doSomething(i, function(){
            loop(i+1, onDone);
        });
     }
}
loop(0, function(){
    blah();
});

//ugh!

Количество вопросов, которые мы получаем здесь, в StackOverflow, спрашивая, как это сделать, является свидетельством того, насколько это запутанно :)

3) Почему это происходит?

Это происходит потому, что в JavaScript единственный способ отложить вычисление, чтобы оно выполнялось после возврата асинхронного вызова, - это поместить отложенный код в функцию обратного вызова. Вы не можете откладывать код, который был написан в традиционном синхронном стиле, поэтому вы повсюду получаете вложенные обратные вызовы.

4) Или «ад обратного вызова» может происходить и в однопоточном приложении?

Асинхронное программирование связано с параллелизмом, а однопоточное - с параллелизмом. Эти две концепции на самом деле не одно и то же.

Вы все еще можете иметь параллельный код в однопоточном контексте. Фактически, JavaScript, королева ада обратных вызовов, является однопоточным.

В чем разница между параллелизмом и параллелизмом?

5) Не могли бы вы также показать, как RX решает «проблему ада обратного вызова» на этом простом примере.

Я ничего не знаю о RX в частности, но обычно эта проблема решается путем добавления встроенной поддержки асинхронных вычислений в языке программирования. Реализации могут различаться и включать: async, генераторы, сопрограммы и callcc.

В Python мы можем реализовать этот предыдущий пример цикла с помощью чего-то вроде:

def myLoop():
    for i in range(10):
        doSomething(i)
        yield

myGen = myLoop()

Это не полный код, но идея состоит в том, что "yield" приостанавливает наш цикл for, пока кто-нибудь не вызовет myGen.next (). Важно то, что мы все еще могли писать код, используя цикл for, без необходимости выворачивать логику «наизнанку», как мы должны были сделать в этой рекурсивной loopфункции.


Значит, ад обратного вызова может произойти только в асинхронной настройке? Если мой код полностью синхронизирован (т.е. без параллелизма), тогда "ад обратного вызова" не может произойти, если я правильно понимаю ваш ответ, верно?
jhegedus 02

Ад обратного вызова больше связан с тем, насколько раздражает код, использующий стиль передачи продолжения. Теоретически вы все равно можете переписать все свои функции, используя стиль CPS, даже для обычной программы (в статье в Википедии есть несколько примеров), но по уважительной причине большинство людей этого не делают. Обычно мы используем стиль передачи продолжения только в том случае, если мы вынуждены, как в случае с асинхронным программированием Javascript.
hugomg 02

Кстати, я искал в Google реактивные расширения, и у меня сложилось впечатление, что они больше похожи на библиотеку Promise, а не на расширение языка, представляющее синтаксис async. Обещания помогают справиться с вложением обратных вызовов и обработкой исключений, но они не так удобны, как расширения синтаксиса. Цикл for по-прежнему утомляет код, и вам все равно нужно перевести код из синхронного стиля в стиль обещания.
hugomg 02

1
Я должен прояснить, как RX в целом работает лучше. RX декларативен. Вы можете объявить, как программа будет реагировать на события, когда они позже произойдут, не влияя на другую логику программы. Это позволяет отделить код основного цикла от кода обработки событий. Вы можете легко обрабатывать такие детали, как порядок асинхронных событий, которые являются кошмаром при использовании переменных состояния. Я обнаружил, что RX была самой чистой реализацией для выполнения нового сетевого запроса после возврата 3 сетевых ответов или для обработки ошибок всей цепочки, если один из них не возвращается. Затем он может перезагрузиться и ждать тех же 3 событий.
colintheshots

Еще один связанный комментарий: RX - это в основном монада продолжения, которая относится к CPS, если я не ошибаюсь, это также может объяснить, как / почему RX хорош для проблемы обратного вызова / ада.
jhegedus

30

Просто ответьте на вопрос: не могли бы вы также показать, как RX решает «адскую проблему обратного вызова» на этом простом примере?

Магия есть flatMap. Мы можем написать следующий код в Rx для примера @hugomg:

def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
         .flatMap(y -> Observable[Z])
         .map(z -> ...)...

Это как если бы вы пишете несколько синхронных кодов FP, но на самом деле вы можете сделать их асинхронными с помощью Scheduler.


26

Чтобы ответить на вопрос, как Rx решает ад обратных вызовов :

Сначала давайте снова опишем ад обратных вызовов.

Представьте себе случай, когда мы должны выполнить http, чтобы получить три ресурса - человека, планету и галактику. Наша цель - найти галактику, в которой живет человек. Сначала мы должны найти человека, затем планету, а затем галактику. Это три обратных вызова для трех асинхронных операций.

getPerson(person => { 
   getPlanet(person, (planet) => {
       getGalaxy(planet, (galaxy) => {
           console.log(galaxy);
       });
   });
});

Каждый обратный вызов является вложенным. Каждый внутренний обратный вызов зависит от своего родителя. Это приводит к аду обратного вызова в стиле «пирамиды гибели» . Код выглядит как знак>.

Чтобы решить эту проблему в RxJs, вы можете сделать что-то вроде этого:

getPerson()
  .map(person => getPlanet(person))
  .map(planet => getGalaxy(planet))
  .mergeAll()
  .subscribe(galaxy => console.log(galaxy));

С помощью оператора mergeMapAKA flatMapвы можете сделать его более лаконичным:

getPerson()
  .mergeMap(person => getPlanet(person))
  .mergeMap(planet => getGalaxy(planet))
  .subscribe(galaxy => console.log(galaxy));

Как видите, код сплющен и содержит единую цепочку вызовов методов. У нас нет «пирамиды гибели».

Следовательно, можно избежать ада обратного вызова.

Если вам интересно, обещания - это еще один способ избежать ада обратных вызовов, но обещания нетерпеливы , а не ленивы, как наблюдаемые, и (вообще говоря) вы не можете их так легко отменить.


Я не JS-разработчик, но это простое объяснение
Омар Бешари

15

Ад обратного вызова - это любой код, в котором использование обратных вызовов функций в асинхронном коде становится неясным или трудным для понимания. Как правило, когда существует более одного уровня косвенного обращения, код, использующий обратные вызовы, может стать труднее отслеживать, труднее рефакторинг и труднее тестировать. Запах кода - это несколько уровней отступов из-за передачи нескольких уровней функциональных литералов.

Это часто случается, когда поведение имеет зависимости, то есть когда A должно произойти до того, как B должно произойти до C. Тогда вы получите такой код:

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});

Если в вашем коде много таких поведенческих зависимостей, это может быстро стать проблемой. Особенно если ветки ...

a({
    parameter : someParameter,
    callback : function(status) {
        if (status == states.SUCCESS) {
          b(function(status) {
              if (status == states.SUCCESS) {
                 c(function(status){
                     if (status == states.SUCCESS) {
                         // Not an exaggeration. I have seen
                         // code that looks like this regularly.
                     }
                 });
              }
          });
        } elseif (status == states.PENDING {
          ...
        }
    }
});

Так не пойдет. Как мы можем заставить асинхронный код выполняться в определенном порядке без необходимости передавать все эти обратные вызовы?

RX - это сокращение от «реактивные расширения». Я не использовал его, но Google предлагает, чтобы это основанная на событиях структура, что имеет смысл. События - это общий шаблон, заставляющий код выполняться по порядку без создания хрупкой связи . Вы можете заставить C прослушивать событие 'bFinished', которое происходит только после того, как B вызывается для прослушивания 'aFinished'. Затем вы можете легко добавить дополнительные шаги или расширить такое поведение и легко проверить , выполняется ли ваш код по порядку, просто транслируя события в вашем тестовом примере.


1

Ад обратного вызова означает, что вы находитесь внутри обратного вызова внутри другого обратного вызова, и он переходит к n-му вызову, пока ваши потребности не будут удовлетворены.

Давайте разберемся на примере поддельного вызова ajax с использованием API установки тайм-аута, допустим, у нас есть API рецептов, нам нужно загрузить все рецепты.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
            }, 1500);
        }
        getRecipe();
    </script>
</body>

В приведенном выше примере через 1,5 секунды, когда истечет время таймера, будет выполнен внутренний код обратного вызова, другими словами, через наш поддельный вызов ajax все рецепты будут загружены с сервера. Теперь нам нужно загрузить данные конкретного рецепта.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Чтобы загрузить данные конкретного рецепта, мы написали код внутри нашего первого обратного вызова и передали идентификатор рецепта.

Теперь предположим, что нам нужно загрузить все рецепты того же издателя рецепта с идентификатором 7638.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                    setTimeout(publisher=>{
                        const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
                        console.log(recipe2);
                    }, 1500, recipe.publisher);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Чтобы полностью удовлетворить нашу потребность, а именно загрузить все рецепты имени издателя suru, мы написали код внутри нашего второго обратного вызова. Понятно, что мы написали цепочку обратных вызовов, которая называется адом обратных вызовов.

Если вы хотите избежать ада обратных вызовов, вы можете использовать Promise, который является функцией js es6, каждое обещание принимает обратный вызов, который вызывается, когда обещание полностью заполнено. Обратный вызов обещания имеет два варианта: разрешен или отклонен. Предположим, что ваш вызов API прошел успешно, вы можете вызвать метод решения и передать данные через разрешение , вы можете получить эти данные с помощью then () . Но если ваш API не работает, вы можете использовать reject, использовать catch, чтобы поймать ошибку. Помните, что обещание всегда используйте тогда для решения и ловите для отклонения

Давайте решим предыдущую адскую проблему обратного вызова с помощью обещания.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        getIds.then(IDs=>{
            console.log(IDs);
        }).catch(error=>{
            console.log(error);
        });
    </script>
</body>

Теперь загрузите конкретный рецепт:

<body>
    <script>
        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }
        getIds.then(IDs=>{
            console.log(IDs);
            return getRecipe(IDs[2]);
        }).
        then(recipe =>{
            console.log(recipe);
        })
        .catch(error=>{
            console.log(error);
        });
    </script>
</body>

Теперь мы можем написать другой вызов метода allRecipeOfAPublisher, например getRecipe, который также будет возвращать обещание, и мы можем написать еще один метод then (), чтобы получить обещание разрешения для allRecipeOfAPublisher, я надеюсь, что на этом этапе вы сможете сделать это самостоятельно.

Итак, мы узнали, как создавать и использовать обещания, а теперь давайте упростим использование обещаний с помощью async / await, представленного в es8.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }

        async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

        getRecipesAw();
    </script>
</body>

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

  async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

Чтобы использовать ожидание, нам понадобится асинхронная функция, мы можем вернуть обещание, поэтому используйте then для обещания разрешения и cath для отклонения обещания

из приведенного выше примера:

 async function getRecipesAw(){
            const IDs = await getIds;
            const recipe = await getRecipe(IDs[2]);
            return recipe;
        }

        getRecipesAw().then(result=>{
            console.log(result);
        }).catch(error=>{
            console.log(error);
        });

0

Один из способов избежать ада обратного вызова - использовать FRP, который является «улучшенной версией» RX.

Я начал использовать FRP недавно, потому что нашел хорошую его реализацию под названием Sodium( http://sodium.nz/ ).

Типичный код выглядит так (Scala.js):

def render: Unit => VdomElement = { _ =>
  <.div(
    <.hr,
    <.h2("Note Selector"),
    <.hr,
    <.br,
    noteSelectorTable.comp(),
    NoteCreatorWidget().createNewNoteButton.comp(),
    NoteEditorWidget(selectedNote.updates()).comp(),
    <.hr,
    <.br
  )
}

selectedNote.updates()- это a, Streamкоторое срабатывает, если selectedNode(а именно Cell) изменяется, NodeEditorWidgetthen обновляется соответственно.

Таким образом, в зависимости от содержимого selectedNode Cell, редактируемый в данный момент файл Note.

Этот код полностью избегает Callback-ов, почти Cacllback-ы выталкиваются на «внешний уровень» / «поверхность» приложения, где логика обработки состояния взаимодействует с внешним миром. Для распространения данных внутри логики обработки внутреннего состояния (которая реализует конечный автомат) не требуются обратные вызовы.

Полный исходный код здесь

Приведенный выше фрагмент кода соответствует следующему простому примеру создания / отображения / обновления:

введите описание изображения здесь

Этот код также отправляет обновления на сервер, поэтому изменения в обновленных объектах автоматически сохраняются на сервере.

Вся обработка событий осуществляется с помощью Streams и Cells. Это концепции FRP. Обратные вызовы необходимы только там, где логика FRP взаимодействует с внешним миром, например, ввод пользователем, редактирование текста, нажатие кнопки, возврат вызова AJAX.

Поток данных описывается явно, декларативно с использованием FRP (реализованного библиотекой Sodium), поэтому для описания потока данных не требуется никакой логики обработки событий / обратного вызова.

FRP (который является более «строгой» версией RX) - это способ описания графа потока данных, который может содержать узлы, содержащие состояние. События запускают изменения состояния в состоянии, содержащем узлы (называемые Cells).

Sodium - это библиотека FRP более высокого порядка, что означает, что с помощью примитива flatMap/ switchможно изменить порядок графа потока данных во время выполнения.

Я рекомендую заглянуть в книгу Sodium , в ней подробно объясняется, как FRP избавляется от всех обратных вызовов, которые не являются существенными для описания логики потока данных, которая связана с обновлением состояния приложений в ответ на некоторые внешние стимулы.

При использовании FRP необходимо сохранять только те обратные вызовы, которые описывают взаимодействие с внешним миром. Другими словами, поток данных описывается функционально / декларативно, когда используется структура FRP (например, Sodium) или когда используется структура, подобная FRP (например, RX).

Sodium также доступен для Javascript / Typescript.


-3

Если у вас нет знаний о обратном вызове и обратном вызове ада, нет проблем. Дело в том, что обратный вызов и обратный вызов ада. Например: обратный вызов ада похож на то, что мы можем сохранить класс внутри класса. Как вы слышали о том, что вложенный в C, язык C ++. Вложенный Означает, что класс внутри другого класса.


Ответ будет более полезным, если он будет содержать фрагмент кода, показывающий, что такое «ад обратного вызова», и тот же фрагмент кода с Rx после удаления «ада обратного вызова»
rafa

-4

Используйте jazz.js https://github.com/Javanile/Jazz.js

это упростить так:

    // запускаем последовательную задачу в цепочке
    jj.script ([
        // первая задача
        function (next) {
            // в конце этого процесса 'next' указывает на вторую задачу и запускает ее 
            callAsyncProcess1 (далее);
        },
      // вторая задача
      function (next) {
        // в конце этого процесса 'next' указывает на третью задачу и запускает ее 
        callAsyncProcess2 (далее);
      },
      // третья задача
      function (next) {
        // в конце этого процесса 'следующий' указывает на (если есть) 
        callAsyncProcess3 (далее);
      },
    ]);


считайте ультракомпактным, как этот github.com/Javanile/Jazz.js/wiki/Script-showcase
cicciodarkast
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.