На прошлой неделе я написал о своем первом знакомстве с JavaScript. Исходя из Ruby, JavaScript сначала был беспорядочным, со скобками и точками с запятой повсюду, с небольшим чувством аккуратности и обходными способами, что в Ruby было бы проще (например, зацикливание).

Хотя я в основном преодолел эти ключевые различия, одна концепция JavaScript, которая доставляла мне больше проблем, чем остальные, - это Promises. В JavaScript Promise - это объект, который «представляет возможное завершение (или сбой) асинхронной операции и ее результирующее значение», согласно Mozilla Developer Network (MDN).

Таким образом, как и обещания в реальном мире, обещание JavaScript настраивает что-то, что должно произойти в другое время, чем обещанное (асинхронное означает «не происходит одновременно»). И на этом сходства не заканчиваются. Обещания в JavaScript также могут быть «выполнены» или «отклонены», т. Е. Действие, указанное в обещании, либо выполняется, либо не выполняется.

Почему обещания?

Настройка асинхронной функции в JavaScript полезна, потому что вы можете ждать определенного события, такого как получение некоторых данных из ответа API. Здесь я должен отметить, что JavaScript Promises на самом деле является довольно недавней разработкой в ​​JavaScript (добавленной в 2015 году с введением ECMAscript 6). (Чтобы глубже познакомиться с историей Promises, рекомендую эту статью).

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

Здесь и проявляется асинхронность - все происходит в разное время. Пока вторая функция ожидает завершения первой функции, остальной код не ждет завершения этой первой функции. Он просто продолжает идти так далеко, как только может. Затем, когда первая функция готова, вызывается вторая функция.

Учтите следующее:

loadData('/myLifeGoals.js');
// the code below doesn't wait for the script loading to finish
//...

Предположим, loadData создает переменную, называемую достижениями. Вероятно, мы захотим вызвать его сразу после загрузки данных. Итак, мы могли бы сделать следующее:

loadData('/myLifeGoals.js'); // defines showAchievements()
showAchievements(); // undefined

Почему достижения до сих пор не определены, хотя кажется, что они определены в первой строке? Это потому, что остальная часть выполнения не ждет загрузки myLifeGoals.js. Таким образом, к моменту выполнения второй строки функции и переменные в myLifeGoals.js еще не были определены в текущем выполнении, поэтому showAchievements остается неопределенным.

Чтобы убедиться, что showAchievements запускается только тогда, когда myLifeGoals.js готов, нам нужно переопределить loadData - функцию, которая подготавливает myLifeGoals.js, - чтобы она могла принимать другую функцию в качестве аргумента. Затем, когда мы вызываем loadData для запуска, мы передаем нашу вторую функцию в качестве параметра, который будет выполняться в loadData.

function loadData(src, callback) {
  let li = document.createElement('li');
  li.src = src;
  li.onload = () => callback();
  document.head.append(li);
}
loadData('/my/script.js', function() {
  showAchievements();
});

Проблема с обратными вызовами

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

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

loadScript('/my/script.js', function(script) {
  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
      // ...continue after all scripts are loaded
    });

  })

});

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

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...continue after all scripts are loaded (*)
          }
        });

      }
    })
  }
});

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

'1.js'.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded(['1.js', '2.js']).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

Сглаживание пирамиды обещаниями

Объекты обещаний определяются со следующей базовой структурой:

var promise = new Promise(function(resolve, reject) {
  // functions we wait on go here

  if (/* some success condition is achieved */) {
    resolve("It worked!");
  }
  else {
    reject(Error("Error"));
  }
});

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

promise.then(function(result) {
  console.log(result); // do stuff with the result of the resolved promise
}, function(err) {
  console.log(err); // do stuff with the error from the rejected promise
});

Написав таким образом асинхронные функции, мы можем избавиться от множества операторов if-else, сгладив пирамиду гибели. А за счет использования ключевых слов «результат» и «ошибка» чтение двух возможных вариантов урегулирования обещания станет проще, чем раньше.

На практике

Обычное обещание может выглядеть так:

function getJson(url){
// make the function return the promise
  return new Promise(function(resolve,reject){
// make a new xhr open a get request to the specified url
    var req = new XMLHttpRequest();
    req.open("GET", url);
req.onload = function(){
      if (req.status == 200){
// carry this out if the load is successful
        resolve(req.response);
      } else {
// carry this out if the load is unsuccessful
        reject(Error(req.statusText));
      }
    };
// handle other errors
req.onerror = function(){
      reject(Error("Network Error"))
    };
// send the request
req.send();
// close the promise
  });
}

Затем мы вызываем это в нашем коде следующим образом:

getJson('hello.json').then(function(result){
  console.log(result);
}, function(error){
  console.log(error);
})
// everything down here will carry on executing while 'hello.json' loads

А что насчет улова?

Иногда цепочки могут заканчиваться методом ловли.

get('hello.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

Проще говоря, catch - это синтаксический сахар для:

then(undefined, function(){})

Что хорошо в catch, так это то, что если функция, связанная заранее, выйдет из строя, функция catch все равно сможет перехватить ошибку. С then(func1, func2) будет вызываться либо func1, либо func2, но не оба одновременно. В то время как с then(func1).catch(func2), оба будут вызваны, если func1 отклонит.

Ресурсы