Synchronizing event-based plugin calls in JavaScri

2019-08-12 14:56发布

问题:

I'm writing some JavaScript that interacts with a browser plugin (add-on in FF, ActiveX in IE). Some of the calls are asynchronous and trigger an event when complete. There are certain cases where I need to chain calls together, but need to wait for the first call to complete before initiating the second call. For instance:

A call to attach a device needs to first check if a device is already attached, and disconnect it. So the calls would be:

pluginObject.disconnectDevice(deviceKey);
pluginObject.connectDevice(deviceKey, backingInfo);

Just calling them straight up like this would fail because a connect is initiated before the disconnect is actually complete, and the plugin can't handle that.

I've already got listeners set up and I can do something like:

function handleConnectionChange(connectionState, /* ... */, doReconnect) {
    if (connectionState === state.disconnected && doReconnect) {
        pluginObject.connectDevice(deviceKey, backingInfo);
    }
}

But I'd actually like a more generic approach -- one that is reusable even with a different set of chained events.. This is simple enough in this particular instance, but even this is a little sloppy. I've added reconnect information logic to the connection event handler and I could easily see conditional chaining getting out of hand when done like this.

Are there any libraries out there that tackle this kind of workflow? I looked at jQuery's deferred stuff and it has the chaining that I want, but it doesn't really seem to fit with callbacks. Sproutcore's StateCharts look interesting, but again don't seem to have the ability to rework an already asynchronous flow.

Update: Solution Thanks to some pointers from Gustavo, I've gone with the approach of wrapping the plugin calls to make them utilize jQuery Deferred objects:

var connectDevice = function(deviceKey, backingInfo) {
    var deferred = $.Deferred();
    var connectHandler = function(event) {
        // unregister connectHandler
        if (event.connectionState === ERROR) {
            showAlert("Error connecting device", event.message);
            deferred.reject();
        } else {
            showAlert("Device connected", event.message);
             deferred.resolve();
        }
    };

    // register connectHandler
    pluginObject.connectDevice(deviceKey, backingInfo);
    return deferred.promise();
};

If I make a similar wrapper for disconnecting, I can now chain the method calls (or not depending on the flow):

if (forceDisconnect) {
    disconnectDevice(deviceKey).done(connectDevice(deviceKey, backingType));
} else {
    connectDevice(deviceKey, backingType);
}

I can also start a chain with the connectDevice, or add another function to the forceDisconnect flow, etc.

回答1:

Why do you say Deferreds don't fit with callbacks? It seems to me that

pluginObject.disconnectDevice(deviceKey).then(function() { 
    pluginObject.connectDevice(deviceKey, backingInfo);
});

reads pretty natural.

If you can't change the plugin code, you can still use this approach, but of course you'll have to wrap the plugin in some sort of API adapter. Basically, each call to disconnectDevice in your wrapper would create a Deferred, register it in a queue and return it. When you receive the disconnection event, you resolve all pending deferreds in order (be careful with re-entrance).

You'd need to do this for every event fired by the plugin that you want to handle.

The deferred approach has the benefit of improved code readability and maintainability IMHO (over the event listener API).



回答2:

Can't you just add a callback to disconnectDevice and pass in the call to connectDevice?

pluginObject.disconnectDevice = function ( key, callback ) {
    // do stuff and then when disconnect complete...
    callback();
};

...then when you want to attach a device...

pluginObject.disconnectDevice(deviceKey, function() {
    pluginObject.connectDevice(deviceKey, backingInfo);
});


回答3:

Assuming you set the handler with a simple property called onConnectionChange, you can write a new function that takes a callback instead.

// The new function, it takes a callback to let you know
// that disconnecting is done
pluginObject.disconnect = function (deviceKey, callback) {
     var me = this;
     me.onConnectionChange = function (connectionState) {
        if (connectionState === state.disconnected) {
             delete me.onConnectionChange;
             callback();
        }
}

// Now you can call
pluginObject.disconnect(deviceKey, function() {
    pluginObject.connectDevice(deviceKey, backingInfo);
});