In javascript, how can I best combine multiple values which are obtained as arguments to callback functions only, preferably without adding library dependencies?
E.g. consider
function eventHandler() {
getSomethingAsync(function(something){
getOtherAsync(function(other){
console.log([something.value, other.status]);
});
});
}
This looks like the starting point of CallbackHell(.com).
In some other languages I would use promises at this point, along the lines of
function eventHandler() {
var something = getSomethingPromise();
var other = getOtherPromise();
console.log([something.get().value, other.get().status]);
}
but it seems that even ES6-promises don't quite allow such deflation of the code, preserving verbose (error-prone?) repetition-patterns like
Promise.all([somePromise, otherPromise]).then(
function([some, other]){
console.log([some.value, other.status]);
});
An example I ran into was trying to combine the tab provided by chrome.tabs.getSelected
and the responseText
provided by an XMLHTTPRequest
in a Chrome extension (see "Example Code #2" below).
Example code #1 (mockup)
Copy/paste into developer console within stackoverflow.com origin.
Obtains three values from different (mockup) async functions, and constructs a new object from them. For demonstration purposes, the result is simply printed to the console.
// mockup async functions
function someAsyncAPI(){
var handle = { 'send': function(){ handle.callback({value: 'SOME_VALUE'}); }}
return handle;
}
function otherAsyncAPI(callback){
callback({ 'version': '1.0', 'value': 'OTHER_VALUE' });
}
function callbackHell(){
// Issue:
// - One level of nesting for each value.
// - Hard to add more values.
// - Doesn't make use of irrelevance of order of evaluation.
var req = new XMLHttpRequest();
req.open('GET', '/jobs');
req.onload = function(){
var handle = someAsyncAPI();
handle.callback = function(someValue){
otherAsyncAPI(function(otherValue){
console.log({
mode: 'direct-callback',
jobs: req.responseText,
some: someValue.value,
other: otherValue.value});
});
};
handle.send();
};
req.send();
}
function promiseVersion(){
// Issue:
// - Still seems repetitive, verbose.
// - Promise.all line repeats variable names (likely place of errors?).
var jobsPromise = new Promise(function(resolve,reject){
var req = new XMLHttpRequest();
req.open('GET', '/jobs');
req.onload = function() { resolve(req.responseText); };
req.send();
});
var somePromise = new Promise(function(resolve,reject){
var handle = someAsyncAPI();
handle.callback = resolve;
handle.send();
});
var otherPromise = new Promise(function(resolve,reject){
otherAsyncAPI(resolve);
});
Promise.all([jobsPromise, somePromise, otherPromise])
.then(function([jobsValue, someValue, otherValue]){
console.log({
mode: 'direct-callback',
jobs: jobsValue,
some: someValue.value,
other: otherValue.value});
});
}
callbackHell();
promiseVersion();
Example code #2 (Real-world example)
Chrome extension that hibernates the current tab by redirecting to a data URI. Essentially my take on the core idea of addons like "TabMemFree".
# --- popup.js -----------------------------------------------------
"use strict";
document.getElementById('hibernateTab1').onclick = hibernateTab1;
document.getElementById('hibernateTab2').onclick = hibernateTab2;
function hibernateTab1(tab){
// Issues:
// - Unintuitive order or statements
// - Beginning of nesting-hell
chrome.tabs.getSelected(null, function(selectedTab){
var req = new XMLHttpRequest();
req.open('GET', 'suspended.html');
req.onload = function(){
var pagesource = req.responseText
.replace(/__TITLE__/g, JSON.stringify(selectedTab.title))
.replace(/__BACKURL__/g, JSON.stringify(selectedTab.url));
var datauri = "data:text/html;base64," + btoa(pagesource);
chrome.tabs.update(selectedTab.id, {"url": datauri});
};
req.send();
});
}
function hibernateTab2(){
// Improvements:
// - Clean separation of independent steps.
// - Reduces nesting by one level per independent asynchronous
// value after the first.
// - Independent values might even be calculated in parallel?
// Issues:
// - Duplicate variable names in `Promise.all` line are prone to error.
// - Still seems needlessly long, with a lot of padding.
var template = new Promise(function(resolve,reject){
var req = new XMLHttpRequest();
req.open('GET', 'suspended.html');
req.onload = function(){ resolve(req.responseText); };
req.send();
});
var selectedTab = new Promise(function(resolve,reject){
chrome.tabs.getSelected(null, resolve);
});
Promise.all([template, selectedTab]).then(function([template, selectedTab]){
var pagesource = template
.replace(/__TITLE__/g, JSON.stringify(selectedTab.title))
.replace(/__BACKURL__/g, JSON.stringify(selectedTab.url));
var datauri = "data:text/html;base64," + btoa(pagesource);
chrome.tabs.update(selectedTab.id, {"url": datauri});
});
}
# --- popup.html -----------------------------------------------------
<html>
<head>
<title>Silence of the Tabs</title>
</head>
<body style='width:300px;'>
<p><h1>Hello World.</h1></p>
<p><a href='#' id='hibernateTab1'>hibernateTab1()</a></p>
<p><a href='#' id='hibernateTab2'>hibernateTab2()</a></p>
</body>
<script src='popup.js'></script>
</html>
# --- suspended.html -----------------------------------------------------
<html>
<body>
<a id='goback'>Go Back</a>
</body>
<script>
var g = document.getElementById('goback');
var url=__BACKURL__;
var title=__TITLE__;
document.title=title;
g.href = 'javascript:goback()';
g.innerText = title;
function goback(){
if(history.length > 2){
history.back();
} else {
location.href = url;
}
}
</script>
</html>
# --- manifest.json -----------------------------------------------------
{
"manifest_version": 2,
"name": "Unload Tab",
"description": "Unload Tab",
"version": "0.1",
"browser_action": {
"default_popup": "popup.html"
},
"permissions": [
"activeTab"
]
}
Apparently the async/wait keywords are what I was looking for. With these my mockup example can be written as
or alternatively (might cause sequential instead of parallel execution of the promises)