Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

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


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

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


Ответы

Ответ 1



Дождались! ES2017 8-ая редакция. Внесено описание для функций с модификатором async, и использование await Пример уже работает в хроме: (async function() { var data = await fetch('https://jsonplaceholder.typicode.com/users'); console.log(await data.json()); })(); 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. частичный перевод данного ответа

Ответ 2



Проблема в том, что в коде нет операции ожидания. Ни подписка на событие, ни AJAX-вызов ни даже вызов API не ждут поступления данных - а сразу же передают управление дальше Поэтому строка someDiv.textContent = result; выполняется ДО того, как переменная resul получит значение! Способов сделать это присваивание после получения значения - несколько. Способ 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 раз, а большинств событий происходят несколько раз. Поэтому использовать обещания для того же onchange - нельзя. Что же до 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);
Способ 3.1 - наблюдаемые значения (observables) в MobX Тут все почти так же, как и в knockout. В примере ниже я использую синтаксис ES201 и старше, потому что библиотека подразумевает использование новых средств языка: 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);

Ответ 3



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

Комментариев нет:

Отправить комментарий