0
\$\begingroup\$

I have an object that needs to have a single point of initialization, and other callers need to wait until the initialization is complete before continuing.

I have done this with jQuery, and it feels like I'm working with the design, but the same approach with a native JavaScript Promise seems like it is going against the grain.

I need to replace use of jQuery promise with a native JS Promise.

Is this initialization pattern OK, or is there a better approach?

On the surface it seems that an event pub/sub pattern would be a better fit, but I can't guarantee that the init() will be called before the ready() is called, which would result in missed events.

jQuery

function Foo() {

    var dfd= $.Deferred();
    this.ready = function () {
        return dfd.promise();
    }
    this.init = function(){
        setTimeout(function(){
            dfd.resolve();
        }, 2000);
    }
};

var foo = new Foo();

//foo.ready() may be called from multiple places.
foo.ready().then(function () {
    alert('foo.ready');
});

//foo.init would only be called once.
foo.init();

JavaScript Promise

function Foo() {

    var resolver,
        rejector,
        readyPromise = new Promise(function (resolve, reject) {
            resolver = resolve;
            rejector = reject;
        });

    this.ready = function () {
        return readyPromise;
    }
    this.init = function () {
        setTimeout(function () {
            resolver();
        }, 2000);
    }
};

var foo = new Foo();

//foo.ready() may be called from multiple places.
foo.ready().then(function () {
    alert('foo.ready');
});

//foo.init would only be called once.
foo.init();
\$\endgroup\$
3
\$\begingroup\$

If I understand your question correctly, your question runs on these assumptions:

  • You want to hook up multiple listeners to a single Promise.
  • The promise is triggered arbitrarily, not necessarily on promise creation.
  • You want to use native promises.
  • You run on an engine that has Promises.

The second bullet point, where the operation is triggered arbitrarily, is the tricky part because native promises are read-only. You'll need to catch resolve and reject from the constructor every time.

So while your implementation is correct, you'll find yourself repeating the pattern. jQuery Deferreds are better in this case, but we don't want to lug around jQuery, or any library for that matter.

What you could do is create your own little Deferred constructor. It just wraps around the existing native Promise API, essentially just forwarding arguments. The only difference is that it's exposing resolve and reject.

function Deferred(callback){
  var instance = this;

  // Catch the resolve and reject
  this._resolver = null;
  this._rejector = null;
  this._promise = new Promise(function(resolve, reject){
    instance._resolver = resolve;
    instance._rejector = reject;
  });

  // Deferred has { resolve, reject }. But personally, I like the Promise
  // version of resolve and reject as separate args.
  if(typeof callback === 'function')
    callback.call(this, this._resolver, this._rejector);
}

Deferred.prototype.then = function(resolve, reject){
  return this._promise.then(resolve, reject);
};

// resolve, reject etc.

// For other APIs, refer to jQuery for Deferred and MDN for Promises:
// https://api.jquery.com/category/deferred-object/
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Now with Deferred out of the way, let's go back to your jQuery version and improve it. Since we created Deferred as a constructor, we can subclass from it. This means we can have Foo become a Deferred and have the luxury of the Deferred API.

// Setup Foo as a Deferred in as little as 2 lines.
function Foo(){ Deferred.apply(this, arguments); }
Foo.prototype = Object.create(Deferred.prototype);

// Your Foo API
Foo.prototype.init = function(){
  var instance = this;
  setTimeout(function(){
    instance.resolve();
  }, 2000);
};

In the end, foo will have Deferred powers, care of an extended Promise API.

var foo = new Foo();

// We can now use `then`.
foo.then(function(){
  alert('foo.ready!');
});

//
foo.init();

Now if I wrote the code properly, this should work:

function Deferred(callback) {
  var instance = this;
  this._resolver = null;
  this._rejector = null;
  this._promise = new Promise(function(resolve, reject) {
    instance._resolver = resolve;
    instance._rejector = reject;
  });
  if (typeof callback === 'function')
    callback.call(this, this._resolver, this._rejector);
}

Deferred.prototype.then = function(resolve, reject) { return this._promise.then(resolve, reject); };
Deferred.prototype.resolve = function(resolution) { this._resolver.call(null, resolution); return this; };
Deferred.prototype.reject = function(rejection) { this._rejector.call(null, rejection); return this; };

// The setup code:

function Foo() { Deferred.apply(this, arguments); }
Foo.prototype = Object.create(Deferred.prototype);

Foo.prototype.init = function() {
  var instance = this;
  setTimeout(function() {
    instance.resolve('YAY!');
  }, 2000);
};

// Phew! Now your Foo is a "native" Deferred.

var foo = new Foo();

foo.then(function(value) {
  alert('foo.ready! ' + value);
});

foo.init();

\$\endgroup\$
  • \$\begingroup\$ Thanks for verifying the implementation. Can you provide any thoughts on whether events would be more suitable, given the note at end of my question? \$\endgroup\$ – Joseph Gabriel Sep 28 '15 at 14:51
  • 1
    \$\begingroup\$ @JosephGabriel Your observation regarding events is correct. There ambiguity when an event has already fired, and a handler is attached after it. Events can also fire more than once, making it more confusing. Events are for messaging, fire and forget, and don't keep state. Promises, however, do keep resolution/rejection which makes it good in your init scenario. \$\endgroup\$ – Joseph Sep 28 '15 at 15:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.