Как вернуть значение из события или из функции обратного вызова?

464
24 ноября 2016, 09:49

Пытаюсь делать вот так, но ничего не получается:

var result = "";
someInput.onchange = function() {
  result = someInput.value;
};
$.get("someapi", function (data) {
  result = data.foo;
});
some.api.call(42, function (data) {
  result = data.bar;
});
someDiv.textContent = result;

Почему-то в someDiv ничего не отображается.

Answer 1

Проблема в том, что в коде нет операции ожидания. Ни подписка на событие, ни AJAX-вызов, ни даже вызов API не ждут поступления данных - а сразу же передают управление дальше. Поэтому строка someDiv.textContent = result; выполняется ДО того, как переменная result получит значение!

Способов сделать это присваивание после получения значения - несколько.

Способ 0 - переместить присваивание внутрь

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

someInput.onchange = function() {
  someDiv.textContent = someInput.value;
};
$.get("someapi", function (data) {
  someDiv.textContent = data.foo;
});
some.api.call(42, function (data) {
  someDiv.textContent = data.bar;
});
someDiv.textContent = "";

В данном случае я вообще избавился от переменной result.

Недостаток у данного способа ровно 1 - отсутствует разбиение на слои. Данные обрабатываются там же, где и получаются. Если вы чувствуете, что ваши скрипты становятся при использовании такого способа все менее понятными, или вам приходится писать одно и то же в нескольких местах - надо переходить к другим способам.

Способ 0+ - вынесение присваивания в именованную функцию.

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

someInput.onchange = function() {
  setResult(someInput.value);
};
$.get("someapi", function (data) {
  setResult(data.foo);
});
some.api.call(42, function (data) {
  setResult(data.bar);
});
setResult("");
function setResult(result) {
  someDiv.textContent = result;
}

Напомню, что в js объявления функций "поднимаются на верх", т.е. объявленной в самом низу функцией setResult можно пользоваться где угодно. Это позволяет начинать скрипт не с объявления 100500 функций - а с того кода, который непосредственно начнет выполняться.

Такой способ неплохо подходит для небольших скриптов, которые не разбиты на модули.

Проблема макаронного кода

Иногда, асинхронный запрос делается в одном модуле или его части, а получить его результат надо в другой. Прямое использование способа 0+ приводит к коду, который называют "макаронным":

// модуль 1
function getResult() {
  $.get("someapi", function (data) {
    setResult(data.foo);
  });
}
// модуль 2
function someFunc() {
  getResult();
}
function setResult(result) {
  someDiv.textContent = result;
}

Обращаю внимание: someFunc вызывает getResult, которая вызывает setResult. В итоге два модуля вызывают друг друга. Это и есть макаронный код.

Для борьбы с таким кодом и предназначены способы ниже.

Способ 1 - обратные вызовы ("колбеки", callbacks)

Добавим той функции, которая делает запрос, параметр callback, куда будем передавать функцию, получающую ответ:

function getResult(callback) {
  $.get("someapi", function (data) {
    callback(data.foo);
  });
}

Теперь такую функцию можно вызвать вот так:

getResult(function(result) {
  someDiv.textContent = result;
})

Или вот так:

getResult(setResult);
function setResult(result) {
  someDiv.textContent = result;
}

Способ 2 - обещания ("промизы", promises)

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

Имеется несколько реализаций обещаний. Основной сейчас являются ES6 Promises, они поддерживаются современными браузерами кроме IE. (Но для тех браузеров, которые их не поддерживают, есть куча полифилов).

Используются они вот так:

function getResult(N) {
  return new Promise(function (resolve, reject) {
    some.api.call(N, function (data) {
      resolve(data.bar);
    });
  });
}

Также в качестве обещания можно использовать JQuery Deferred:

function getResult(N) {
  var d = $.Deferred();
  some.api.call(N, function (data) {
    d.resolve(data.bar);
  });
  return d.promise();
}

Или Angular $q:

function getResult(N) {
  var d = $q.defer();
  some.api.call(N, function (data) {
    d.resolve(data.bar);
  });
  return d.promise;
}

Кстати, Angular $q можно использовать и подобно es6 promise:

function getResult(N) {
  return $q(function (resolve, reject) {
    some.api.call(N, function (data) {
      resolve(data.bar);
    });
  });
}

В любом случае, использование такой функции getResult будет выглядеть одинаково:

getResult(42).then(function (result) {
  someDiv.textContent = result;
});

Обращаю внимание, что здесь я для примера взял именно some.api.call, но не событие или ajax-вызов - и это не случайно!

Дело в том, что обещание может быть выполнено (resolved) только 1 раз, а большинство событий происходят несколько раз. Поэтому использовать обещания для того же onchanged - нельзя.

Что же до ajax-вызова - то надо помнить, что он УЖЕ возвращает обещание! А потому все способы выше в комбинации с ним будут выглядеть смешными. Все делается гораздо проще:

function getResult() {
  return $.get("someapi")
    .then(function (data) {
      return data.foo;
    });
}

На случай если вы запутались в коде выше, вот его "развернутая" версия:

function getResult() {
  var q1 = $.get("someapi");
  var q2 = q1.then(function (data) {
    return data.foo;
  });
  return q2;
}

Тут все просто. Сам по себе вызов $.get возвращает обещание, которое при выполнении будет содержать прищедшие с сервера данные.

Далее мы создаем для него продолжение, которое обработает эти данные (достанет поле foo).

Ну и потом это продолжение (которое тоже является обещанием) мы и возвращаем.

Способ 3 - наблюдаемые значения (observables) в Knockout

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

Можно сделать так. Для начала, заведем наблюдаемое значение:

var result = ko.observable("");

Это значение можно менять по событию:

someInput.onchange = function() {
  // вызов result с параметром устанавливает значение равным параметру
  result(someInput.value);
};

И теперь можно выполнять некоторый блок кода каждый раз когда это значение меняется:

ko.computed(function() {
  // вызов result без параметров возвращает текущее значение
  someDiv.textContent = result();
});

Функция, переданная в ko.computed, будет вызвана каждый раз, когда ее зависимости изменятся.

PS код выше приведен как пример ручной работы с наблюдаемыми значениями. Но имейте в виду, что в Knockout есть более простые способы для работы с содержимым элементов DOM:

var vm = {
  result: ko.observable()
};
ko.applyBindings(vm);
<input data-bind="value: result"></input> <!-- бывший someInput -->
<div data-bind="text: result"></div> <!-- бывший someDiv -->
Answer 2

ES2015

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

function* foo(){
    yield 1;
    yield 2;
    while(true) yield 3;
}

Данная функция возвращает итератор для последовательности 1,2,3,3,3,..., который может быть проитерирован. Хотя это интересно и само по себе, но есть один специфический случай.

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

Это несколько сложнее, но очень мощный трюк позволяет нам писать асинхронный код в синхронном режиме. Есть несколько "запускальщиков", которые делают это. Для примера будет использован Promise.coroutine из Bluebird, но есть и другие упаковщики, как со или Q.async.

var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // обратите внимание на yield
    // код здесь будет выполнен после получения ответа на запрос
    return data.json(); // data здесь определена
});

Этот метод тоже возвращает обещание, которое может быть использовано в других сопрограммах. Например:

var main = coroutine(function*(){
   var bar = yield foo(); // ожидаем окончания нашей сопрограммы она вернет обещание
   // код ниже выполнится когда будет получен ответ от сервера
   var baz = yield fetch("/api/users/"+bar.userid); // зависит от результата возвращенного функцией foo
   console.log(baz); // выполнится когда завершатся оба запроса
});
main();

ES2016 (ES7) Недалекое будущее

В стандартах есть намеки на введение новых ключевых слов async, await позволивших бы сделать работу с обещаниями более простой.

async function foo(){
    var data = await fetch("/echo/json"); // обратите внимание на await
    // код тут выполнится только после выполнения запроса
    return data.json(); // data определена
}

Но пока это просто зарезервированные слова и неизвестно попадут ли они в следующий стандарт и когда будут реализации.

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

частичный перевод данного ответа

READ ALSO
Как передать переменную из JavaScript в php?

Как передать переменную из JavaScript в php?

У меня есть скрипт который срабатывает каждый 10 секунд. Каждый раз когда она срабатывает значение переменных меняется.

342
Как задать масштаб странице

Как задать масштаб странице

Есть страница, как сделать так, если разрешение экрана меньше или равно 1024, то размер страницы становился такой, как будто мы нажали ctrl-(с масштабом...

488
Включить сразу 3 графика

Включить сразу 3 графика

Есть 3 диаграммы, заключены в полимер, сделал переключалку между ними, по отдельности все работают идеально, но мне необходимо и включать...

450
при нажатии на любую первую ячейку выполняется одна ф-ция, в остальных случаях

при нажатии на любую первую ячейку выполняется одна ф-ция, в остальных случаях

Имеется таблица, при нажатии на любую первую ячейку выполняется метод initExpandableTableRows(), а если нажать на любую другую ячейку, то выполняется...

375