Chaining promises without using 'then'

2019-01-26 21:00发布

问题:

I have an object (foo) that exposes several methods as promises (using JQuery deferred). The way I did it ended up with this kind of code:

var foo = createNewFoo();
$.when(foo.method1(arg))
    .then(foo.method2)
    .then(foo.method3);

I wish to refactor my code to something nicer, like this:

var foo = createNewFoo()
    .method1(arg)
    .method2()
    .method3();

But I'm not sure how to implement foo so it would be possible.

回答1:

Yes sure, you just need to extend your Deferreds to have these methods:

function MyRpc { // if you can use ES2015 - this should be a `class`
  this._deferred = new $.Deferred();
}
// teach it to be a promise
MyRpc.prototype.then = function(onFulfilled, onRejected) {
  return this._deferred.then(onFulfilled, onRejected);
};

// teach it to be a deferred
MyRpc.protototype.resolve = function(arg) {
  this._deferred.resolve(arg);
};

MyRpc.prototype.reject = function(arg) {
  this._deferred.reject(arg);
};

// define your methods!

MyRpc.prototype.method1 = function(arg) {
  var p = this._deferred.then(function(value) {
    // logic of `method1` from foo.method1 here
  });
  var rpc = new MyRpc(); // our continuation;
  p.then(function(v) { rpc.resolve(v) }, function(e) { rpc.reject(e); });
  return rpc;
};

Of course, with a real promise library all this is a lot easier than with jQuery's minimal promises.

This would let you do:

var rpc = new MyRpc();
rpc.method1(1).method1(2).method1(3); // can also `.then` here

I'm not sure it's worth it, but it works.



回答2:

You will need to return a custom object with the methods you need, and let it have a promise for the state instead of the state as properties directly. In every of your methods, you'd need to call then on the wrapped promise, and return another instance that wraps a new promise for the new state (method result).

function Foo(promise) {
    // make every instance a thenable:
    this.then = promise.then.bind(promise);
    // alternatively store the promise itself as a property and always call this.promise.then
}
function createNewFoo() {
    return new Foo($.when({/* initial state */}));
}
Foo.prototype.method1 = function method1(args) {
    return new Foo(this.then(function(curstate) {
        // method logic here
        // should `return` a new state - or a promise for it
    });
};
Foo.prototype.method2 = …;

This is similar to the approaches outlined by Benjamin Gruenbaum and Louy, but with a much simpler implementation.



回答3:

I'm not really aware of Promises (so forgive my example if there is some implementation error) but this is possible using ES6 Proxy.

At this time it's only available on the latest browsers so it might not meet your requirements.

A Proxy allow to add a callback function on each object operations, it mean that you can retrieve the name of the called method to do what you want. In your case you want to call $.when() with the first promise and call .then() with others as parameter.

"use strict";
function createNewFoo(){
    let foo = new Foo();

    let proxy = new Proxy(foo, {

        // @param target : Object on which the operation will be made
        // @param name : property name
        get(target, name) {

            let promise = target[name];

            return function (...args) {

                if (typeof promise === "function"){
                    promise = promise.apply(target, args);
                }
                else if (!(promise instanceof Promise)){
                    throw 'Can\'t handle "'+name+'"';
                }

                // Perform chaining
                if (!target.promise){
                    target.promise = $.when(promise);
                }
                else{
                    target.promise.then(promise);
                }
                return proxy;
            };
        }
    });
    // Storing the Foo instance in the Proxy object, could be implemented in other way.
    proxy._target = foo;
    return proxy;
}

Assuming you're using a "class" defined as bellow

function Foo(){
    // storing the promise result
    this.promise = null;

    this.method1 = function(arg){ 
        return new Promise(function(resolve, reject) {
            resolve("1");
        }); 
    }

    this.method2 = new Promise(function(resolve, reject) {
        resolve("2");
    });
    this.method3 = new Promise(function(resolve, reject) {
        resolve("3");
    });
}

You can now use this piece of code to chain your promises

var fooProxy = createNewFoo()
    .method1(1)
    .method2()
    .method3();

And retrieve the original Foo instance

fooProxy._target 


回答4:

Create your own promise.

function MyPromise(resolver) {
  var _promise = new Promise(resolver);
  this.then = function(onFulfilled, onRejected) {
    var _p = _promise.then(onFulfilled, onRejected);
    return new MyPromise(_p.then.bind(_p));
  };
}

Add whatever methods you want...

MyPromise.prototype.doSomething = function() {
  return this.then(/*...*/);
};

voila!

new MyPromise()
  .then(/*...*/).doSomething()
  .then(/*...*/).doSomething(); // etc...

Bonus:

['resolve', 'reject', 'all'].forEach(function(method) {
  MyPromise[method] = function(...args) {
    var promise = Promise[method](...args);
    return new MyPromise(promise.then.bind(promise));
  };
});

Also in ES6:

class MyPromise {
  constructor(resolver) {
    var _promise = new Promise(resolver);
    this.then = function(onFulfilled, onRejected) {
      var _p = _promise.then(onFulfilled, onRejected);
      return new MyPromise(_p.then.bind(_p));
    };
  }
  doSomething() {
    return this.then(/*...*/);
  }
}