How to use jQuery Deferred functionality instead of async.waterfall?

本小妞迷上赌 提交于 2019-12-23 01:18:30

问题


I have a chain of function calls and use async.waterfall. It works like a charm. But I'd like to do it with jQuery Deferred. How to transform my code?

The example from jQuery site is like this. Both results are passed to done function:

$.when( $.ajax( "/page1.php" ), $.ajax( "/page2.php" ) ).done(function( a1, a2 ) {
  // a1 and a2 are arguments resolved for the page1 and page2 ajax requests, respectively.
  // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
  var data = a1[ 0 ] + a2[ 0 ]; // a1[ 0 ] = "Whip", a2[ 0 ] = " It"
  if ( /Whip It/.test( data ) ) {
    alert( "We got what we came for!" );
  }
});

But my code is different. I need to pass a callback to every step of a waterfall and I have ifs in callbacks. How to implement it with jQuery? Is it possible?

async.waterfall([
        function(cb) {
            VK.Api.call('users.get', {user_ids: res.session.mid, fields: fields}, function(userDataRes) {
                cb(null, userDataRes);
            });
        },
        function(userDataRes, cb) {
            if(userDataRes.response[0].city) {
                VK.Api.call('database.getCitiesById', {city_ids: userDataRes.response[0].city}, function(cityDataRes) {
                    cb(null, userDataRes, {city: cityDataRes.response[0].name});
                });
            }
            else {
                cb(null, userDataRes, {});
            }
        },
        function(userDataRes, cityDataRes, cb) {
            if(userDataRes.response[0].country) {
                VK.Api.call("database.getCountriesById", {country_ids: userDataRes.response[0].country}, function(countryDataRes) {
                    cb(null, userDataRes, cityDataRes, {country: countryDataRes.response[0].name});
                });
            }
            else {
                cb(null, userDataRes, {}, {});
            }
        },
        function(userDataRes, cityDataRes, countryDataRes, cb) {
            var resObj = $.extend(true, {}, userDataRes.response[0], cityDataRes, countryDataRes);
            cb(null, resObj);
        },
    ],
    function(err, res) {
        console.log("res::: ", res);
    }
);

UPD 1:

So, I've implemented a solution, but it doesn't work as expected. There is an asynchronous API function call in .then() and jQuery deferred flow is broken there. I don't know how to make a .then() function as an API callback.

var dfr = $.Deferred();

dfr.then(function(val) {

    // THIS is an asynchronous API function call. And its callback returns result that is passed to the next .then()
    // But jQuery deferred flow doesn't follow this API call. 
    // It goes along to the next .then ignoring this API call.
    // How to make it enter this API call and be returned from a API's callback.
    VK.Api.call('users.get', {user_ids: res.session.mid, fields: fields}, function(userDataRes) {
        // cb(null, userDataRes);
        console.log("countryDataRes: ", userDataRes);
        return userDataRes;
    });
}).
then(function(userDataRes) {
    console.log("countryDataRes: ", userDataRes);
    if(userDataRes.response[0].city) {
        VK.Api.call('database.getCitiesById', {city_ids: userDataRes.response[0].city}, function(cityDataRes) {
            // cb(null, userDataRes, {city: cityDataRes.response[0].name});
            return [userDataRes, {city: cityDataRes.response[0].name}];
        });
    }
    else {
        // cb(null, userDataRes, {});
        return [userDataRes, {}];
    }
}).
then(function(aRes) {
    if(aRes[0].response[0].country) {
        VK.Api.call("database.getCountriesById", {country_ids: aRes[0].response[0].country}, function(countryDataRes) {
            // cb(null, userDataRes, cityDataRes, {country: countryDataRes.response[0].name});
            return [aRes[0], aRes[1], {country: countryDataRes.response[0].name}];
        });
    }
    else {
        cb(null, aRes[0], {}, {});
    }
}).
then(function(aRes) {
    var resObj = $.extend(true, {}, aRes[0].response[0], aRes[1], aRes[2]);
    console.log("cityDataRes: ", aRes[1]);
    console.log("countryDataRes: ", aRes[2]);
    cb(null, resObj);
    return resObj;
}).
done(function(res) {
    console.log("res::: ", res);
});

dfr.resolve();

回答1:


Let's start with the general rule for using promises:

Every function that does something asynchronous must return a promise

Which functions are these in your case? Basically, the complete waterfall, each of the waterfall functions that took a cb and VK.Api.call.

Hm, VK.Api.call doesn't return a promise, and it's a library function so we cannot modify it. Rule 2 comes into play:

Create an immediate wrapper for every function that doesn't

In our case, it will look like this:

function callApi(method, data) {
    var dfr = $.Deferred();
    VK.Api.call(method, data, function(result) {
        dfr.resolve(result);
    });
    // No error callbacks? That's scary!
    // If it does offer one, call `dfr.reject(err)` from it
    return dfr.promise();
}

Now we have only promises around, and do no more need any deferreds. Third rule comes into play:

Everything that does something with an async result goes into a .then callback

…and returns its result.

That result might as well be a promise not a plain value, .then can handle these - and will give us back a new promise for the eventual result of executing the "something". So, let's chain some then()s:

apiCall('users.get', {user_ids: res.session.mid, fields: fields})
.then(function(userDataRes) {
    console.log("countryDataRes: ", userDataRes);
    if (userDataRes.response[0].city) {
        return apiCall('database.getCitiesById', {city_ids: userDataRes.response[0].city})
        .then(function(cityDataRes) {
            return [userDataRes, {city: cityDataRes.response[0].name}];
        });
    } else {
        return [userDataRes, {}];
    }
})
.then(function(aRes) {
    if (aRes[0].response[0].country) {
        return apiCall("database.getCountriesById", {country_ids: aRes[0].response[0].country})
        .then(function(countryDataRes) {
            return [aRes[0], aRes[1], {country: countryDataRes.response[0].name}];
        });
    } else {
        return [aRes[0], aRes[1], {}];
    }
})
.then(function(aRes) {
    var resObj = $.extend(true, {}, aRes[0].response[0], aRes[1], aRes[2]);
    console.log("cityDataRes: ", aRes[1]);
    console.log("countryDataRes: ", aRes[2]);
    return resObj;
})
.done(function(res) {
    console.log("res::: ", res);
});

At least, that's what your original waterfall did. Let's polish it up a bit by executing getCitiesById and getCountriesById in parallel, and removing all the boilerplate of explicitly creating these aRes arrays.

function callApi(method, data) {
    var dfr = $.Deferred();
    VK.Api.call(method, data, function(result) {
        dfr.resolve(result.response[0]);
//               changed: ^^^^^^^^^^^^
    });
    // No error callbacks? That's scary!
    // If it does offer one, call `dfr.reject(err)` from it
    return dfr.promise();
}
apiCall('users.get', {user_ids: res.session.mid, fields: fields})
.then(function(userData) {
    if (userData.city)
        var cityProm = apiCall('database.getCitiesById', {city_ids: userData.city});
    if (userData.country)
        var countryProm = apiCall("database.getCountriesById", {country_ids: userData.country});
    return $.when(cityProm, countrProm).then(function(city, country) {
        var resObj = $.extend(true, {}, userData);
        if (city)
            resObj.city = city.name;
        if (country)
            resObj.country = country.name;
        return resObj;
    });
})
.done(function(res) {
    console.log("res::: ", res);
});


来源:https://stackoverflow.com/questions/25724015/how-to-use-jquery-deferred-functionality-instead-of-async-waterfall

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!