The problem is that the code does not have a wait operation. Neither a subscription to an event, nor an AJAX call, nor even an API call is waiting for the data to be received, but they immediately transfer control further. Therefore, the string someDiv.textContent = result;
is executed BEFORE the result
variable gets the value!
There are several ways to make this assignment after getting the value.
Method 0 - Move Assignment Inward
Perhaps this method looks like something stupid - but it solves the problem and is the easiest to understand. If your application is simple enough, this is how it should be done. See:
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 = "";
In this case, I generally got rid of the variable result
.
The disadvantage of this method is exactly 1 - there is no splitting into layers. Data is processed in the same place where it is obtained. If you feel that your scripts are becoming less and less understandable when using this method, or you have to write the same thing in several places, you need to switch to other methods.
Method 0+ - making assignment to a named function.
The simplest modification of the past method, which allows to get rid of code duplication.
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; }
I recall that in js function declarations "rise to the top", i.e. The setResult
function declared at the bottom can be used anywhere. This allows you to start the script not with the declaration of 100500 functions - but with the code that will immediately begin to be executed.
This method is well suited for small scripts that are not divided into modules.
Macaroni problem
Sometimes, an asynchronous request is made in one module or its part, and its result must be obtained in another. Direct use of method 0+ leads to a code that is called "macaroni":
// модуль 1 function getResult() { $.get("someapi", function (data) { setResult(data.foo); }); } // модуль 2 function someFunc() { getResult(); } function setResult(result) { someDiv.textContent = result; }
Pay attention: someFunc
calls getResult
, which calls setResult
. As a result, the two modules call each other. This is the macaroni code.
The methods below are intended to combat this code.
Method 1 - Callbacks (callbacks)
Add the function that makes the request, the callback
parameter, to which we will pass the function that receives the answer:
function getResult(callback) { $.get("someapi", function (data) { callback(data.foo); }); }
Now this function can be called like this:
getResult(function(result) { someDiv.textContent = result; })
Or like this:
getResult(setResult); function setResult(result) { someDiv.textContent = result; }
Method 2 - promises (promises)
A promise in js is a programming pattern that indicates a value that is not currently there, but it is expected to be in the future.
There are several promises implemented. The main are now ES6 Promises , they are supported by modern browsers except IE. (But for those browsers that do not support them, there is a bunch of polyfiles ).
Promises are created like this:
function getResult(N) { return new Promise(function (resolve, reject) { some.api.call(N, function (data) { resolve(data.bar); }); }); }
You can also use jQuery Deferred as a promise:
function getResult(N) { var d = $.Deferred(); some.api.call(N, function (data) { d.resolve(data.bar); }); return d.promise(); }
Or Angular $ q :
function getResult(N) { var d = $q.defer(); some.api.call(N, function (data) { d.resolve(data.bar); }); return d.promise; }
By the way, Angular $ q can be used like es6 promise:
function getResult(N) { return $q(function (resolve, reject) { some.api.call(N, function (data) { resolve(data.bar); }); }); }
In any case, using this getResult function will look the same:
getResult(42).then(function (result) { someDiv.textContent = result; });
Alternatively, you can use the new syntax async / await, described in the answer below from Grundy
I draw attention to the fact that here I took some.api.call
as an some.api.call
, but not an event or an ajax call — and this is not by accident!
The fact is that a promise can be fulfilled ( resolved
) only once, and most of the events occur several times. Therefore, to use promises for the same onchanged
- it is impossible.
As for the ajax call, we must remember that it ALREADY returns the promise! And because all the ways above in combination with it will look ridiculous. Everything is made much easier:
function getResult() { return $.get("someapi") .then(function (data) { return data.foo; }); }
By the way, here too it was possible to use async / await
In case you get confused in the code above, here is its "unwrapped" version:
function getResult() { var q1 = $.get("someapi"); var q2 = q1.then(function (data) { return data.foo; }); return q2; }
It's simple. By itself, the $.get
returns a promise that, when executed, will contain data pinched from the server.
Next, we create a continuation for it, which will process this data (get the foo
field).
Well, then this continuation (which is also a promise) is what we return.
Method 3 — Observed values (observables) in Knockout
Usually, Knockout is remembered as a library for two-way data binding to a view - but its capabilities can also be useful in solving such problems.
You can do so. First, let's get the observed value:
var result = ko.observable("");
This value can be changed by event:
someInput.onchange = function() { // вызов result с параметром устанавливает значение равным параметру result(someInput.value); };
And now you can execute some block of code every time this value changes:
ko.computed(function() { // вызов result без параметров возвращает текущее значение someDiv.textContent = result(); });
The function passed to ko.computed
will be called each time its dependencies change.
The PS code above is shown as an example of manual work with observable values. But keep in mind that Knockout has easier ways to work with the contents of DOM elements:
var vm = { result: ko.observable() }; ko.applyBindings(vm);
<input data-bind="value: result"></input> <!-- бывший someInput --> <div data-bind="text: result"></div> <!-- бывший someDiv -->
Method 3.1 — Observables in MobX
Everything is almost the same as in knockout. In the example below, I use the syntax of ES2016 and later, because the library involves the use of new language features:
import { observable, autorun } from 'mobx'; var result = observable(""); someInput.onchange = () => { result.set(someInput.value); }; autorun(() => someDiv.textContent = result.get());
However, MobX usually uses classes, not single obervables:
class ViewModel { @observable result = ""; } var vm = new ViewModel(); someInput.onchange = () => { vm.result = someInput.value; }; autorun(() => someDiv.textContent = vm.result);