Никто не хочет, чтобы их отношения или JavaScript выглядели как ад, хотя и не консультант по отношениям, но может говорить о JavaScript в течение часа или около того. Легенда гласит, раздавить никогда не отзовитесь; это не более чем, и я не легенда, поэтому давайте сосредоточимся на чем-то, на что можно ответить, обратные вызовы JavaScript их рай и ад.

Вы можете понять обратные вызовы JavaScript из повседневного разговора «Эй, я перезвоню, как только закончу свою работу». Обратные вызовы JavScript ничем не отличаются, код, который вы хотите запустить после завершения основной работы. В сценарии обратного вызова участвуют 2 тела (функции). Мы можем назвать их вызываемыми и функциями обратного вызова. Вызываемая функция знает, когда вызывать обратный вызов, а функция обратного вызова знает, что выполнять.

Как сделать обратный звонок?

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

// super complex math problem
if (number % 2 === 0) {
    // Yay! it's even
} else {
    // Meh! it's odd
}

Включение вышеуказанного вычисления в удобную функцию с именем «isEven»:

const isEven = (number) => {
    // super complex math problem
    if (number % 2 === 0) {
        // Yay! it's even
    } else {
        // Meh! it's odd
    }
};

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

const isEven = (number, callback) => {
    // super complex math problem
    if (number % 2 === 0) {
        callback(true);     // Yay! it's even
    } else {
        callback(false);    // Meh! it's odd
    }
};

Теперь давайте создадим функцию обратного вызова isEven, которую мы можем передать в качестве параметра при вызове isEven.

const isEven = (number, callback) => {
    // super complex math problem
    if (number % 2 === 0) {
        callback(true);
    } else {
        callback(false);
    }
};
const isEvenCallback = function (result) {
    if (result) {
        console.log("Yay! it's even");
    } else {
        console.log("Meh! it's odd");
    }
};
isEven(10, isEvenCallback);

Жизнь хороша, но она может быть лучше с некоторым сокращением кода. Мы создали функцию isEvenCallback, которая будет вызываться после проверки четности методом isEven. Но этот isEvenCallback предназначен исключительно для передачи в качестве обратного вызова isEven, иначе он не имеет смысла. Таким образом, мы можем просто сделать анонимную функцию обратного вызова только при объявлении параметра.

isEven(10, function (result) {
    if (result) {
        console.log("Yay! it's even");
    } else {
        console.log("Meh! it's odd");
    }
});

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

isEven(10, result => {
    if (result) {
        console.log("Yay! it's even");
    } else {
        console.log("Meh! it's odd");
    }
});

С силой ternary сделает это одним вкладышем:

isEven(10, result => result ? console.log("Yay! it's even") : console.log("Meh! it's odd"));

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

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

Самый общий формат обратного вызова

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

const factorial = (number, callback) => {
    if (typeof number === 'number') {
        let fact = 1;
        for (let i = 1; i <= number; i++) {
            fact *= i;
        }
        callback(null, fact);
    } else {
        callback('The number provided must be of type number', null);
    }
};

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

Вызов факториала и реализация обратного вызова:

factorial(5, (error, result) => {
    if (error) {
        console.log('Error Occurred:', error);
        return;
    }
    console.log('Factorial:', result);
});
// Factorial:120

Итак, мы увидели общий формат для большинства функций обратного вызова, которые вы найдете в основном в средах NodeJS.

Обратные вызовы повсюду

Они буквально повсюду: в интервалах, тайм-ауте, функциях массива, прослушивателях событий, fetch API, обратных вызовах Crush или Interviewer и т. д. Мы проверим их по одному, кроме последних 2. В таких случаях нам не нужно создавать вызываемой функции, вместо этого мы фокусируемся только на коде обратного вызова, который будет выполняться вызываемой функцией.

Интервальный обратный вызов

let counter = 0;
const interval = setInterval(() => {
    (++counter) <= 5 ?
        console.log(`Callback on interval ${counter}`) :
        clearInterval(interval);
}, 1000);

setInterval принимает 2 параметра, функцию обратного вызова и время в миллисекундах, после чего интервал будет вызываться повторно. Мы можем написать нашу логику внутри функции обратного вызова, которая будет выполняться снова и снова через заданные миллисекунды.

Обратный вызов по тайм-ауту

let counter = 0;
setTimeout(() => {
    console.log('Callback when timeout');
}, 6000);

Подобно setInterval, setTimeout принимает 2 параметра, функцию обратного вызова и время в миллисекундах, по истечении которого будет вызван тайм-аут. Мы можем написать нашу логику внутри функции обратного вызова, которая будет выполняться через заданные миллисекунды.

Обратный вызов прослушивателя событий

const button = document.querySelector('button');
document.querySelector('button').addEventListener('click', function () {
    console.log('Button is clicked');
});

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

Обратный вызов Fetch API

fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => response.json())
  .then(json => console.log(json));

Многие API JavaScript принимают обратные вызовы и выполняют предоставленный фрагмент кода. Методы выше 2 then выполняют предоставленную функцию обратного вызова, когда API выборки приносит данные.

Обратный вызов методов массива

const db = {
    users: [
        { id: 1, name: 'Allen' },
        { id: 2, name: 'John' },
        { id: 3, name: 'Martin' },
    ],
    posts: [
        { id: 101, userId: 1, title: 'Nisi sint cillum officia laborum consequat labore.' },
        { id: 102, userId: 1, title: 'Ipsum ea fugiat velit do.' },
        { id: 103, userId: 2, title: 'Qui ullamco veniam non sit mollit.' },
        { id: 104, userId: 3, title: 'Laboris qui officia anim proident Lorem esse aliquip.' },
    ],
    comments: [
        { id: 11, postId: 101, userId: 2, comment: 'Nulla tempor nisi dolor velit id qui culpa et tempor eiusmod sint.' },
        { id: 12, postId: 101, userId: 1, comment: 'Reprehenderit est cupidatat magna eu anim.' },
        { id: 13, postId: 102, userId: 3, comment: 'Sint adipisicing sint ad cillum ipsum aute voluptate ea fugiat nostrud ut.' },
        { id: 14, postId: 103, userId: 1, comment: 'Mollit id ipsum sunt laborum duis.' },
        { id: 15, postId: 101, userId: 3, comment: 'Exercitation ipsum pariatur sit Lorem deserunt occaecat.' },
    ]
};

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

// array functions
const found = db.users.find(i => i.id === 1);
const filtered = db.posts.filter(i => i.userId === 1);
const mapped = db.posts.map(i => ({ id: i.id, title: i.title }));
console.log('found:', found);
console.log('filtered:', filtered);
console.log('mapped:', mapped);

Здесь мы используем различные методы массива для получения желаемых данных. Чтобы лучше понять методы массива, вы можете ознакомиться с методами массива JavaScript для жизни в CodeOmelet. Все эти методы массива принимают метод обратного вызова, который считается предикатом и выполняется на основе предоставленной нами логики.

Асинхронная работа с обратными вызовами

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

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

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

const fetchUserById = (id, callback) => {
    setTimeout(() => {
        const results = db.users.find(i => i.id === id);
        results ?
            callback(null, results) :
            callback('Not found', null);
    }, 2000);
};

Здесь fetchUserById предоставит пользователя на основе идентификатора. Функция завершится через 2 секунды, а затем вызовет функцию обратного вызова, предоставив либо ошибку, либо результат.

fetchUserById(1, (err, user) => {
    if (err) {
        console.log('Error:', err);
        return;
    }
    console.log('user:', user);
});

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

Что такое ад обратного вызова?

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

const fetchUserById = (id, callback) => {
    setTimeout(() => {
        const results = db.users.find(i => i.id === id);
        results ?
            callback(null, results) :
            callback('Not found', null);
    }, 2000);
};
const fetchPostsByUserId = (userId, callback) => {
    setTimeout(() => {
        const results = db.posts.filter(i => i.userId === userId);
        results.length ?
            callback(null, results) :
            callback('Not found', null);
    }, 2000);
};
const fetchCommentsByPostId = (postId, callback) => {
    setTimeout(() => {
        const results = db.comments.filter(i => i.postId === postId);
        results.length ?
            callback(null, results) :
            callback('Not found', null);
    }, 2000);
};

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

fetchUserById(1, (err, user) => {
    if (err) {
        console.log('Error:', err);
        return;
    }
fetchPostsByUserId(user.id, (err, posts) => {
        if (err) {
            console.log('Error:', err);
            return;
        }
        user.posts = posts.map(i => ({ id: i.id, title: i.title, comments: [] }));
user.posts.forEach(post => {
            fetchCommentsByPostId(post.id, (err, comments) => {
                if (err) {
                    console.log('Error:', err);
                    return;
                }
                post.comments = comments.map(i => ({ id: i.id, title: i.title }));
                console.log('user:', user);
            })
        });
    });
});

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

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

const userLoaded = (user) => {
    // came from callback hell
    console.log('user:', user);
};
fetchUserById(1, (err, user) => {
    if (err) {
        console.log('Error:', err);
        return;
    }
fetchPostsByUserId(user.id, (err, posts) => {
        if (err) {
            console.log('Error:', err);
            return;
        }
        user.posts = posts.map(i => ({ id: i.id, title: i.title, comments: [] }));
if (!user.posts.length) {
            userLoaded(user);
            return;
        }
let postCount = 1;
        user.posts.forEach(post => {
            fetchCommentsByPostId(post.id, (err, comments) => {
                if (err) {
                    console.log('Error:', err);
                    return;
                }
                post.comments = comments.map(i => ({ id: i.id, title: i.title }));
if (postCount === user.posts.length) {
                    userLoaded(user);
                } else {
                    postCount++;
                }
            })
        });
    });
});

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

// user have not posts
if (!user.posts.length) {
    userLoaded(user);
    return;
}

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

let postCount = 1;
user.posts.forEach(post => {
    fetchCommentsByPostId(post.id, (err, comments) => {
        if (err) {
            console.log('Error:', err);
            return;
        }
        post.comments = comments.map(i => ({ id: i.id, title: i.title }));
if (postCount === user.posts.length) {
            userLoaded(user);
        } else {
            postCount++;
        }
    })
});

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

user loaded function.
const userLoaded = (user) => {
    // came from callback hell
    console.log('user:', user);
};

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

До ES6 (ES2015) с обратными вызовами было не так хорошо, после чего мы получили Promise от JavaScript. Не говоря уже об обещаниях, которые делают пары, но об объекте обещания JavaScript, который поможет вам избавиться от пирамиды гибели обратного вызова. Мы проверим промисы в ближайших статьях и сравним их с обратными вызовами.

Git-репозиторий

Проверьте репозиторий git для этого проекта или загрузите код.

Скачать код

Git-репозиторий

Краткое содержание

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

Надеюсь, эта статья поможет.

Первоначально опубликовано на https://codeomelet.com.