Пытаюсь делать вот так, но ничего не получается:
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
ничего не отображается.
Проблема в том, что в коде нет операции ожидания. Ни подписка на событие, ни AJAX-вызов, ни даже вызов API не ждут поступления данных - а сразу же передают управление дальше. Поэтому строка someDiv.textContent = result;
выполняется ДО того, как переменная result
получит значение!
Способов сделать это присваивание после получения значения - несколько.
Возможно, этот способ выглядит как-то глупо - но он решает задачу и наиболее прост в понимании. Если ваше приложение достаточно простое - то так и надо делать. Смотрите:
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 - отсутствует разбиение на слои. Данные обрабатываются там же, где и получаются. Если вы чувствуете, что ваши скрипты становятся при использовании такого способа все менее понятными, или вам приходится писать одно и то же в нескольких местах - надо переходить к другим способам.
Простейшая модификация прошлого способа, позволяющая избавиться от дублирования кода.
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
. В итоге два модуля вызывают друг друга. Это и есть макаронный код.
Для борьбы с таким кодом и предназначены способы ниже.
Добавим той функции, которая делает запрос, параметр 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;
}
Обещание в 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
).
Ну и потом это продолжение (которое тоже является обещанием) мы и возвращаем.
Обычно про 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 -->
Тут все почти так же, как и в 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);
В данном стандарте введено понятие функции-генератора - функции которая может передать управление из середины и затем вернуться в то же место. Обычно их используют для получения последовательностей
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();
В стандартах есть намеки на введение новых ключевых слов async
, await
позволивших бы сделать работу с обещаниями более простой.
async function foo(){
var data = await fetch("/echo/json"); // обратите внимание на await
// код тут выполнится только после выполнения запроса
return data.json(); // data определена
}
Но пока это просто зарезервированные слова и неизвестно попадут ли они в следующий стандарт и когда будут реализации.
На данный момент для их использования можно воспользоваться сборщиками, например Babel.
Внесено описание для функций с модификатором async
, и использование await
Пример уже работает в хроме:
(async function() {
var data = await fetch('https://jsonplaceholder.typicode.com/users');
console.log(await data.json());
})();
частичный перевод данного ответа
Код с асинхронными функциями можно исполнять синхронно используя альтернативный JS движок nsynjs
то просто вызываем функцию, а значение промиса получаем через свойство 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>
Шаг 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 будет последовательно исполнять код функции, останавливаясь и дожидаясь результата вызовов всех аснихронных функций.
Кофе для программистов: как напиток влияет на продуктивность кодеров?
Рекламные вывески: как привлечь внимание и увеличить продажи
Стратегії та тренди в SMM - Технології, що формують майбутнє сьогодні
Выделенный сервер, что это, для чего нужен и какие характеристики важны?
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Есть код, не важно какой, суть в чем: при делении получается число равное площади используемой для изделия из плит, которые продаются либо...
Посоветуйте что-нибудь для работы со звуком, микрофономНужно измерять громкость звука
Нужна программа на javaКак исправить кодировку сохраняемого на телефон файла?
Подскажите как можно совместить UI приложения со всеми устройствами?