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

Pavel Mayorov

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

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 ничего не отображается.

3 ответа

Pavel Mayorov

Проблема в том, что в коде нет операции ожидания. Ни подписка на событие, ни 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);
<!-- бывший someInput --> <!-- бывший 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);


Pavel Mayorov

ES2015

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

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

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

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

Это несколько сложнее, но очень мощный трюк позволяет нам писать асинхронный код в синхронном режиме. Есть несколько "запускальщиков", которые делают это. Для примера будет использован Promise.coroutine из ********, но есть и другие упаковщики, как со или 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());
})();

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


Pavel Mayorov

Код с асинхронными функциями можно исполнять синхронно используя альтернативный 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');
});

Если асинхронная функция вызывает 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 будет последовательно исполнять код функции, останавливаясь и дожидаясь результата вызовов всех аснихронных функций.

licensed under cc by-sa 3.0 with attribution.