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

236
26 июня 2018, 09:20

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

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;
});

Или же можно использовать новый синтаксис async/await, описанный в ответе ниже от Grundy

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

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

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

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

Кстати, здесь тоже можно было использовать async/await

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

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 -->

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

Тут все почти так же, как и в knockout. В примере ниже я использую синтаксис ES2016 и старше, потому что библиотека подразумевает использование новых средств языка:

import { observable, autorun } from 'mobx';
var result = observable("");
someInput.onchange = () => {
  result.set(someInput.value);
};
autorun(() => someDiv.textContent = result.get());

Однако, обычно в MobX используются классы, а не одиночные obervable:

class ViewModel {
  @observable result = "";
}
var vm = new ViewModel();
someInput.onchange = () => {
  vm.result = someInput.value;
};
autorun(() => someDiv.textContent = vm.result);
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.

Дождались! ES2017 8-ая редакция.

Внесено описание для функций с модификатором async, и использование await

Пример уже работает в хроме:

(async function() { 
  var data = await fetch('https://jsonplaceholder.typicode.com/users'); 
  console.log(await data.json()); 
})();

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

Answer 3

Код с асинхронными функциями можно исполнять синхронно используя альтернативный JS движок nsynjs

Если асинхронная функция возвращает promise

то просто вызываем функцию, а значение промиса получаем через свойство data:

function synchronousCode() { 
 
    var getURL = function(url) { 
        return window.fetch(url).data.text().data; 
    }; 
     
    var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js'; 
    console.log('received bytes:',getURL(url).length); 
     
}; 
 
nsynjs.run(synchronousCode,{},function(){ 
    console.log('synchronousCode done'); 
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

Если асинхронная функция вызывает callback

Шаг 1. Оборачиваем асинхронную функцию в nsynjs-обертку (либо в промис):

var ajaxGet = function (ctx,url) {
    var res = {};
    var ex;
    $.ajax(url)
    .done(function (data) {
        res.data = data;
    })
    .fail(function(e) {
        ex = e;
    })
    .always(function() {
        ctx.resume(ex);
    });
    return res;
};
ajaxGet.nsynjsHasCallback = true;

Шаг 2. Помещаем логику в функцию, как если бы логика исполнялась синхронно

function process() {
    console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}

Шаг 3. Исполняем функцию через nsynjs

nsynjs.run(process,this,function () {
    console.log("synchronous function finished");
});

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

READ ALSO
Как округлять в большую сторону до половины или до целого числа JS

Как округлять в большую сторону до половины или до целого числа JS

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

202
Посоветуйте что-нибудь для работы со звуком, измерения громкости звука

Посоветуйте что-нибудь для работы со звуком, измерения громкости звука

Посоветуйте что-нибудь для работы со звуком, микрофономНужно измерять громкость звука

216
Создание файла с названием на кириллице

Создание файла с названием на кириллице

Нужна программа на javaКак исправить кодировку сохраняемого на телефон файла?

232
Как совместить написаное для Android приложение со всеми резолюциями экрана

Как совместить написаное для Android приложение со всеми резолюциями экрана

Подскажите как можно совместить UI приложения со всеми устройствами?

259