Deferred promise or similar pattern that can be reset

I know this has been asked several ocassions, but I think that none of the Q&A in SO refers to the scenario explained in this question.

I've got a JavaScript component (it's a KO component, bu it doesn't matter). Depending on some actions in this component it can load asynchronously several different versions of a child component. This can happen several times. Ie originally it can load child A, and later replace it with child B ans so on.

The main component needs to run some functionality of the child components. To do so, it passes a function called registerApi to the child component constructors. In this way, when the child component finishes loading, it calls the registerApi . To avoid the parent from calling the API before the child component has been loaded, the registerApi uses a promise in this way:

var childApiDeferred = $.Deferred();
var childApiPromise = childApiDerred.promise();

var registerApi = function(api) {
  childApiDeferred.resolve(api);
};

In this way, whenever the main component needs to invoke the child component's API, it does it through the promise, so that, it will only run when the child component have finished loading and have registered its API. The methods in the child component API are invoked like this:

childApiPromise.then(function(api) { api.method(); })

So far, so good. The problem is that at some point in time, the child component can be swapped by a new one. At this point, I'd need to reset the deferred to an unresolved state, so that, if the main component tries to call the API it has to wait until the new component has been loaded and has registered its API.

I have not seen any implementation of resettable deferreds, nor any other thing that can solve this problem. The only solution that I've found, and which makes the code much more complex (I'm not showing it), is, whenever a new component starts loading, I create a new deferred, and expose the promise of the new deferred, so that the calls to:

childApiPromise.then(function(api) { api.method(); })

always refer to the fresh new promise. (As I've said, that complicates the code).

Of course, for the first child component a deferred/promise works like a charm, but, I'm looking for something like a deferred which can be unresolved, so that I can unresolve it whenever a new component starts loading.

Is there a kind of resettable deferred or any other JavaScript pattern that allows to store the callbacks and invoke them when the new functionality is ready? Is it possible to implement it in any way?

How does the component swapping happen?

As I've been asked in a comment, I'll explain it, although I think it's not relevant for this Q.

I'm using Knockout with components. I have a main component which has a child component. The child component is swapped automatically by KO infrastructure when I change a property that holds the component's name. At this point in time I know that I have to "suspend" the calls to the child component's API, because a new child is being loaded, and the API isn't available at this moment. The child component loads asynchronously and receives a series of parameters, including the registerApi callback from the main component. When the child component finishes loading, it calls registerApi to expose its API to the parent component. (As a side note, all the child components expose the same functionality to the main component, but with different implementations).

SO, this is are the steps that happen when swapping a component:

  • The main component creates a new deferred "childApiDeferred", which is unresolved
  • The main component sets the new child component's name, so that ko infrastructure swaps the child component, and passes the registerApi callback to the child component
  • The child component is loaded asynchronously, and, when it finishes loading, it calls the registerApi , which also resolves the deferred
  • The parent component can safely makes calls to the child API through the promise, because they won't be executed until it's resolved
  • Deleting and creating a new promise each time works fine. The problem is that this requires to write a lot of code to ensure that this is done correctly and that all the code uses the new, and not the old, deferred or promise. If the promise where resettable You'd simply had to reset and resolve it: two simple lines of code.


    By definition, Promises can only be fulfilled or rejected once . So you will not be able to reset the deferred Promise. I would suggest that the structure of your code needs to change in order to obey a more Promisey architecture.

    For instance perhaps all the functions that the main component calls on the child component should return Promises. If the child is already loaded, then that Promise will be resolved immediately. Something like this;

    class Child {
      apiMethod() {
        return this.waitForApiRegister()
          .then(() => {
            // do api stuff and return result
            return res;
          })
      },
      waitForApiRegister() {
        return ((this.apiPromise) || this.apiPromise = $.Deferred());
      }
      registerApi() {
        this.apiPromise.resolve();
      }
      unregisterApi() {
        this.apiPromise = undefined;
      }
    }
    

    You'd then call it as a normal Promise, knowing the the Promise will only be resolved if the API is registered.

    const c = new Child();
    c.apiMethod().then(res => {
      console.log(res);
    });
    

    If the child changes or for whatever reason you need to reset the Promise, you just set it to null and the next time an api function is called a new Promise will be created.


    Give this a go. I've not tested, but think it should work.

    function ApiAbstraction() {
        var deferred;
        this.promise = null;
        this.set = function(kill) {
            if(kill) {
                deferred.reject('this child never became active'); // This will affect the initial `deferred` and any subsequent `deferred` if not yet resolved.
            }
            deferred = $.Deferred();
            this.promise = deferred.promise();
            return deferred.resolve;
        };
        this.set(); // establish an initial (dummy) deferred (which will never be resolved).
    };
    

    First, create an instance of ApiAbstraction :

    var childAbstract = new ApiAbstraction();
    

    You can have as many instances as you want, for different types of children, but probably need just the one.

    Then, to load a new child (including the initial child), let's assume an asynchronous myChildLoader() method - then you have two choices :

    Either :

    var registerChild = childAbstract.set();
    myChildLoader(params).then(registerChild);
    

    Or :

    var registerChild = childAbstract.set(true);
    myChildLoader(params).then(registerChild);
    

    Both versions will cause any fresh .then() etc callbacks to attach to the new child immediately, even if it has not yet delivered. No semaphore required .

    The kill boolean may be useful but is limited to killing any callbacks attached to the previous child only if it's not yet been delivered. If is was already delivered, then its Deferred will (in all probability) be resolved and any attached callbacks either executed or irrevocably in the process of execution. Promises do not include a mechanism to withdraw attached callbacks.

    If it turns out that there's no value in the kill option, then it can be removed from the constructor.

    It's tempting but not recommended to write :

    myChildLoader(params).then(childAbstract.set());
    

    That (with or without the kill boolean) would cause any fresh .then handlers to attach to the old child until the new child became available. However, a dangerous race would occur with any other similar expressions anywhere in the code-base. The last to deliver its child would win the race (not necessarily the last one called). ApiAbstraction() would need to be significantly modified to ensure that a race did not occur (difficult?). Anyway, probably academic because you most probably want to switch to the new child ASAP.

    Elsewhere in the code-base you need to invoke child methods via the ApiAbstraction() instance, which needs to be in scope in exactly the same way child would be if its API was called directly, For example :

    function doStuffInvolvingChildAPI() {
        ...
        var promise = childAbstract.promise.then(function(child) {
            child.method(...);
            return child; // make child available down the success path
        }); // ... and form a .then chain as required 
        ...
        return promise;
    }
    

    childAbstract.promise.then(...) is at worst mildly cumbersome.

    Although the explanation is long, this will give you the "two simple lines of code" you seek to swap children, ie :

    var registerChild = childAbstract.set();
    myChildLoader(params).then(registerChild);
    

    As stated in the question, "deleting and creating a new promise each time works fine" - and that's exactly what the .set() method does. The problem of what to write to ensure that all the code uses the new and not the old child is looked after by the abstraction.

    链接地址: http://www.djcxy.com/p/55432.html

    上一篇: 斯卡拉:承诺是多余的?

    下一篇: 延期承诺或可以重置的类似模式