Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: implement ready without Deferred #2850

Closed
wants to merge 11 commits into from
Closed

Conversation

timmywil
Copy link
Member

  • Keeps it Promise-compatible
  • +39 bytes
  • Keeps jQuery.ready to avoid breakage on wikipedia.

Fixes gh-1778

- Keeps it Promise-compatible

Fixes jquerygh-1778
@mention-bot
Copy link

By analyzing the blame information on this pull request, we identified @markelog and @mgol to be potential reviewers

}
} );

// Make jQuery.ready promise compatible (gh-1778)
jQuery.ready.then = jQuery.fn.ready;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may want to add some unit tests using a Promise lib, or just plain old native Promises. Since the implementation of jQuery.fn.ready returns this, and you're assigning that function to your thenable object, you're essentially getting into this scenario:

var myCrazyPromise = { then: function() { return this; };
Promise.resolve(myCrazyPromise).then(console.log.bind(console));

If you run that, you'll see that the then handler is never invoked since this is infinitely recursive

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added, but no changes required. Notice that while jQuery.ready is a thenable, jQuery.fn.ready is not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, thinking about this wrong. Maybe it's only a problem pre-ready event.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about it wrong (context is still jQuery.ready), but I don't see the problem you're describing whether it's before the ready event or not. Do you see anything wrong with my test?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, blurp, I think this is a misunderstanding of Promise.resolve(thenable). Promise.resolve(thenable) follows the thenable and eventually defers to its state. This means that the thenable is responsible for calling .then. Notice that your example doesn't resolve even when you don't return this. For example, this works fine...

var myCrazyPromise = { then: function (fn) { fn(); return this; } };
Promise.resolve(myCrazyPromise).then(function() { console.log('resolved'); });
// Promise instance resolves asynchronously

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I agree with that. Apparently I'm not as familiar with thenables as I thought.

FWIW part of my confusion may be attributed to something that seems weird in Chrome, but I have no idea if this is a legitimate thing.

If you run Promise.resolve(jQuery.ready) then inspect the internal [[PromiseValue]] the returned promise, you can see that the value the promise was resolved with is not what was passed to the .then handler; the resolved value is jQuery.ready as I had initially expected. Might be an implementation and/or devTools bug, but it doesn't look like it has any observable side effects other than confusion.

whenReady = function( fn ) {
readyCallbacks.push( fn );

if ( !++readyFiring ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the easiest to read. Would breaking it up increase the size? What size penalty would it bring to just use booleans?

I'd prefer to such "clever coding" to be performed by Uglify, not in source.

while ( readyCallbacks.length ) {
fn = readyCallbacks.shift();
if ( jQuery.isFunction( fn ) ) {
fn.call( document, jQuery );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the invocations are synchronous and not wrapped in a try block, jQuery.ready is not a conforming Promises/A+ promise. This should either include the explanatory comment from #1778 (comment) or be corrected (not that that would be sufficient, but it would prevent callback lockups).

@gibson042
Copy link
Member

Other than the missing comment or missing safeguards (depending on your preference), this looks good.

Also, what of the deprecations from #1778 (comment) ? Have we done those?

  • Deprecate jQuery( document ).ready( fn ) etc. in favor of jQuery( fn )
  • Deprecate jQuery.holdReady (a nasty global behavior modifier better covered by case-specific deferred/promise patterns)

}
} );

// Make jQuery.ready promise compatible (gh-1778)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd update this to "promise-consumable"; it's more clear and might head off too much effort being spent on bugs like jQuery.ready.then( fn ).then(…) not working as expected.

@timmywil
Copy link
Member Author

I'm not in favor of deprecating jQuery( document ).ready( fn ). It existed before jQuery( fn ) and imo is more semantic. I am in favor of deprecating jQuery.holdReady tho.

@markelog
Copy link
Member

Given how code is started to look like without the Deferred dep and because custom build is rarity in our user base (that would be an assumption) and since no one actually asked for it - i'm against that change.

I'm not in favor of deprecating jQuery( document ).ready( fn )

Me too, but i would wait long time before removing it, next couple majors or so.

I am in favor of deprecating jQuery.holdReady tho.

Why? It seems it still has valid use-cases?

@timmywil
Copy link
Member Author

Me too, but i would wait long time to remove it, next majors.

I think you misread. I don't want to deprecate that.

Why? It seems it still has valid use-cases?

@gibson042 pointed out those cases can be covered by promises.

@markelog
Copy link
Member

I think you misread. I don't want to deprecate that.

Oh, i did, sorry.

@gibson042 pointed out those cases can be covered by promises.

Like?

@gibson042
Copy link
Member

I'm not in favor of deprecating jQuery( document ).ready( fn ). It existed before jQuery( fn ) and imo is more semantic.

Really? Because I contend that all semantic meaning is lost by ignoring the argument to jQuery—the following are all equivalent:

  • jQuery( document ).ready( fn )
  • jQuery().ready( fn )
  • jQuery( "<div/>" ).ready( fn )
  • jQuery( [] ).ready( fn )
  • jQuery( jQuery.noop ).ready( fn )
  • jQuery.fn.ready( fn )

@gibson042
Copy link
Member

@gibson042 pointed out those [jQuery.holdReady] cases can be covered by promises.

jQuery.when( jQuery.ready, jQuery.Deferred( waitForMyDependencies ) )

@timmywil
Copy link
Member Author

Because I contend that all semantic meaning is lost by ignoring the argument to jQuery.

Agreed, but I don't see that often. I was referring to how jQuery( document ).ready() reads, and I like that.

@dmethvin
Copy link
Member

Yeah, jQuery( document ).ready() certainly seems to be the most poetic to me. I concede that it's mostly in the eye of the beholder though. 😄

@markelog
Copy link
Member

jQuery.when( jQuery.ready, jQuery.Deferred( waitForMyDependencies ) )

And what if you want other plugins that depend on ready event to hold?

@gibson042
Copy link
Member

Then those plugins would appear in waitForMyDependencies, and theirselves pay attention to jQuery.ready.

@dmethvin
Copy link
Member

Although there are a lot of dups here, seems like .holdReady is pretty commonly used. One of those is stealjs, i think they said they've moved away from it but still a lot out there.

https://github.com/search?q=jquery.holdReady+true&type=Code&utf8=✓

@gibson042
Copy link
Member

That's fine. This is a deprecation, not a removal.

@markelog
Copy link
Member

Then those plugins would appear in waitForMyDependencies, and theirselves pay attention to jQuery.ready.

User usually download their deps with builded file, which means we affect how app should be loaded and initiated, it seems we became opinionated here

@markelog
Copy link
Member

Whereas waitForMyDependencies approach is really not common these days, but holdReady is

@markelog
Copy link
Member

Whereas waitForMyDependencies approach is really not common these days

Buy that i mean, as i understand, you want user to push plugins load resolve and data on which those plugins operate under waitForMyDependencies promise

@dmethvin
Copy link
Member

Looking at some of the usage it seems like many people have taken the action-at-a-distance behavior of .holdReady as a feature rather than a programming atrocity. I'd actually be for deprecating this just to let people know they should find a better way, and show that better way in the docs. Who knows when it could actually be removed.

@markelog
Copy link
Member

Looking at some of the usage it seems like many people have taken the action-at-a-distance behavior of .holdReady as a feature rather than a programming atrocity.

Well, probably because we didn't use word "atrocity" but "advanced feature" in the docs :-)

@timmywil
Copy link
Member Author

I think we agree that holdReady is best avoided, so let's go with the deprecation, but no set time for removal.

@gibson042
Copy link
Member

you want user to push plugins load resolve and data on which those plugins operate under waitForMyDependencies promise

I'm not sure what you mean... any dependencies either expose their own thenable (valid as a jQuery.when argument), or the conditions can be manually checked in a waitForMyDependencies function as shown above (responsible for resolving a Deferred or Promise once they are satisfied).

@timmywil
Copy link
Member Author

Added a fix for gh-1823.

}
}
} finally {
readyFiring = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As coded, this still halts execution until a subsequent handler is added. But you could fix it with a setTimeout( whenReady ) here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you say, this only fixes the case where ready could never be called again. I'm not sure we need to go beyond that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would certainly be more Promise-compatible to have

jQuery.ready.then( throws );
jQuery.ready.then( shouldRun );

work without waiting for a subsequent jQuery.ready.then invocation. And fixes don't get much simpler than setTimeout( whenReady ).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we'd have to add a catch. Wouldn't putting the whenReady call in the finally block cause an infinite loop?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need a catch, and the whenReady doesn't go infinite because the throwing callback has already been .shift()ed off the list.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how it wouldn't be a loop. A finally is called regardless of whether an error is thrown, so it would just keep calling itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So by not having a catch, it also barfs immediately on the console right? That's a good thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It keeps calling itself, but will terminate when the callbacks list is empty: https://jsfiddle.net/ewjschzw/

So by not having a catch, it also barfs immediately on the console right? That's a good thing.

Yes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops; I see what you mean @timmywil... you just need to move the try into the loop (I thought it was already there) check readyCallbacks.length.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, that would work.

var readyCallbacks = [],
readyFiring = false,
whenReady = function( fn ) {
readyCallbacks.push( fn );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it help for the check for fn being a function be done here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as you duplicate it in the redefinition of whenReady. Doing it this way is just conceptually simpler, and it's not like the function test is expensive.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, given that it's inexpensive, I'd want to do whichever is smaller. Right now, that would be what we currently have.

// If there was an error in a ready callback,
// continue with the rest (gh-1823)
if ( readyCallbacks.length ) {
whenReady();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retrying synchronously here swallows all but the last exception generated by a block of readyCallbacks (e.g., jQuery.ready.then( thrower ); jQuery.ready.then( thrower )): https://jsfiddle.net/ewjschzw/1/

I think a setTimeout is necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, didn't notice that.

@@ -67,4 +91,32 @@ QUnit.module( "ready" );
"Argument passed to fn in jQuery(document).ready( fn ) should be jQuery" );
} );

QUnit.test( "Promise.resolve(jQuery.ready)", function( assert ) {
assert.expect( 2 );
var done = jQuery.map( new Array( 2 ), function() { return assert.async(); } );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole point of deprecating stop() semaphore in favour of assert.async() was to discourage fragile patterns like this. The code should reflect care of which callback gets popped by which sub path. E.g. create them explicitly and have each mark its own subpath as complete.

var resolveDone = assert.async(),
    promisifiedDone = assert.async();

..
    promisifiedDone();
..
    resolveDone();

Otherwise, at least simplify with var done = [ assert.async(), assert.async() ];.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pattern we already use in several places. Regardless, we'll switch patterns when we update QUnit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to somehow detect such pattern? You mean update to 2.0... or?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krinkle assert.async now accepts a number specifying how many times its result should get called so this is sort of blessed by upstream currently... We planned to switch to this pattern once we update QUnit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated QUnit to 1.20.0, so after rebase, you can start using new signature in this pull

@markelog
Copy link
Member

markelog commented Feb 2, 2016

Even though we unanimously decided to choose a different approach it's still would be cool to hear @scottgonzalez opinion.

@scottgonzalez
Copy link
Member

I don't have much to add here. I'll just say that I think jQuery.ready is a nice feature, though easily implemented in user land; and jQuery.holdReady() can go away and I'd wouldn't think twice about it.

@timmywil
Copy link
Member Author

Closing in favor of #2891

@timmywil timmywil closed this Feb 10, 2016
@timmywil timmywil deleted the ready branch February 10, 2016 21:18
@lock lock bot locked as resolved and limited conversation to collaborators Jan 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

10 participants