В последнее время мне приходилось много работать с nodejs, и проект требовал использования множества остальных сервисов. Я получил много асинхронной боли.

Поначалу я плохо понимал обещания, поэтому не использовал их. Дела шли так плохо, пока я не провел небольшое исследование и не понял, как работать с большим количеством асинхронного кода, не теряя при этом рассудка.

Асинхронный код в стиле обратного вызова имеет много проблем, наиболее известным из которых является очевидный факт, что отступ может легко стать БОЛЬШИМ, если вам нужно связать несколько асинхронных фрагментов кода. Давайте представим, что у нас есть функция prompt(), которая асинхронно показывает пользователю сообщения, запрашивающие некоторый текст в модальном вводе, а затем передает этот текст функции обратного вызова в качестве значения. Мы могли бы сделать систему поверх нее, чтобы запрашивать у пользователя ее имя, фамилию и возраст:

prompt.get('enter your name' , function(err, value ){
    var name = value;
    prompt.get('enter your surname' , function(err , value){
        var surname = value;
        promt.get('enter your date' , function(err , value ){
            doSomething(name , surname , value);
        });
    });
});

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

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

var getName = function(surnameCallback , ageCallback ){
    prompt.get('enter the name' , function(err , value){
        data = {};    
        data.name = value;
        surnameCallback(ageCallback , data);
    });
}
var getSurname = function(cb , data){
    prompt.get('enter the surname' , function(err , value ){
        data.surname = value;
        cb(data);
    });
}
vat getAge = function(data ){
    prompt.get('enter the date' , function(err , value){
        doSomething( data.name , data.surname , value);
    });
};
//now all the code is used by the next line: no indentation hell at //all
getName( getSurname , getAge );

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

getName(getSurname, getNationality, getAge, getSomething... );

Однако это решает только проблему чтения кода и отступов. Теперь читать и понимать код действительно проще. Однако повторно использовать или модифицировать его по-прежнему сложно, что очень важно для фрагмента кода.

В этой версии кода getName кодируется особым образом, поэтому вся система может работать, чтобы получить только имя, фамилию и возраст пользователя. Существует иллюзия, что getSurname, например, является допустимым фрагментом кода, который можно использовать повторно, и что другие системы, запрашивающие большее или меньшее количество полей, могут легко использовать его. На самом деле это не так, как мы увидим, и это основная проблема стиля обратного вызова.

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

var getZipCode = function(cb , data){
    prompt.get('enter the zip code' , function(err , value ){
        data.zipCode = value;
        cb(data);
    });
}

Что кажется довольно полезным и автономным.

Однако вторым шагом должен стать некоторый рефакторинг getName(), чтобы заставить getZipCode() работать с остальными функциями. Например что-то вроде:

var getName = function(surnameCallback , zipCodeCallback, ageCallback ){
    prompt.get('enter the name' , function(err , value){
        data.name = value;
        surnameCallback(zipCodeCallback , ageCallback , data);
    } );
}

Но чтобы использовать zipCodeCallback, нам нужно, чтобы surnameCallback теперь принимал два обратных вызова, иначе у нас нет никакого способа поместить zipCodeCallback в цепочку и заставить его работать. Теперь getSurname может выглядеть примерно так:

var getSurname = function(zipCodeCallback, ageCallback , data){
    prompt.get('enter the surname' , function(err , value ){
        data.surname = value;
        zipCodeCallback( ageCallback , data);
    });
}

Нам нужно было модифицировать код двух функций, чтобы добавить еще одно поле в систему. Это может быть вообще невыполнимо, если есть другой код, фактически вызывающий эти функции, и мы должны затем дублировать новые функции с новым добавленным поведением. Добавление и удаление полей в системе приводит к большому количеству проблем.

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

На самом деле есть еще одна проблема, о которой мы не говорили: функция doSomething жестко запрограммирована в getAge(). Это плохо, но чтобы избежать этого, нам нужно передать другую функцию обратного вызова в качестве параметра через всю цепочку и вызвать этот обратный вызов в конце вместо жестко запрограммированной функции doSomething(). На самом деле, мы должны передать обратный вызов getName для каждой операции, которую мы хотим выполнить впоследствии.

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

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

promptAsync('your name')
.then(function(val){
    data.name = val;
    return promptAsync('your surname');
})
.catch(console.log)
.then(function(val){
    data.surname = val;
    return return promptAsync('your age');
})
.catch(console.log)
.then(function(val){
    data.age = val;
})
.catch(console.log)
.then(anyFunctionThatUsesData);

Итак, если мы хотим добавить zipCode, нам не нужно изменять какой-либо код, который получает имя, фамилию или возраст, мы просто передаем еще один вызов promptAsync() в промисе:

promptAsync('your name')
.then(function(val){
    data.name = val;
    return promptAsync('your surname');
})
.catch(console.log)
.then(function(val){
    data.surname = val;
    return return promptAsync('your age');
})
.catch(console.log)
.then(function(val){
    data.age = val;
})
.catch(console.log)
.then(function(){
    /*Here we add zip code to the chain*/
    return promptAsync('your zip code');
})
.catch(console.log)
.then(function(val){
    data.zipCode = val;
})
.then(anyFunctionThatUsesData);

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

...
.then(function(val){
    data.zipCode = val;
})
.then(function(){
    return data;
})
.then(anyFunctionThatTakesDataAsParameter);

Конечно, мы модифицируем код, чтобы внести эти изменения. На самом деле, когда вы добавляете функциональность в систему, часть кода приходится модифицировать или писать с нуля. Однако мы не возимся с самими кирпичиками нашей системы, например, модифицируя getSurname() для передачи дополнительного обратного вызова. Должно быть имя, фамилия и возраст — кортеж, относящийся к нашей системе и полезный для различных частей системы, мы могли бы просто написать такую ​​функцию, как:

var getUserData = function(){
    var data = {};
    return promptAsync('your name')
    .catch(console.log)
    .then(function(val){
        data.name = val;
        return promptAsync('your surname');
    })
    .catch(console.log)
    .then(function(val){
        data.surname = val;
        return promptAsync('your age');
    })
    .catch(console.log)
    .then(function(val){
        data.age = val;
        return data;
    });
}

И мы могли бы повторно использовать его снова и снова с любой операцией, например:

getUserData.then(console.log);
getUserData.then(writeToDatabase);
getUserData.then(sendDataToRestService); 

Без возни с передачей обратных вызовов в конец цепочки обратных вызовов и без дублирования кода. Нам нужна суперверсия функции, чтобы получить почтовый индекс, и мы хотим, чтобы ее можно было использовать повторно? Просто:

var getCompleteUserData = function(){
    
    var data = {};
    return getUserData()
    .catch(console.log)
    .then(function(result){
        data = result;
        return promptAsync('your zip code');
    })
    .catch(console.log)
    .then(function(value){
        data.zipCode = value;
        return data;
    });
}

и getCompleteUserData() получает zipCode, не связываясь с кодом getUserData(), и по-прежнему может использоваться повторно.

Итак, всякий раз, когда вы думаете, что можете очистить ад асинхронных обратных вызовов, просто рефакторингом в более мелкие функции, имейте в виду, что функции нельзя использовать повторно и что любая другая операция, которую вы хотите выполнить с результатом ваших вызовов, будет нужно возиться с самим кодом и передавать дополнительные обратные вызовы. Если вашей системе просто нужно асинхронно получить токен аутентификации из файловой системы, а затем подключиться к службе REST, возможно, вы можете просто вызвать обратный вызов внутри другого, если вы так уверены, что вам не нужно ничего делать с результатом удаленного вызова. Но если вам нужно связать несколько вызовов удаленных служб и/или использовать эти службы в качестве основных компонентов вашей системы, использование простого кода в стиле асинхронного обратного вызова наверняка истощит ваше здравомыслие быстрее, чем танцы с Ktulu.