#3 Callback Hell

#3 Callback Hell

- 12 mins

С каждой неделей замечаю, что становлюсь всё более невосприимчивым ко всяким отвлечениям вроде скроллинга новостей, ютуба, и каких-то других бесполезных занятий. Конечно, без этого никуда, но сократить подобное времяпрепровождение всё таки получается и на этой недели я наконец-то смог плотно заняться изучением английского языка. Начал с грамматических основ и конструкций языка - именно такой метод, как мне кажется, позволит наиболее основательно втянуться и понять “суть происходящего”. Противоположная методика, которая пропагандируется в Rosetta Stone и подобных системах, плохо подходит для изучения когда у вас в голове уже отложились основные структуры родного языка. Напомню что стиль Rosetta Stone - это использование комбинаций текста, звука и изображений предметов с той целью чтобы обучающийся изучал и запоминал слова и грамматику на интуитивном уровне, без каких-либо объяснений.

За эту неделю успел пополнить словарный запас на 60-70 слов (Anki), изучить поподробнее самые основы вроде Present/Past/Future simple, Possessive Case и применение различных предлогов. Какие-то простые предложения уже становятся чем-то понятным и привычным. Думаю сейчас самое главное - это не останавливаться.

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

WaterFall

Как и говорил ранее - иногда буду выкладывать разборы каких-то задач, функций или просто интересные фичи из мира Javascript’a. Сегодня хотелось бы поговорить о коллбеках, их вложенности (Callback Hell), а также некоего решения этой проблемы.

Для начала давайте посмотрим как выглядит этот самый Callback Hell: hadouken-code Коллбек, внутри которого вызывается функция, внутри которой вызывается следующая и так далее… С ростом вложенности читать такой код становится просто невозможно и нам очень хотелось бы иметь что-то более “плоское” и читаемое. Для этого нам понадобится некоторая абстракция в виде функции, назовем её waterFall.

Сначала разберемся с интерфейсом:

//Массив наших функций, которые будут выполняться последовательно.
const functions = [
    (cb) => cb(null, 'text'), //null, если нет ошибок
    (data1, cb) => cb(null, data1, "another text"),
    (data1, data2, cb) => cb(null, data1, data2),
];

//Вызов waterFall последовательно запускает функции...
//...при ошибке вызывается коллбек в который она передается
waterFall(functions, (err, result) => {
    console.log('Error: ' + err);
    console.log('Result: ' + result);
});

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

Посмотрим на всю реализацию:

const waterFall = (functions, callback) => {
    if(functions.length === 0) return callback();

    const next = ([head, ...rest], previousValue) => {
        const cb = (err, ...args) => {
            if(err || rest.length === 0) {
                callback(err, args); 
            }
            else {
                next(rest, args);
            }
        };
        head(...previousValue, cb);
    };
    next(functions, []); // первый вызов next
}

Ну а теперь по порядку. Если список функций пуст - вызываем общий коллбек функции waterFall

...
    if(functions.length === 0) return callback();
...

Вызываем функцию next которая будет рекурсивно вызывать все переданные ей функции (functions), а также передаем в нее результат предыдущей функции - при первом запуске этот результат отсутствует, поэтому передадим пустой массив.

...
    next(functions, []); 
// [] - далее будет развертываться с помощь оператора spread
...

Функция next внутри себя выполняет несколько действий - cначала она с помощью destruction assignment получает отдельно первую функцию (из functions) в переменную head, а остальные в rest, для дальнейшего рекурсивного вызова. Вызываем первую функцию, передавая в нее результат предыдущего вызова (…previousValue), а также наш коллбек (cb)

...
const next = ([head, ...rest], previousValue) => {
    const cb = (err, ...args) => {
        if(err || rest.length === 0) {
            callback(err, args); 
        }
        else {
            next(rest, args);
        }
    };
    head(...previousValue, cb); // вызов одной функции из списка с передачей в нее предыдущего значения и коллбека (cb)
};
...

Именно этот коллбек (cb) передается в каждую нашу функцию и вызывается в самом конце её выполнения. Он является основой waterFall и передает результат предыдущего вызова функции в следующую.

...
const cb = (err, ...args) => { // 
    if(err || rest.length === 0) { // проверка на ошибку и конец списка функций
        callback(err, args);
    }
    else {
        next(rest, args); // рекурсивный вызов next
    }
};
...

Таким образом мы поочередно вызываем все функции, каждый раз передавая в них результат с предыдущего вызова и последним элементом - коллбек. Функция next накапливает (обновляет последний) эти результаты, а cb - проверяет на ошибки и рекурсивно вызывает next для последующей обработки оставшихся функций. На последней итерации (или при ошибке) вызывается внешний callback, в который мы передаем результат и останавливаем выполнение

Если запустим код ниже, то на первой же функции он выпадет в ошибку, она будет передана в основной коллбек функции waterFall (callback), вместе с последним результатом выполнения, а именно строкой - “text”.

const functions = [
(cb) => cb('ВНЕЗАПНАЯ ошибка', 'text'),
(data1, cb) => cb(null, data1, "another text"),
(data1, data2, cb) => cb(null, data1, data2),
];

const waterFall = (functions, callback) => {
    if(functions.length === 0) return callback();

    const next = ([head, ...rest], previousValue) => {
        const cb = (err, ...args) => {
            if(err || rest.length === 0) {
                callback(err, args); 
            }
            else {
                next(rest, args);
            }
        };
        head(...previousValue, cb);
    };
    next(functions, []); // первый вызов next
}

waterFall(functions, (err, result) => {
    console.log('Error: ' + err);
    console.log('Result: ' + result);
}); 

// Error: ВНЕЗАПНАЯ ошибка
// Result: text


// А если в функциях не будет ошибок...
const validFunctions = [
(cb) => cb(null, 'text'),
(data1, cb) => cb(null, data1, "another text"),
(data1, data2, cb) => cb(null, data1, data2),
];

waterFall(validFunctions, (err, result) => {
    console.log('Error: ' + err);
    console.log('Result: ' + result);
}); 

// Error: null
// Result: text,another text

Вот такая вот хитренькая реализация =)

comments powered by Disqus
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora