How to use multiple XMLHttpRequest.responseText va

2019-08-03 05:07发布

问题:

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"
  ]
}

回答1:

Apparently the async/wait keywords are what I was looking for. With these my mockup example can be written as

async function async_await_2(){
    var jobs = new Promise(function(resolve,reject){
        var req = new XMLHttpRequest();
        req.open('GET', '/jobs');
        req.onload = function() { resolve(req.responseText); };
        req.send();
    });
    var some = new Promise(function(resolve,reject){
        var handle = someAsyncAPI();
        handle.callback = resolve;
        handle.send();
    });
    var other = new Promise(function(resolve,reject){ 
        otherAsyncAPI(resolve); 
    });
    console.log({
        mode: 'async-await',
        jobs: await jobs,
        some: (await some).value,
        other: (await other).value});
}

or alternatively (might cause sequential instead of parallel execution of the promises)

async function async_await(){
    // Concise, but sacrifices parallelism of the Promises?
    var jobs = await new Promise(function(resolve,reject){
        var req = new XMLHttpRequest();
        req.open('GET', '/jobs');
        req.onload = function() { resolve(req.responseText); };
        req.send();
    });
    var some = await new Promise(function(resolve,reject){
        var handle = someAsyncAPI();
        handle.callback = resolve;
        handle.send();
    });
    var other = await new Promise(function(resolve,reject){ 
        otherAsyncAPI(resolve); 
    });
    console.log({
        mode: 'async-await',
        jobs: jobs,
        some: some.value,
        other: other.value});
}